upload preview functionality
This commit is contained in:
parent
e9072bba51
commit
3de8872f26
|
@ -39,32 +39,10 @@ const IconPopover = ({
|
||||||
onClose={() => onClose()}
|
onClose={() => onClose()}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
className,
|
className,
|
||||||
"fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg shadow-md"
|
"fade-in bg-base-200 border border-neutral-content p-2 w-[22.5rem] rounded-lg shadow-md"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex gap-2 h-full w-full">
|
<div className="flex gap-2 h-full w-full">
|
||||||
<div className="flex flex-col gap-2 h-full w-fit color-picker">
|
|
||||||
<div
|
|
||||||
className="btn btn-ghost btn-xs"
|
|
||||||
onClick={reset as React.MouseEventHandler<HTMLDivElement>}
|
|
||||||
>
|
|
||||||
{t("reset")}
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
className="border border-neutral-content bg-base-100 focus:outline-none focus:border-primary duration-100 w-full rounded-md h-7"
|
|
||||||
value={weight}
|
|
||||||
onChange={(e) => setWeight(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="regular">{t("regular")}</option>
|
|
||||||
<option value="thin">{t("thin")}</option>
|
|
||||||
<option value="light">{t("light_icon")}</option>
|
|
||||||
<option value="bold">{t("bold")}</option>
|
|
||||||
<option value="fill">{t("fill")}</option>
|
|
||||||
<option value="duotone">{t("duotone")}</option>
|
|
||||||
</select>
|
|
||||||
<HexColorPicker color={color} onChange={(e) => setColor(e)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<TextInput
|
<TextInput
|
||||||
className="p-2 rounded w-full h-7 text-sm"
|
className="p-2 rounded w-full h-7 text-sm"
|
||||||
|
@ -73,7 +51,7 @@ const IconPopover = ({
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-1 w-full overflow-y-auto h-32 border border-neutral-content bg-base-100 rounded-md p-2">
|
<div className="grid grid-cols-6 gap-1 w-full overflow-y-auto h-44 border border-neutral-content bg-base-100 rounded-md p-2">
|
||||||
<IconGrid
|
<IconGrid
|
||||||
query={query}
|
query={query}
|
||||||
color={color}
|
color={color}
|
||||||
|
@ -82,6 +60,79 @@ const IconPopover = ({
|
||||||
setIconName={setIconName}
|
setIconName={setIconName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 color-picker w-full">
|
||||||
|
<HexColorPicker color={color} onChange={(e) => setColor(e)} />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-1 text-sm">
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="regular"
|
||||||
|
checked={weight === "regular"}
|
||||||
|
onChange={() => setWeight("regular")}
|
||||||
|
/>
|
||||||
|
{t("regular")}
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="thin"
|
||||||
|
checked={weight === "thin"}
|
||||||
|
onChange={() => setWeight("thin")}
|
||||||
|
/>
|
||||||
|
{t("thin")}
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="light"
|
||||||
|
checked={weight === "light"}
|
||||||
|
onChange={() => setWeight("light")}
|
||||||
|
/>
|
||||||
|
{t("light_icon")}
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="bold"
|
||||||
|
checked={weight === "bold"}
|
||||||
|
onChange={() => setWeight("bold")}
|
||||||
|
/>
|
||||||
|
{t("bold")}
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="fill"
|
||||||
|
checked={weight === "fill"}
|
||||||
|
onChange={() => setWeight("fill")}
|
||||||
|
/>
|
||||||
|
{t("fill")}
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
className="radio radio-primary mr-2"
|
||||||
|
value="duotone"
|
||||||
|
checked={weight === "duotone"}
|
||||||
|
onChange={() => setWeight("duotone")}
|
||||||
|
/>
|
||||||
|
{t("duotone")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="btn btn-neutral btn-sm mt-2 w-fit mx-auto"
|
||||||
|
onClick={reset as React.MouseEventHandler<HTMLDivElement>}
|
||||||
|
>
|
||||||
|
{t("reset_defaults")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
ArchivedFormat,
|
ArchivedFormat,
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import {
|
import {
|
||||||
pdfAvailable,
|
pdfAvailable,
|
||||||
readabilityAvailable,
|
readabilityAvailable,
|
||||||
|
@ -17,7 +16,11 @@ import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { BeatLoader } from "react-spinners";
|
import { BeatLoader } from "react-spinners";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
import { useGetLink, useUpdateLink } from "@/hooks/store/links";
|
import {
|
||||||
|
useGetLink,
|
||||||
|
useUpdateLink,
|
||||||
|
useUpdatePreview,
|
||||||
|
} from "@/hooks/store/links";
|
||||||
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
|
import LinkIcon from "./LinkViews/LinkComponents/LinkIcon";
|
||||||
import CopyButton from "./CopyButton";
|
import CopyButton from "./CopyButton";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -31,6 +34,7 @@ import TagSelection from "./InputSelect/TagSelection";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import IconPopover from "./IconPopover";
|
import IconPopover from "./IconPopover";
|
||||||
import TextInput from "./TextInput";
|
import TextInput from "./TextInput";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -50,8 +54,13 @@ export default function LinkDetails({
|
||||||
const [link, setLink] =
|
const [link, setLink] =
|
||||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLink(activeLink);
|
||||||
|
}, [activeLink]);
|
||||||
|
|
||||||
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const session = useSession();
|
|
||||||
const getLink = useGetLink();
|
const getLink = useGetLink();
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
|
|
||||||
|
@ -140,13 +149,14 @@ export default function LinkDetails({
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [link?.monolith]);
|
}, [link.monolith]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
|
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
|
||||||
|
|
||||||
const updateLink = useUpdateLink();
|
const updateLink = useUpdateLink();
|
||||||
|
const updatePreview = useUpdatePreview();
|
||||||
|
|
||||||
const submit = async (e?: any) => {
|
const submit = async (e?: any) => {
|
||||||
e?.preventDefault();
|
e?.preventDefault();
|
||||||
|
@ -169,7 +179,6 @@ export default function LinkDetails({
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("updated"));
|
toast.success(t("updated"));
|
||||||
setMode && setMode("view");
|
setMode && setMode("view");
|
||||||
console.log(data);
|
|
||||||
setLink(data);
|
setLink(data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -208,7 +217,7 @@ export default function LinkDetails({
|
||||||
>
|
>
|
||||||
{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&updatedAt=${link.updatedAt}`}
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
|
@ -227,7 +236,7 @@ export default function LinkDetails({
|
||||||
<div className="duration-100 h-40 skeleton rounded-b-none"></div>
|
<div className="duration-100 h-40 skeleton rounded-b-none"></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!standalone && (
|
{!standalone && (permissions === true || permissions?.canUpdate) && (
|
||||||
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
|
<div className="absolute top-0 bottom-0 left-0 right-0 opacity-0 group-hover:opacity-100 duration-100 flex justify-end items-end">
|
||||||
<label className="btn btn-xs mb-2 mr-3 opacity-50 hover:opacity-100">
|
<label className="btn btn-xs mb-2 mr-3 opacity-50 hover:opacity-100">
|
||||||
{t("upload_preview_image")}
|
{t("upload_preview_image")}
|
||||||
|
@ -238,37 +247,26 @@ export default function LinkDetails({
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const load = toast.loading(t("updating"));
|
||||||
formData.append("file", file);
|
|
||||||
|
|
||||||
const load = toast.loading(t("uploading"));
|
await updatePreview.mutateAsync(
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(
|
|
||||||
`/api/v1/archives/${link.id}/preview`,
|
|
||||||
{
|
{
|
||||||
method: "POST",
|
linkId: link.id as number,
|
||||||
body: formData,
|
file,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSettled: (data, error) => {
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message);
|
||||||
|
} else {
|
||||||
|
toast.success(t("updated"));
|
||||||
|
setLink({ updatedAt: data.updatedAt, ...link });
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(await res.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
preview: data.preview,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(t("uploaded"));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
toast.dismiss(load);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
@ -277,11 +275,7 @@ export default function LinkDetails({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{standalone ? (
|
{!standalone && (permissions === true || permissions?.canUpdate) ? (
|
||||||
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
|
||||||
<LinkIcon link={link} onClick={() => setIconPopover(true)} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
||||||
<div className="tooltip tooltip-bottom" data-tip={t("change_icon")}>
|
<div className="tooltip tooltip-bottom" data-tip={t("change_icon")}>
|
||||||
<LinkIcon
|
<LinkIcon
|
||||||
|
@ -316,6 +310,10 @@ export default function LinkDetails({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="-mt-14 ml-8 relative w-fit pb-2">
|
||||||
|
<LinkIcon link={link} onClick={() => setIconPopover(true)} />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
|
<div className="max-w-xl sm:px-8 p-5 pb-8 pt-2">
|
||||||
|
|
|
@ -49,7 +49,7 @@ export default function LinkActions({
|
||||||
await updateLink.mutateAsync(
|
await updateLink.mutateAsync(
|
||||||
{
|
{
|
||||||
...link,
|
...link,
|
||||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: user.id }],
|
pinnedBy: isAlreadyPinned ? [{ id: undefined }] : [{ id: user.id }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSettled: (data, error) => {
|
onSettled: (data, error) => {
|
||||||
|
@ -95,6 +95,9 @@ export default function LinkActions({
|
||||||
className={`absolute ${position || "top-3 right-3"} ${
|
className={`absolute ${position || "top-3 right-3"} ${
|
||||||
alignToTop ? "" : "dropdown-end"
|
alignToTop ? "" : "dropdown-end"
|
||||||
} z-20`}
|
} z-20`}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onMouseDown={dropdownTriggerer}
|
||||||
onClick={() => setLinkModal(true)}
|
onClick={() => setLinkModal(true)}
|
||||||
>
|
>
|
||||||
<div className="btn btn-ghost btn-sm btn-square text-neutral">
|
<div className="btn btn-ghost btn-sm btn-square text-neutral">
|
||||||
|
@ -120,7 +123,6 @@ export default function LinkActions({
|
||||||
alignToTop ? "" : "translate-y-10"
|
alignToTop ? "" : "translate-y-10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{(permissions === true || permissions?.canUpdate) && (
|
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -136,7 +138,6 @@ export default function LinkActions({
|
||||||
: t("pin_to_dashboard")}
|
: t("pin_to_dashboard")}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -165,7 +166,7 @@ export default function LinkActions({
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{link.type === "url" && (
|
{link.type === "url" && permissions === true && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
|
|
@ -154,7 +154,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
|
||||||
<div className="relative rounded-t-2xl h-40 overflow-hidden">
|
<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&updatedAt=${link.updatedAt}`}
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
|
|
|
@ -91,5 +91,3 @@ const LinkPlaceholderIcon = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// `text-black aspect-square text-4xl ${iconClasses}`
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
|
||||||
<div className="relative rounded-t-2xl overflow-hidden">
|
<div className="relative rounded-t-2xl 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&updatedAt=${link.updatedAt}`}
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
alt=""
|
alt=""
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
ArchivedFormat,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useDeleteLink } from "@/hooks/store/links";
|
||||||
import { useDeleteLink, useGetLink, useUpdateLink } from "@/hooks/store/links";
|
|
||||||
import Drawer from "../Drawer";
|
import Drawer from "../Drawer";
|
||||||
import LinkDetails from "../LinkDetails";
|
import LinkDetails from "../LinkDetails";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -34,86 +29,6 @@ export default function LinkModal({
|
||||||
activeMode,
|
activeMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const getLink = useGetLink();
|
|
||||||
const { data: user = {} } = useUser();
|
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
|
||||||
id: null as unknown as number,
|
|
||||||
name: "",
|
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
archiveAsScreenshot: undefined as unknown as boolean,
|
|
||||||
archiveAsMonolith: undefined as unknown as boolean,
|
|
||||||
archiveAsPDF: undefined as unknown as boolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOwner = async () => {
|
|
||||||
if (link.collection.ownerId !== user.id) {
|
|
||||||
const owner = await getPublicUserData(
|
|
||||||
link.collection.ownerId as number
|
|
||||||
);
|
|
||||||
setCollectionOwner(owner);
|
|
||||||
} else if (link.collection.ownerId === user.id) {
|
|
||||||
setCollectionOwner({
|
|
||||||
id: user.id as number,
|
|
||||||
name: user.name,
|
|
||||||
username: user.username as string,
|
|
||||||
image: user.image as string,
|
|
||||||
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
|
|
||||||
archiveAsMonolith: user.archiveAsScreenshot as boolean,
|
|
||||||
archiveAsPDF: user.archiveAsPDF as boolean,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOwner();
|
|
||||||
}, [link.collection.ownerId]);
|
|
||||||
|
|
||||||
const isReady = () => {
|
|
||||||
return (
|
|
||||||
link &&
|
|
||||||
(collectionOwner.archiveAsScreenshot === true
|
|
||||||
? link.pdf && link.pdf !== "pending"
|
|
||||||
: true) &&
|
|
||||||
(collectionOwner.archiveAsMonolith === true
|
|
||||||
? link.monolith && link.monolith !== "pending"
|
|
||||||
: true) &&
|
|
||||||
(collectionOwner.archiveAsPDF === true
|
|
||||||
? link.pdf && link.pdf !== "pending"
|
|
||||||
: true) &&
|
|
||||||
link.readable &&
|
|
||||||
link.readable !== "pending"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
await getLink.mutateAsync({
|
|
||||||
id: link.id as number,
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
let interval: any;
|
|
||||||
|
|
||||||
if (!isReady()) {
|
|
||||||
interval = setInterval(async () => {
|
|
||||||
await getLink.mutateAsync({
|
|
||||||
id: link.id as number,
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [link?.monolith]);
|
|
||||||
|
|
||||||
const permissions = usePermissions(link.collection.id as number);
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
|
||||||
|
@ -136,6 +51,7 @@ export default function LinkModal({
|
||||||
onClick={() => onClose()}
|
onClick={() => onClose()}
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
|
{(permissions === true || permissions?.canUpdate) && (
|
||||||
<div className="flex gap-1 h-8 rounded-full bg-neutral-content bg-opacity-50 text-base-content p-1 text-xs duration-100 select-none z-10">
|
<div className="flex gap-1 h-8 rounded-full bg-neutral-content bg-opacity-50 text-base-content p-1 text-xs duration-100 select-none z-10">
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -160,6 +76,7 @@ export default function LinkModal({
|
||||||
Edit
|
Edit
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className={`dropdown dropdown-end z-20`}>
|
<div className={`dropdown dropdown-end z-20`}>
|
||||||
|
@ -174,7 +91,7 @@ export default function LinkModal({
|
||||||
<ul
|
<ul
|
||||||
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box`}
|
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box`}
|
||||||
>
|
>
|
||||||
{(permissions === true || permissions?.canUpdate) && (
|
{
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -190,7 +107,7 @@ export default function LinkModal({
|
||||||
: t("pin_to_dashboard")}
|
: t("pin_to_dashboard")}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
}
|
||||||
{link.type === "url" && permissions === true && (
|
{link.type === "url" && permissions === true && (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onChangeViewMode(e, ViewMode.Card)}
|
onClick={(e) => onChangeViewMode(e, ViewMode.Card)}
|
||||||
className={`btn btn-square btn-sm btn-ghost ${
|
className={`btn w-[31%] btn-sm btn-ghost ${
|
||||||
viewMode === ViewMode.Card
|
viewMode === ViewMode.Card
|
||||||
? "bg-primary/20 hover:bg-primary/20"
|
? "bg-primary/20 hover:bg-primary/20"
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
|
@ -67,7 +67,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onChangeViewMode(e, ViewMode.Masonry)}
|
onClick={(e) => onChangeViewMode(e, ViewMode.Masonry)}
|
||||||
className={`btn btn-square btn-sm btn-ghost ${
|
className={`btn w-[31%] btn-sm btn-ghost ${
|
||||||
viewMode === ViewMode.Masonry
|
viewMode === ViewMode.Masonry
|
||||||
? "bg-primary/20 hover:bg-primary/20"
|
? "bg-primary/20 hover:bg-primary/20"
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
|
@ -77,7 +77,7 @@ export default function ViewDropdown({ viewMode, setViewMode }: Props) {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => onChangeViewMode(e, ViewMode.List)}
|
onClick={(e) => onChangeViewMode(e, ViewMode.List)}
|
||||||
className={`btn btn-square btn-sm btn-ghost ${
|
className={`btn w-[31%] btn-sm btn-ghost ${
|
||||||
viewMode === ViewMode.List
|
viewMode === ViewMode.List
|
||||||
? "bg-primary/20 hover:bg-primary/20"
|
? "bg-primary/20 hover:bg-primary/20"
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import Jimp from "jimp";
|
||||||
|
|
||||||
const useLinks = (params: LinkRequestQuery = {}) => {
|
const useLinks = (params: LinkRequestQuery = {}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -395,6 +396,41 @@ const useUploadFile = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useUpdatePreview = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ linkId, file }: { linkId: number; file: File }) => {
|
||||||
|
const formBody = new FormData();
|
||||||
|
|
||||||
|
if (!linkId || !file)
|
||||||
|
throw new Error("Error generating preview: Invalid parameters");
|
||||||
|
|
||||||
|
formBody.append("file", file);
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/archives/${linkId}?format=` + ArchivedFormat.jpeg,
|
||||||
|
{
|
||||||
|
body: formBody,
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = res.json();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["links"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["dashboardData"] });
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["collections"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tags"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["publicLinks"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const useBulkEditLinks = () => {
|
const useBulkEditLinks = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
@ -479,4 +515,5 @@ export {
|
||||||
useGetLink,
|
useGetLink,
|
||||||
useBulkEditLinks,
|
useBulkEditLinks,
|
||||||
resetInfiniteQueryPagination,
|
resetInfiniteQueryPagination,
|
||||||
|
useUpdatePreview,
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,15 +25,15 @@ export default async function updateLinkById(
|
||||||
(e: UsersAndCollections) => e.userId === userId
|
(e: UsersAndCollections) => e.userId === userId
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the user is able to create a link, they can pin it to their dashboard only.
|
// If the user is part of a collection, they can pin it to their dashboard
|
||||||
if (canPinPermission) {
|
if (canPinPermission && data.pinnedBy && data.pinnedBy[0]) {
|
||||||
const updatedLink = await prisma.link.update({
|
const updatedLink = await prisma.link.update({
|
||||||
where: {
|
where: {
|
||||||
id: linkId,
|
id: linkId,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
pinnedBy:
|
pinnedBy:
|
||||||
data?.pinnedBy && data.pinnedBy[0]
|
data?.pinnedBy && data.pinnedBy[0].id === userId
|
||||||
? { connect: { id: userId } }
|
? { connect: { id: userId } }
|
||||||
: { disconnect: { id: userId } },
|
: { disconnect: { id: userId } },
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,7 @@ export default async function updateLinkById(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// return { response: updatedLink, status: 200 };
|
return { response: updatedLink, status: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetCollectionIsAccessible = await getPermission({
|
const targetCollectionIsAccessible = await getPermission({
|
||||||
|
|
|
@ -105,8 +105,6 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
response: "Collection is not accessible.",
|
response: "Collection is not accessible.",
|
||||||
});
|
});
|
||||||
|
|
||||||
// await uploadHandler(linkId, )
|
|
||||||
|
|
||||||
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER || 30000);
|
const MAX_LINKS_PER_USER = Number(process.env.MAX_LINKS_PER_USER || 30000);
|
||||||
|
|
||||||
const numberOfLinksTheUserHas = await prisma.link.count({
|
const numberOfLinksTheUserHas = await prisma.link.count({
|
||||||
|
@ -119,8 +117,7 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
|
if (numberOfLinksTheUserHas > MAX_LINKS_PER_USER)
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
response:
|
response: `Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.`,
|
||||||
"Each collection owner can only have a maximum of ${MAX_LINKS_PER_USER} Links.",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
|
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
|
||||||
|
@ -208,4 +205,94 @@ export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// To update the link preview
|
||||||
|
else if (req.method === "PUT" && format === ArchivedFormat.jpeg) {
|
||||||
|
if (process.env.NEXT_PUBLIC_DEMO === "true")
|
||||||
|
return res.status(400).json({
|
||||||
|
response:
|
||||||
|
"This action is disabled because this is a read-only demo of Linkwarden.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await verifyUser({ req, res });
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const collectionPermissions = await getPermission({
|
||||||
|
userId: user.id,
|
||||||
|
linkId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collectionPermissions)
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "Collection is not accessible.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberHasAccess = collectionPermissions.members.some(
|
||||||
|
(e: UsersAndCollections) => e.userId === user.id && e.canCreate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(collectionPermissions.ownerId === user.id || memberHasAccess))
|
||||||
|
return res.status(400).json({
|
||||||
|
response: "Collection is not accessible.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const NEXT_PUBLIC_MAX_FILE_BUFFER = Number(
|
||||||
|
process.env.NEXT_PUBLIC_MAX_FILE_BUFFER || 10
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = formidable({
|
||||||
|
maxFields: 1,
|
||||||
|
maxFiles: 1,
|
||||||
|
maxFileSize: NEXT_PUBLIC_MAX_FILE_BUFFER * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
form.parse(req, async (err, fields, files) => {
|
||||||
|
const allowedMIMETypes = ["image/png", "image/jpg", "image/jpeg"];
|
||||||
|
|
||||||
|
if (
|
||||||
|
err ||
|
||||||
|
!files.file ||
|
||||||
|
!files.file[0] ||
|
||||||
|
!allowedMIMETypes.includes(files.file[0].mimetype || "")
|
||||||
|
) {
|
||||||
|
// Handle parsing error
|
||||||
|
return res.status(400).json({
|
||||||
|
response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const fileBuffer = fs.readFileSync(files.file[0].filepath);
|
||||||
|
|
||||||
|
if (
|
||||||
|
Buffer.byteLength(fileBuffer) >
|
||||||
|
1024 * 1024 * Number(NEXT_PUBLIC_MAX_FILE_BUFFER)
|
||||||
|
)
|
||||||
|
return res.status(400).json({
|
||||||
|
response: `Sorry, we couldn't process your file. Please ensure it's a PNG, or JPG format and doesn't exceed ${NEXT_PUBLIC_MAX_FILE_BUFFER}MB.`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkStillExists = await prisma.link.update({
|
||||||
|
where: { id: linkId },
|
||||||
|
data: {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (linkStillExists) {
|
||||||
|
const collectionId = collectionPermissions.id;
|
||||||
|
createFolder({
|
||||||
|
filePath: `archives/preview/${collectionId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
generatePreview(fileBuffer, collectionId, linkId);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.unlinkSync(files.file[0].filepath);
|
||||||
|
|
||||||
|
if (linkStillExists)
|
||||||
|
return res.status(200).json({
|
||||||
|
response: linkStillExists,
|
||||||
|
});
|
||||||
|
else return res.status(400).json({ response: "Link not found." });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -297,7 +297,7 @@
|
||||||
"for_collection": "For {{name}}",
|
"for_collection": "For {{name}}",
|
||||||
"create_new_collection": "Create a New Collection",
|
"create_new_collection": "Create a New Collection",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"reset": "Reset",
|
"reset_defaults": "Reset to Defaults",
|
||||||
"updating_collection": "Updating Collection...",
|
"updating_collection": "Updating Collection...",
|
||||||
"collection_name_placeholder": "e.g. Example Collection",
|
"collection_name_placeholder": "e.g. Example Collection",
|
||||||
"collection_description_placeholder": "The purpose of this Collection...",
|
"collection_description_placeholder": "The purpose of this Collection...",
|
||||||
|
|
Ŝarĝante…
Reference in New Issue