finished the public page
This commit is contained in:
parent
b50ec09727
commit
614d92f050
|
@ -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">
|
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wider">
|
||||||
{name}
|
{name}
|
||||||
</p>
|
</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}
|
{value}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default function FilterSearchDropdown({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Text Content"
|
label="Full Content"
|
||||||
state={searchFilter.textContent}
|
state={searchFilter.textContent}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSearchFilter({
|
setSearchFilter({
|
||||||
|
|
|
@ -20,7 +20,7 @@ type Props = {
|
||||||
onClick?: Function;
|
onClick?: Function;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsSidebar({ className, onClick }: Props) {
|
export default function LinkSidebar({ className, onClick }: Props) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const userId = session.data?.user.id;
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,14 @@ export default function PreservedFormats() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let interval: NodeJS.Timer | undefined;
|
let interval: NodeJS.Timer | undefined;
|
||||||
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
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 {
|
} else {
|
||||||
if (interval) {
|
if (interval) {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
|
|
@ -38,6 +38,7 @@ export default function ProfilePhoto({ src, className, priority }: Props) {
|
||||||
width={112}
|
width={112}
|
||||||
priority={priority}
|
priority={priority}
|
||||||
draggable={false}
|
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={`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 || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Link as LinkType, Tag } from "@prisma/client";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/client/isValidUrl";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import { TagIncludingLinkCount } from "@/types/global";
|
import { TagIncludingLinkCount } from "@/types/global";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface LinksIncludingTags extends LinkType {
|
interface LinksIncludingTags extends LinkType {
|
||||||
tags: TagIncludingLinkCount[];
|
tags: TagIncludingLinkCount[];
|
||||||
|
@ -27,75 +28,69 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={link.url} target="_blank" rel="noreferrer" className="rounded-3xl">
|
<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="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="flex justify-between items-end gap-5 w-full h-full z-0">
|
||||||
{url && (
|
<div className="flex flex-col justify-between w-full">
|
||||||
<>
|
<div className="flex items-center gap-2">
|
||||||
<Image
|
<p className="text-2xl">
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
{url && (
|
||||||
width={42}
|
<Image
|
||||||
height={42}
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||||
alt=""
|
width={30}
|
||||||
className="select-none mt-3 z-10 rounded-md shadow border-[3px] border-white bg-white"
|
height={30}
|
||||||
draggable="false"
|
alt=""
|
||||||
onError={(e) => {
|
className="select-none z-10 rounded-md shadow border-[1px] border-white bg-white float-left mr-2"
|
||||||
const target = e.target as HTMLElement;
|
draggable="false"
|
||||||
target.style.display = "none";
|
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}
|
{unescapeString(link.name || link.description)}
|
||||||
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>
|
</p>
|
||||||
<div className="flex gap-3 items-center flex-wrap my-3">
|
</div>
|
||||||
<div className="flex gap-1 items-center flex-wrap mt-1">
|
|
||||||
{link.tags.map((e, i) => (
|
<div className="flex gap-3 items-center flex-wrap my-2">
|
||||||
<p
|
<div className="flex gap-1 items-center flex-wrap">
|
||||||
key={i}
|
{link.tags.map((e, i) => (
|
||||||
className="px-2 py-1 bg-sky-200 text-black text-xs rounded-3xl cursor-pointer truncate max-w-[10rem]"
|
<Link
|
||||||
>
|
href={"/public/collections/20?q=" + e.name}
|
||||||
{e.name}
|
key={i}
|
||||||
</p>
|
className="px-2 text-xs rounded-md border border-black dark:border-white truncate max-w-[10rem] hover:opacity-50 duration-100"
|
||||||
))}
|
>
|
||||||
</div>
|
{e.name}
|
||||||
</div>
|
</Link>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:group-hover/item:block duration-100 text-slate-500">
|
<div className="flex gap-1 items-center flex-wrap text-sm text-gray-500 dark:text-gray-300">
|
||||||
<FontAwesomeIcon
|
<p>{formattedDate}</p>
|
||||||
icon={faChevronRight}
|
<p>·</p>
|
||||||
className="w-7 h-7 slide-right-with-fade"
|
<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 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-3 h-3 mt-[0.15rem]"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,13 @@ type Props = {
|
||||||
export default function PublicSearchBar({ placeHolder }: Props) {
|
export default function PublicSearchBar({ placeHolder }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(router);
|
router.query.q
|
||||||
});
|
? setSearchQuery(decodeURIComponent(router.query.q as string))
|
||||||
|
: setSearchQuery("");
|
||||||
|
}, [router.query.q]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center relative group">
|
<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 '%'.");
|
toast.error("The search query should not contain '%'.");
|
||||||
setSearchQuery(e.target.value.replace("%", ""));
|
setSearchQuery(e.target.value.replace("%", ""));
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) => {
|
||||||
e.key === "Enter" &&
|
if (e.key === "Enter") {
|
||||||
router.push(
|
if (!searchQuery) {
|
||||||
"/public/collections/" +
|
return router.push("/public/collections/" + router.query.id);
|
||||||
router.query.id +
|
}
|
||||||
"?q=" +
|
|
||||||
encodeURIComponent(searchQuery)
|
return router.push(
|
||||||
)
|
"/public/collections/" +
|
||||||
}
|
router.query.id +
|
||||||
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"
|
"?q=" +
|
||||||
|
encodeURIComponent(searchQuery || "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,8 +5,7 @@ import useModalStore from "@/store/modals";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Link from "next/link";
|
|
||||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
import {
|
import {
|
||||||
faPen,
|
faPen,
|
||||||
|
@ -66,22 +65,22 @@ export default function LinkLayout({ children }: Props) {
|
||||||
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
||||||
|
|
||||||
useEffect(() => {
|
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]);
|
}, [links]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (link)
|
if (link)
|
||||||
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
|
setLinkCollection(collections.find((e) => e.id === link?.collection.id));
|
||||||
}, [link]);
|
}, [link, collections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
<ModalManagement />
|
||||||
|
|
||||||
<div className="flex mx-auto">
|
<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 />
|
<LinkSidebar />
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
|
<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">
|
<div className="flex gap-3 mb-5 duration-100 items-center justify-between">
|
||||||
|
@ -93,84 +92,95 @@ export default function LinkLayout({ children }: Props) {
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<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"
|
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" />
|
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||||
Back{" "}
|
Back{" "}
|
||||||
<span className="hidden sm:inline-block">
|
<span className="hidden sm:inline-block">
|
||||||
to <span className="capitalize">{linkCollection?.name}</span>
|
to{" "}
|
||||||
|
<span className="capitalize">
|
||||||
|
{linkCollection?.name || link?.collection?.name}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:hidden">
|
<div className="flex gap-5">
|
||||||
<div className="flex gap-5">
|
{link?.collection?.ownerId === userId ||
|
||||||
{link?.collection.ownerId === userId ||
|
linkCollection?.members.some(
|
||||||
linkCollection?.members.some(
|
(e) => e.userId === userId && e.canUpdate
|
||||||
(e) => e.userId === userId && e.canUpdate
|
) ? (
|
||||||
) ? (
|
|
||||||
<div
|
|
||||||
title="Edit"
|
|
||||||
onClick={() => {
|
|
||||||
link
|
|
||||||
? setModal({
|
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
active: link,
|
|
||||||
method: "UPDATE",
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
}}
|
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faPen}
|
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
title="Edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
link
|
link
|
||||||
? setModal({
|
? setModal({
|
||||||
modal: "LINK",
|
modal: "LINK",
|
||||||
state: true,
|
state: true,
|
||||||
active: link,
|
active: link,
|
||||||
method: "FORMATS",
|
method: "UPDATE",
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
}}
|
}}
|
||||||
title="Preserved Formats"
|
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faBoxesStacked}
|
icon={faPen}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
{link?.collection.ownerId === userId ||
|
<div
|
||||||
linkCollection?.members.some(
|
onClick={() => {
|
||||||
(e) => e.userId === userId && e.canDelete
|
link
|
||||||
) ? (
|
? setModal({
|
||||||
<div
|
modal: "LINK",
|
||||||
onClick={() => {
|
state: true,
|
||||||
if (link?.id) {
|
active: link,
|
||||||
removeLink(link.id);
|
method: "FORMATS",
|
||||||
router.back();
|
})
|
||||||
}
|
: undefined;
|
||||||
}}
|
}}
|
||||||
title="Delete"
|
title="Preserved Formats"
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTrashCan}
|
icon={faBoxesStacked}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{link?.collection?.ownerId === userId ||
|
||||||
|
linkCollection?.members.some(
|
||||||
|
(e) => e.userId === userId && e.canDelete
|
||||||
|
) ? (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (link?.id) {
|
||||||
|
removeLink(link.id);
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Delete"
|
||||||
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faTrashCan}
|
||||||
|
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
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";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
|
|
||||||
export default async function getLinkById(userId: number, linkId: number) {
|
export default async function getLinkById(userId: number, linkId: number) {
|
||||||
|
@ -27,7 +27,7 @@ export default async function getLinkById(userId: number, linkId: number) {
|
||||||
status: 401,
|
status: 401,
|
||||||
};
|
};
|
||||||
else {
|
else {
|
||||||
const updatedLink = await prisma.link.findUnique({
|
const link = await prisma.link.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: linkId,
|
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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 };
|
||||||
|
}
|
|
@ -1,24 +1,35 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
userId: number;
|
userId?: number;
|
||||||
collectionId?: number;
|
collectionId?: number;
|
||||||
linkId?: number;
|
linkId?: number;
|
||||||
|
isPublic?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function getPermission({
|
export default async function getPermission({
|
||||||
userId,
|
userId,
|
||||||
collectionId,
|
collectionId,
|
||||||
linkId,
|
linkId,
|
||||||
|
isPublic,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
if (linkId) {
|
if (linkId) {
|
||||||
const check = await prisma.collection.findFirst({
|
const check = await prisma.collection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
links: {
|
[isPublic ? "OR" : "AND"]: [
|
||||||
some: {
|
{
|
||||||
id: linkId,
|
id: collectionId,
|
||||||
|
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
|
||||||
|
links: {
|
||||||
|
some: {
|
||||||
|
id: linkId,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
isPublic: isPublic ? true : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
});
|
});
|
||||||
|
@ -27,10 +38,15 @@ export default async function getPermission({
|
||||||
} else if (collectionId) {
|
} else if (collectionId) {
|
||||||
const check = await prisma.collection.findFirst({
|
const check = await prisma.collection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
AND: {
|
[isPublic ? "OR" : "AND"]: [
|
||||||
id: collectionId,
|
{
|
||||||
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
|
id: collectionId,
|
||||||
},
|
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isPublic: isPublic ? true : undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
include: { members: true },
|
include: { members: true },
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
import readFile from "@/lib/api/storage/readFile";
|
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) {
|
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.query.params)
|
if (!req.query.params)
|
||||||
return res.status(401).json({ response: "Invalid parameters." });
|
return res.status(401).json({ response: "Invalid parameters." });
|
||||||
|
|
||||||
const user = await verifyUser({ req, res });
|
const token = await getToken({ req });
|
||||||
if (!user) return;
|
const userId = token?.id;
|
||||||
|
|
||||||
const collectionId = req.query.params[0];
|
const collectionId = req.query.params[0];
|
||||||
const linkId = req.query.params[1];
|
const linkId = req.query.params[1];
|
||||||
|
|
||||||
const collectionIsAccessible = await getPermission({
|
const collectionIsAccessible = await getPermission({
|
||||||
userId: user.id,
|
userId,
|
||||||
collectionId: Number(collectionId),
|
collectionId: Number(collectionId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -2,26 +2,43 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import readFile from "@/lib/api/storage/readFile";
|
import readFile from "@/lib/api/storage/readFile";
|
||||||
import verifyUser from "@/lib/api/verifyUser";
|
import verifyUser from "@/lib/api/verifyUser";
|
||||||
|
import { getToken } from "next-auth/jwt";
|
||||||
|
|
||||||
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const queryId = Number(req.query.id);
|
const queryId = Number(req.query.id);
|
||||||
|
|
||||||
const user = await verifyUser({ req, res });
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
if (!queryId)
|
if (!queryId)
|
||||||
return res
|
return res
|
||||||
.setHeader("Content-Type", "text/plain")
|
.setHeader("Content-Type", "text/plain")
|
||||||
.status(401)
|
.status(401)
|
||||||
.send("Invalid parameters.");
|
.send("Invalid parameters.");
|
||||||
|
|
||||||
if (user.id !== queryId) {
|
const token = await getToken({ req });
|
||||||
const targetUser = await prisma.user.findUnique({
|
const userId = token?.id;
|
||||||
|
|
||||||
|
const targetUser = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: queryId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
whitelistedUsers: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (targetUser?.isPrivate) {
|
||||||
|
if (!userId) {
|
||||||
|
return res
|
||||||
|
.setHeader("Content-Type", "text/plain")
|
||||||
|
.status(400)
|
||||||
|
.send("File inaccessible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: queryId,
|
id: userId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
whitelistedUsers: true,
|
subscriptions: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -29,15 +46,18 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
(whitelistedUsername) => whitelistedUsername.username
|
(whitelistedUsername) => whitelistedUsername.username
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!user?.username) {
|
||||||
targetUser?.isPrivate &&
|
|
||||||
user.username &&
|
|
||||||
!whitelistedUsernames?.includes(user.username)
|
|
||||||
) {
|
|
||||||
return res
|
return res
|
||||||
.setHeader("Content-Type", "text/plain")
|
.setHeader("Content-Type", "text/plain")
|
||||||
.status(400)
|
.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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import getPublicCollection from "@/lib/api/controllers/public/collections/getPublicCollection";
|
import getPublicCollection from "@/lib/api/controllers/public/collections/getPublicCollection";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
export default async function collections(
|
export default async function collection(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
|
@ -146,7 +146,7 @@ export default function Index() {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="link-banner"
|
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>
|
<div id="link-banner-inner" className="link-banner-inner"></div>
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,6 @@ export default function PublicCollections() {
|
||||||
image: "",
|
image: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {}, []);
|
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState({
|
const [searchFilter, setSearchFilter] = useState({
|
||||||
name: true,
|
name: true,
|
||||||
url: true,
|
url: true,
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ type LinkStore = {
|
||||||
addLink: (
|
addLink: (
|
||||||
body: LinkIncludingShortenedCollectionAndTags
|
body: LinkIncludingShortenedCollectionAndTags
|
||||||
) => Promise<ResponseObject>;
|
) => Promise<ResponseObject>;
|
||||||
getLink: (linkId: number) => Promise<ResponseObject>;
|
getLink: (linkId: number, publicRoute?: boolean) => Promise<ResponseObject>;
|
||||||
updateLink: (
|
updateLink: (
|
||||||
link: LinkIncludingShortenedCollectionAndTags
|
link: LinkIncludingShortenedCollectionAndTags
|
||||||
) => Promise<ResponseObject>;
|
) => Promise<ResponseObject>;
|
||||||
|
@ -66,8 +66,12 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
|
|
||||||
return { ok: response.ok, data: data.response };
|
return { ok: response.ok, data: data.response };
|
||||||
},
|
},
|
||||||
getLink: async (linkId) => {
|
getLink: async (linkId, publicRoute) => {
|
||||||
const response = await fetch(`/api/v1/links/${linkId}`);
|
const path = publicRoute
|
||||||
|
? `/api/v1/public/links/${linkId}`
|
||||||
|
: `/api/v1/links/${linkId}`;
|
||||||
|
|
||||||
|
const response = await fetch(path);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
|
|
@ -190,7 +190,11 @@ body {
|
||||||
|
|
||||||
/* Reader view custom stylings */
|
/* Reader view custom stylings */
|
||||||
.reader-view {
|
.reader-view {
|
||||||
line-height: 3rem;
|
line-height: 2.8rem;
|
||||||
|
}
|
||||||
|
.reader-view p {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
}
|
}
|
||||||
.reader-view h1 {
|
.reader-view h1 {
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
|
|
Ŝarĝante…
Reference in New Issue