improve link refresh logic + many changes and improvements
This commit is contained in:
parent
71b99bb25c
commit
b65787358f
|
@ -35,6 +35,8 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -48,6 +50,8 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
name: account.name,
|
name: account.name,
|
||||||
username: account.username as string,
|
username: account.username as string,
|
||||||
image: account.image as string,
|
image: account.image as string,
|
||||||
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -70,8 +74,7 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
>
|
>
|
||||||
<i className="bi-three-dots text-xl" title="More"></i>
|
<i className="bi-three-dots text-xl" title="More"></i>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
||||||
className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
|
||||||
{permissions === true ? (
|
{permissions === true ? (
|
||||||
<li>
|
<li>
|
||||||
<div
|
<div
|
||||||
|
@ -166,17 +169,23 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
||||||
{collection.isPublic ? (
|
{collection.isPublic ? (
|
||||||
<i className="bi-globe-americas drop-shadow text-neutral"
|
<i
|
||||||
title="This collection is being shared publicly."></i>
|
className="bi-globe-americas drop-shadow text-neutral"
|
||||||
|
title="This collection is being shared publicly."
|
||||||
|
></i>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<i className="bi-link-45deg text-lg text-neutral"
|
<i
|
||||||
title="This collection is being shared publicly."></i>
|
className="bi-link-45deg text-lg text-neutral"
|
||||||
|
title="This collection is being shared publicly."
|
||||||
|
></i>
|
||||||
{collection._count && collection._count.links}
|
{collection._count && collection._count.links}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-1 text-neutral">
|
<div className="flex items-center justify-end gap-1 text-neutral">
|
||||||
<p className="font-bold text-xs flex gap-1 items-center">
|
<p className="font-bold text-xs flex gap-1 items-center">
|
||||||
<i className="bi-calendar3 text-neutral"
|
<i
|
||||||
title="This collection is being shared publicly."></i>
|
className="bi-calendar3 text-neutral"
|
||||||
|
title="This collection is being shared publicly."
|
||||||
|
></i>
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default function dashboardItem({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="w-20 aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
|
<div className="w-[4.7rem] aspect-square flex justify-center items-center bg-primary/20 rounded-xl select-none">
|
||||||
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
|
<i className={`${icon} text-primary text-4xl drop-shadow`}></i>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 flex flex-col justify-center">
|
<div className="ml-4 flex flex-col justify-center">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { MouseEventHandler, ReactNode } from "react";
|
import React, { MouseEventHandler, ReactNode, useEffect } from "react";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -8,6 +8,13 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Modal({ toggleModal, className, children }: Props) {
|
export default function Modal({ toggleModal, className, children }: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
|
@ -21,9 +28,7 @@ export default function Modal({ toggleModal, className, children }: Props) {
|
||||||
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
||||||
className="absolute top-4 right-3 btn btn-sm outline-none btn-circle btn-ghost z-10"
|
className="absolute top-4 right-3 btn btn-sm outline-none btn-circle btn-ghost z-10"
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-x text-neutral text-2xl"></i>
|
||||||
className="bi-x text-neutral text-2xl"
|
|
||||||
></i>
|
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
|
||||||
import useCollectionStore from "@/store/collections";
|
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
|
||||||
import SubmitButton from "@/components/SubmitButton";
|
|
||||||
import { HexColorPicker } from "react-colorful";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import TextInput from "@/components/TextInput";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
toggleCollectionModal: Function;
|
|
||||||
setCollection: Dispatch<
|
|
||||||
SetStateAction<CollectionIncludingMembersAndLinkCount>
|
|
||||||
>;
|
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
method: "CREATE" | "UPDATE" | "VIEW_TEAM";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CollectionInfo({
|
|
||||||
toggleCollectionModal,
|
|
||||||
setCollection,
|
|
||||||
collection,
|
|
||||||
method,
|
|
||||||
}: Props) {
|
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
|
||||||
const { updateCollection, addCollection } = useCollectionStore();
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!collection) return null;
|
|
||||||
|
|
||||||
setSubmitLoader(true);
|
|
||||||
|
|
||||||
const load = toast.loading(
|
|
||||||
method === "UPDATE" ? "Applying..." : "Creating..."
|
|
||||||
);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (method === "CREATE") response = await addCollection(collection);
|
|
||||||
else response = await updateCollection(collection);
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(
|
|
||||||
`Collection ${method === "UPDATE" ? "Saved!" : "Created!"}`
|
|
||||||
);
|
|
||||||
toggleCollectionModal();
|
|
||||||
} else toast.error(response.data as string);
|
|
||||||
|
|
||||||
setSubmitLoader(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<div className="w-full">
|
|
||||||
<p className="mb-2">Name</p>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<TextInput
|
|
||||||
value={collection.name}
|
|
||||||
placeholder="e.g. Example Collection"
|
|
||||||
onChange={(e) =>
|
|
||||||
setCollection({ ...collection, name: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="color-picker flex justify-between">
|
|
||||||
<div className="flex flex-col justify-between items-center w-32">
|
|
||||||
<p className="w-full mb-2">Color</p>
|
|
||||||
<div style={{ color: collection.color }}>
|
|
||||||
<i className={"bi-folder-fill"}></i>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="btn btn-ghost btn-xs"
|
|
||||||
onClick={() =>
|
|
||||||
setCollection({ ...collection, color: "#0ea5e9" })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HexColorPicker
|
|
||||||
color={collection.color}
|
|
||||||
onChange={(e) => setCollection({ ...collection, color: e })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<p className="mb-2">Description</p>
|
|
||||||
<textarea
|
|
||||||
className="w-full h-[11.4rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-sky-300 dark:focus:border-sky-600"
|
|
||||||
placeholder="The purpose of this Collection..."
|
|
||||||
value={collection.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
description: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SubmitButton
|
|
||||||
onClick={submit}
|
|
||||||
loading={submitLoader}
|
|
||||||
label={method === "CREATE" ? "Add" : "Save"}
|
|
||||||
className="mx-auto mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
|
||||||
import useCollectionStore from "@/store/collections";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import TextInput from "@/components/TextInput";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
toggleDeleteCollectionModal: Function;
|
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DeleteCollection({
|
|
||||||
toggleDeleteCollectionModal,
|
|
||||||
collection,
|
|
||||||
}: Props) {
|
|
||||||
const [inputField, setInputField] = useState("");
|
|
||||||
|
|
||||||
const { removeCollection } = useCollectionStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (permissions === true) if (collection.name !== inputField) return null;
|
|
||||||
|
|
||||||
const load = toast.loading("Deleting...");
|
|
||||||
|
|
||||||
const response = await removeCollection(collection.id as number);
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success("Collection Deleted.");
|
|
||||||
toggleDeleteCollectionModal();
|
|
||||||
router.push("/collections");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const permissions = usePermissions(collection.id as number);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 justify-between sm:w-[35rem] w-80">
|
|
||||||
{permissions === true ? (
|
|
||||||
<>
|
|
||||||
<p className="text-red-500 font-bold text-center">Warning!</p>
|
|
||||||
|
|
||||||
<div className="max-h-[20rem] overflow-y-auto">
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
Please note that deleting the collection will permanently remove
|
|
||||||
all its contents, including the following:
|
|
||||||
</p>
|
|
||||||
<div className="p-3">
|
|
||||||
<li className="list-inside">
|
|
||||||
Links: All links within the collection will be permanently
|
|
||||||
deleted.
|
|
||||||
</li>
|
|
||||||
<li className="list-inside">
|
|
||||||
Tags: All tags associated with the collection will be removed.
|
|
||||||
</li>
|
|
||||||
<li className="list-inside">
|
|
||||||
Screenshots/PDFs: Any screenshots or PDFs attached to links
|
|
||||||
within this collection will be permanently deleted.
|
|
||||||
</li>
|
|
||||||
<li className="list-inside">
|
|
||||||
Members: Any members who have been granted access to the
|
|
||||||
collection will lose their permissions and no longer be able
|
|
||||||
to view or interact with the content.
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Please double-check that you have backed up any essential data
|
|
||||||
and have informed the relevant members about this action.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<p className="text-center">
|
|
||||||
To confirm, type "
|
|
||||||
<span className="font-bold">{collection.name}</span>
|
|
||||||
" in the box below:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
autoFocus={true}
|
|
||||||
value={inputField}
|
|
||||||
onChange={(e) => setInputField(e.target.value)}
|
|
||||||
placeholder={`Type "${collection.name}" Here.`}
|
|
||||||
className="w-3/4 mx-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>Click the button below to leave the current collection.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`mx-auto mt-2 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold duration-100 ${
|
|
||||||
permissions === true
|
|
||||||
? inputField === collection.name
|
|
||||||
? "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
|
|
||||||
: "cursor-not-allowed bg-red-300 dark:bg-red-900"
|
|
||||||
: "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
|
|
||||||
}`}
|
|
||||||
onClick={submit}
|
|
||||||
>
|
|
||||||
<i className={`${ permissions === true ? 'bi-trash' : 'bi-arrow-right'}`}></i>
|
|
||||||
{permissions === true ? "Delete" : "Leave"} Collection
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,417 +0,0 @@
|
||||||
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
|
|
||||||
import useCollectionStore from "@/store/collections";
|
|
||||||
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
|
||||||
import addMemberToCollection from "@/lib/client/addMemberToCollection";
|
|
||||||
import Checkbox from "../../Checkbox";
|
|
||||||
import SubmitButton from "@/components/SubmitButton";
|
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
|
||||||
import TextInput from "@/components/TextInput";
|
|
||||||
import useAccountStore from "@/store/account";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
toggleCollectionModal: Function;
|
|
||||||
setCollection: Dispatch<
|
|
||||||
SetStateAction<CollectionIncludingMembersAndLinkCount>
|
|
||||||
>;
|
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
method: "CREATE" | "UPDATE";
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TeamManagement({
|
|
||||||
toggleCollectionModal,
|
|
||||||
setCollection,
|
|
||||||
collection,
|
|
||||||
method,
|
|
||||||
}: Props) {
|
|
||||||
const { account } = useAccountStore();
|
|
||||||
const permissions = usePermissions(collection.id as number);
|
|
||||||
|
|
||||||
const currentURL = new URL(document.URL);
|
|
||||||
|
|
||||||
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
|
|
||||||
|
|
||||||
const [memberUsername, setMemberUsername] = useState("");
|
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOwner = async () => {
|
|
||||||
const owner = await getPublicUserData(collection.ownerId as number);
|
|
||||||
setCollectionOwner(owner);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOwner();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { addCollection, updateCollection } = useCollectionStore();
|
|
||||||
|
|
||||||
const setMemberState = (newMember: Member) => {
|
|
||||||
if (!collection) return null;
|
|
||||||
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
members: [...collection.members, newMember],
|
|
||||||
});
|
|
||||||
|
|
||||||
setMemberUsername("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
if (!collection) return null;
|
|
||||||
|
|
||||||
setSubmitLoader(true);
|
|
||||||
|
|
||||||
const load = toast.loading(
|
|
||||||
method === "UPDATE" ? "Applying..." : "Creating..."
|
|
||||||
);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
|
|
||||||
if (method === "CREATE") response = await addCollection(collection);
|
|
||||||
else response = await updateCollection(collection);
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success("Collection Saved!");
|
|
||||||
toggleCollectionModal();
|
|
||||||
} else toast.error(response.data as string);
|
|
||||||
|
|
||||||
setSubmitLoader(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
|
||||||
{permissions === true && (
|
|
||||||
<>
|
|
||||||
<p>Make Public</p>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Make this a public collection."
|
|
||||||
state={collection.isPublic}
|
|
||||||
onClick={() =>
|
|
||||||
setCollection({ ...collection, isPublic: !collection.isPublic })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-neutral text-sm">
|
|
||||||
This will let <b>Anyone</b> to view this collection.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{collection.isPublic ? (
|
|
||||||
<div>
|
|
||||||
<p className="mb-2">Public Link (Click to copy)</p>
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
try {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(publicCollectionURL)
|
|
||||||
.then(() => toast.success("Copied!"));
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 dark:bg-neutral-950 border-neutral-content border-solid border outline-none hover:border-sky-300 dark:hover:border-sky-600 duration-100 cursor-text"
|
|
||||||
>
|
|
||||||
{publicCollectionURL}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{permissions !== true && collection.isPublic && (
|
|
||||||
<div className="divider mb-3 mt-0"></div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{permissions === true && (
|
|
||||||
<>
|
|
||||||
<p>Member Management</p>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<TextInput
|
|
||||||
value={memberUsername || ""}
|
|
||||||
placeholder="Username (without the '@')"
|
|
||||||
onChange={(e) => setMemberUsername(e.target.value)}
|
|
||||||
onKeyDown={(e) =>
|
|
||||||
e.key === "Enter" &&
|
|
||||||
addMemberToCollection(
|
|
||||||
account.username as string,
|
|
||||||
memberUsername || "",
|
|
||||||
collection,
|
|
||||||
setMemberState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
onClick={() =>
|
|
||||||
addMemberToCollection(
|
|
||||||
account.username as string,
|
|
||||||
memberUsername || "",
|
|
||||||
collection,
|
|
||||||
setMemberState
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex items-center justify-center bg-sky-700 hover:bg-sky-600 duration-100 text-white w-10 h-10 p-2 rounded-md cursor-pointer"
|
|
||||||
>
|
|
||||||
<i className="bi-person-add text-xl"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{collection?.members[0]?.user && (
|
|
||||||
<>
|
|
||||||
<p className="text-center text-neutral text-xs sm:text-sm">
|
|
||||||
(All Members have <b>Read</b> access to this collection.)
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-col gap-3 rounded-md">
|
|
||||||
{collection.members
|
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
|
||||||
.map((e, i) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="relative border p-2 rounded-md border-neutral flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
|
||||||
>
|
|
||||||
{permissions === true && (
|
|
||||||
<i
|
|
||||||
className={"bi-x text-xl absolute right-1 top-1 text-neutral hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"}
|
|
||||||
title="Remove Member"
|
|
||||||
onClick={() => {
|
|
||||||
const updatedMembers = collection.members.filter(
|
|
||||||
(member) => {
|
|
||||||
return member.user.username !== e.user.username;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
members: updatedMembers,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ProfilePhoto
|
|
||||||
src={e.user.image ? e.user.image : undefined}
|
|
||||||
className="border-[3px]"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-bold">{e.user.name}</p>
|
|
||||||
<p className="text-neutral">@{e.user.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex sm:block items-center justify-between gap-5 min-w-[10rem]">
|
|
||||||
<div>
|
|
||||||
<p
|
|
||||||
className={`font-bold text-sm ${
|
|
||||||
permissions === true ? "" : "mb-2"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Permissions
|
|
||||||
</p>
|
|
||||||
{permissions === true && (
|
|
||||||
<p className="text-xs text-neutral mb-2">
|
|
||||||
(Click to toggle.)
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{permissions !== true &&
|
|
||||||
!e.canCreate &&
|
|
||||||
!e.canUpdate &&
|
|
||||||
!e.canDelete ? (
|
|
||||||
<p className="text-sm text-neutral">
|
|
||||||
Has no permissions.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className={
|
|
||||||
permissions === true
|
|
||||||
? "cursor-pointer mr-1"
|
|
||||||
: "mr-1"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="canCreate"
|
|
||||||
className="peer sr-only"
|
|
||||||
checked={e.canCreate}
|
|
||||||
onChange={() => {
|
|
||||||
if (permissions === true) {
|
|
||||||
const updatedMembers = collection.members.map(
|
|
||||||
(member) => {
|
|
||||||
if (
|
|
||||||
member.user.username === e.user.username
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
canCreate: !e.canCreate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
members: updatedMembers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
|
||||||
permissions === true
|
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
|
||||||
: ""
|
|
||||||
} rounded p-1 select-none`}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
className={
|
|
||||||
permissions === true
|
|
||||||
? "cursor-pointer mr-1"
|
|
||||||
: "mr-1"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="canUpdate"
|
|
||||||
className="peer sr-only"
|
|
||||||
checked={e.canUpdate}
|
|
||||||
onChange={() => {
|
|
||||||
if (permissions === true) {
|
|
||||||
const updatedMembers = collection.members.map(
|
|
||||||
(member) => {
|
|
||||||
if (
|
|
||||||
member.user.username === e.user.username
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
canUpdate: !e.canUpdate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
members: updatedMembers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
|
||||||
permissions === true
|
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
|
||||||
: ""
|
|
||||||
} rounded p-1 select-none`}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
className={
|
|
||||||
permissions === true
|
|
||||||
? "cursor-pointer mr-1"
|
|
||||||
: "mr-1"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="canDelete"
|
|
||||||
className="peer sr-only"
|
|
||||||
checked={e.canDelete}
|
|
||||||
onChange={() => {
|
|
||||||
if (permissions === true) {
|
|
||||||
const updatedMembers = collection.members.map(
|
|
||||||
(member) => {
|
|
||||||
if (
|
|
||||||
member.user.username === e.user.username
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
canDelete: !e.canDelete,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return member;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
setCollection({
|
|
||||||
...collection,
|
|
||||||
members: updatedMembers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
|
||||||
permissions === true
|
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
|
||||||
: ""
|
|
||||||
} rounded p-1 select-none`}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="relative border px-2 rounded-md border-neutral-content flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between"
|
|
||||||
title={`'@${collectionOwner.username}' is the owner of this collection.`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ProfilePhoto
|
|
||||||
src={collectionOwner.image ? collectionOwner.image : undefined}
|
|
||||||
className="border-[3px]"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-neutral">@{collectionOwner.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col justify-center min-w-[10rem]">
|
|
||||||
<p className={`font-bold text-sm`}>Permissions</p>
|
|
||||||
<p>Full Access (Owner)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{permissions === true && (
|
|
||||||
<SubmitButton
|
|
||||||
onClick={submit}
|
|
||||||
loading={submitLoader}
|
|
||||||
label={method === "CREATE" ? "Add" : "Save"}
|
|
||||||
className="mx-auto mt-2"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ViewTeam({ collection }: Props) {
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
username: "",
|
|
||||||
image: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOwner = async () => {
|
|
||||||
const owner = await getPublicUserData(collection.ownerId as number);
|
|
||||||
setCollectionOwner(owner);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOwner();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
|
||||||
<p className="ml-10 text-xl font-thin">Team</p>
|
|
||||||
|
|
||||||
<p>Here are all the members who are collaborating on this collection.</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="relative border px-2 rounded-md border-neutral flex min-h-[4rem] gap-2 justify-between"
|
|
||||||
title={`@${collectionOwner.username} is the owner of this collection.`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 w-full">
|
|
||||||
<ProfilePhoto
|
|
||||||
src={collectionOwner.image ? collectionOwner.image : undefined}
|
|
||||||
className="border-[3px]"
|
|
||||||
/>
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex items-center gap-1 w-full justify-between">
|
|
||||||
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
|
||||||
<div className="flex text-xs gap-1 items-center">
|
|
||||||
Admin
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-neutral">@{collectionOwner.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{collection?.members[0]?.user && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-3 rounded-md">
|
|
||||||
{collection.members
|
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
|
||||||
.map((e, i) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="relative border p-2 rounded-md border-neutral flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ProfilePhoto
|
|
||||||
src={e.user.image ? e.user.image : undefined}
|
|
||||||
className="border-[3px]"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-bold">{e.user.name}</p>
|
|
||||||
<p className="text-neutral">@{e.user.username}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import CollectionInfo from "./CollectionInfo";
|
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
|
||||||
import TeamManagement from "./TeamManagement";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import DeleteCollection from "./DeleteCollection";
|
|
||||||
import ViewTeam from "./ViewTeam";
|
|
||||||
|
|
||||||
type Props =
|
|
||||||
| {
|
|
||||||
toggleCollectionModal: Function;
|
|
||||||
activeCollection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
method: "UPDATE";
|
|
||||||
isOwner: boolean;
|
|
||||||
className?: string;
|
|
||||||
defaultIndex?: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
toggleCollectionModal: Function;
|
|
||||||
activeCollection?: CollectionIncludingMembersAndLinkCount;
|
|
||||||
method: "CREATE";
|
|
||||||
isOwner: boolean;
|
|
||||||
className?: string;
|
|
||||||
defaultIndex?: number;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
toggleCollectionModal: Function;
|
|
||||||
activeCollection: CollectionIncludingMembersAndLinkCount;
|
|
||||||
method: "VIEW_TEAM";
|
|
||||||
isOwner: boolean;
|
|
||||||
className?: string;
|
|
||||||
defaultIndex?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CollectionModal({
|
|
||||||
className,
|
|
||||||
defaultIndex,
|
|
||||||
toggleCollectionModal,
|
|
||||||
isOwner,
|
|
||||||
activeCollection,
|
|
||||||
method,
|
|
||||||
}: Props) {
|
|
||||||
const [collection, setCollection] =
|
|
||||||
useState<CollectionIncludingMembersAndLinkCount>(
|
|
||||||
activeCollection || {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
color: "#0ea5e9",
|
|
||||||
isPublic: false,
|
|
||||||
members: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Tab.Group defaultIndex={defaultIndex}>
|
|
||||||
{method === "CREATE" && (
|
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
|
||||||
New Collection
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{method !== "VIEW_TEAM" && (
|
|
||||||
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5">
|
|
||||||
{method === "UPDATE" && (
|
|
||||||
<>
|
|
||||||
{isOwner && (
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
selected
|
|
||||||
? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
|
|
||||||
: "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Collection Info
|
|
||||||
</Tab>
|
|
||||||
)}
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
selected
|
|
||||||
? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
|
|
||||||
: "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isOwner ? "Share & Collaborate" : "View Team"}
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
className={({ selected }) =>
|
|
||||||
selected
|
|
||||||
? "px-2 py-1 bg-sky-200 dark:bg-sky-800 dark:text-white duration-100 rounded-md outline-none"
|
|
||||||
: "px-2 py-1 hover:bg-slate-200 hover:dark:bg-neutral-700 hover:dark:text-white rounded-md duration-100 outline-none"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isOwner ? "Delete Collection" : "Leave Collection"}
|
|
||||||
</Tab>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Tab.List>
|
|
||||||
)}
|
|
||||||
<Tab.Panels>
|
|
||||||
{(isOwner || method === "CREATE") && (
|
|
||||||
<Tab.Panel>
|
|
||||||
<CollectionInfo
|
|
||||||
toggleCollectionModal={toggleCollectionModal}
|
|
||||||
setCollection={setCollection}
|
|
||||||
collection={collection}
|
|
||||||
method={method}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{method === "UPDATE" && (
|
|
||||||
<>
|
|
||||||
<Tab.Panel>
|
|
||||||
<TeamManagement
|
|
||||||
toggleCollectionModal={toggleCollectionModal}
|
|
||||||
setCollection={setCollection}
|
|
||||||
collection={collection}
|
|
||||||
method={method}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
|
||||||
<DeleteCollection
|
|
||||||
toggleDeleteCollectionModal={toggleCollectionModal}
|
|
||||||
collection={collection}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{method === "VIEW_TEAM" && (
|
|
||||||
<>
|
|
||||||
<Tab.Panel>
|
|
||||||
<ViewTeam collection={collection} />
|
|
||||||
</Tab.Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Tab.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
|
||||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
|
||||||
import useLinkStore from "@/store/links";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import useCollectionStore from "@/store/collections";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import SubmitButton from "../../SubmitButton";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import Link from "next/link";
|
|
||||||
import TextInput from "@/components/TextInput";
|
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
|
||||||
|
|
||||||
type Props =
|
|
||||||
| {
|
|
||||||
toggleLinkModal: Function;
|
|
||||||
method: "CREATE";
|
|
||||||
activeLink?: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
toggleLinkModal: Function;
|
|
||||||
method: "UPDATE";
|
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function AddOrEditLink({
|
|
||||||
toggleLinkModal,
|
|
||||||
method,
|
|
||||||
activeLink,
|
|
||||||
}: Props) {
|
|
||||||
const [submitLoader, setSubmitLoader] = useState(false);
|
|
||||||
|
|
||||||
const [optionsExpanded, setOptionsExpanded] = useState(
|
|
||||||
method === "UPDATE" ? true : false
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useSession();
|
|
||||||
|
|
||||||
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>(
|
|
||||||
activeLink || {
|
|
||||||
name: "",
|
|
||||||
url: "",
|
|
||||||
type: "",
|
|
||||||
description: "",
|
|
||||||
tags: [],
|
|
||||||
screenshotPath: "",
|
|
||||||
pdfPath: "",
|
|
||||||
readabilityPath: "",
|
|
||||||
textContent: "",
|
|
||||||
collection: {
|
|
||||||
name: "",
|
|
||||||
ownerId: data?.user.id as number,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { updateLink, addLink } = useLinkStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { collections } = useCollectionStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (method === "CREATE") {
|
|
||||||
if (router.query.id) {
|
|
||||||
const currentCollection = collections.find(
|
|
||||||
(e) => e.id == Number(router.query.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentCollection &&
|
|
||||||
currentCollection.ownerId &&
|
|
||||||
router.asPath.startsWith("/collections/")
|
|
||||||
)
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
collection: {
|
|
||||||
id: currentCollection.id,
|
|
||||||
name: currentCollection.name,
|
|
||||||
ownerId: currentCollection.ownerId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
collection: {
|
|
||||||
// id: ,
|
|
||||||
name: "Unorganized",
|
|
||||||
ownerId: data?.user.id as number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setTags = (e: any) => {
|
|
||||||
const tagNames = e.map((e: any) => {
|
|
||||||
return { name: e.label };
|
|
||||||
});
|
|
||||||
|
|
||||||
setLink({ ...link, tags: tagNames });
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCollection = (e: any) => {
|
|
||||||
if (e?.__isNew__) e.value = null;
|
|
||||||
|
|
||||||
setLink({
|
|
||||||
...link,
|
|
||||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const submit = async () => {
|
|
||||||
setSubmitLoader(true);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
const load = toast.loading(
|
|
||||||
method === "UPDATE" ? "Applying..." : "Creating..."
|
|
||||||
);
|
|
||||||
|
|
||||||
if (method === "UPDATE") response = await updateLink(link);
|
|
||||||
else response = await addLink(link);
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(`Link ${method === "UPDATE" ? "Saved!" : "Created!"}`);
|
|
||||||
toggleLinkModal();
|
|
||||||
} else toast.error(response.data as string);
|
|
||||||
|
|
||||||
setSubmitLoader(false);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
|
||||||
{method === "UPDATE" ? (
|
|
||||||
<div
|
|
||||||
className="text-neutral break-all w-full flex gap-2"
|
|
||||||
title={link.url || ""}
|
|
||||||
>
|
|
||||||
<i className={"bi-link-45deg"}></i>
|
|
||||||
<Link href={link.url || ""} target="_blank" className="w-full">
|
|
||||||
{link.url}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{method === "CREATE" ? (
|
|
||||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
|
||||||
<div className="sm:col-span-3 col-span-5">
|
|
||||||
<p className="mb-2">Address (URL)</p>
|
|
||||||
<TextInput
|
|
||||||
value={link.url || ""}
|
|
||||||
onChange={(e) => setLink({ ...link, url: e.target.value })}
|
|
||||||
placeholder="e.g. http://example.com/"
|
|
||||||
className="bg-base-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2 col-span-5">
|
|
||||||
<p className="mb-2">Collection</p>
|
|
||||||
{link.collection.name ? (
|
|
||||||
<CollectionSelection
|
|
||||||
onChange={setCollection}
|
|
||||||
// defaultValue={{
|
|
||||||
// label: link.collection.name,
|
|
||||||
// value: link.collection.id,
|
|
||||||
// }}
|
|
||||||
defaultValue={
|
|
||||||
link.collection.id
|
|
||||||
? {
|
|
||||||
value: link.collection.id,
|
|
||||||
label: link.collection.name,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: null as unknown as number,
|
|
||||||
label: "Unorganized",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{optionsExpanded ? (
|
|
||||||
<div>
|
|
||||||
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
|
||||||
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
|
||||||
<p className="mb-2">Name</p>
|
|
||||||
<TextInput
|
|
||||||
value={link.name}
|
|
||||||
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
|
||||||
placeholder="e.g. Example Link"
|
|
||||||
className="bg-base-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{method === "UPDATE" ? (
|
|
||||||
<div>
|
|
||||||
<p className="mb-2">Collection</p>
|
|
||||||
{link.collection.name ? (
|
|
||||||
<CollectionSelection
|
|
||||||
onChange={setCollection}
|
|
||||||
defaultValue={
|
|
||||||
link.collection.name && link.collection.id
|
|
||||||
? {
|
|
||||||
value: link.collection.id,
|
|
||||||
label: link.collection.name,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
value: null as unknown as number,
|
|
||||||
label: "Unorganized",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="mb-2">Tags</p>
|
|
||||||
<TagSelection
|
|
||||||
onChange={setTags}
|
|
||||||
defaultValue={link.tags.map((e) => {
|
|
||||||
return { label: e.name, value: e.id };
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<p className="mb-2">Description</p>
|
|
||||||
<textarea
|
|
||||||
value={unescapeString(link.description) as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setLink({ ...link, description: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
method === "CREATE"
|
|
||||||
? "Will be auto generated if nothing is provided."
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
<div className="flex justify-between items-stretch mt-2">
|
|
||||||
<div
|
|
||||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
|
||||||
className={`${
|
|
||||||
method === "UPDATE" ? "hidden" : ""
|
|
||||||
} rounded-md cursor-pointer btn btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
|
||||||
>
|
|
||||||
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SubmitButton
|
|
||||||
onClick={submit}
|
|
||||||
label={method === "CREATE" ? "Add" : "Save"}
|
|
||||||
loading={submitLoader}
|
|
||||||
className={`${method === "CREATE" ? "" : "mx-auto"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,177 +0,0 @@
|
||||||
import {
|
|
||||||
ArchivedFormat,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import useLinkStore from "@/store/links";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import {
|
|
||||||
pdfAvailable,
|
|
||||||
screenshotAvailable,
|
|
||||||
} from "@/lib/shared/getArchiveValidity";
|
|
||||||
|
|
||||||
export default function PreservedFormats() {
|
|
||||||
const session = useSession();
|
|
||||||
const { links, getLink } = useLinkStore();
|
|
||||||
|
|
||||||
const [link, setLink] = useState<LinkIncludingShortenedCollectionAndTags>();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (links) setLink(links.find((e) => e.id === Number(router.query.id)));
|
|
||||||
}, [links]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let interval: any;
|
|
||||||
if (screenshotAvailable(link) && pdfAvailable(link)) {
|
|
||||||
let isPublicRoute = router.pathname.startsWith("/public")
|
|
||||||
? true
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
interval = setInterval(
|
|
||||||
() => getLink(link?.id as number, isPublicRoute),
|
|
||||||
5000
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
|
|
||||||
|
|
||||||
const updateArchive = async () => {
|
|
||||||
const load = toast.loading("Sending request...");
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/links/${link?.id}/archive`, {
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(`Link is being archived...`);
|
|
||||||
getLink(link?.id as number);
|
|
||||||
} else toast.error(data.response);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = (format: ArchivedFormat) => {
|
|
||||||
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
|
||||||
fetch(path)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
// Create a temporary link and click it to trigger the download
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = path;
|
|
||||||
link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
|
|
||||||
link.click();
|
|
||||||
} else {
|
|
||||||
console.error("Failed to download file");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error:", error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-col gap-3 sm:w-[35rem] w-80 pt-3`}>
|
|
||||||
{screenshotAvailable(link) ? (
|
|
||||||
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Screenshot</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<div
|
|
||||||
onClick={() => handleDownload(ArchivedFormat.png)}
|
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/api/v1/archives/${link?.id}?format=${
|
|
||||||
link?.screenshotPath?.endsWith("png")
|
|
||||||
? ArchivedFormat.png
|
|
||||||
: ArchivedFormat.jpeg
|
|
||||||
}`}
|
|
||||||
target="_blank"
|
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{pdfAvailable(link) ? (
|
|
||||||
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>PDF</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<div
|
|
||||||
onClick={() => handleDownload(ArchivedFormat.pdf)}
|
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/api/v1/archives/${link?.id}?format=${ArchivedFormat.pdf}`}
|
|
||||||
target="_blank"
|
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
|
||||||
>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
|
||||||
{link?.collection.ownerId === session.data?.user.id ? (
|
|
||||||
<div
|
|
||||||
className={`btn btn-accent dark:border-violet-400 text-white ${
|
|
||||||
screenshotAvailable(link) && pdfAvailable(link) ? "mt-3" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => updateArchive()}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>Update Preserved Formats</p>
|
|
||||||
<p className="text-xs">(Refresh Link)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : undefined}
|
|
||||||
<Link
|
|
||||||
href={`https://web.archive.org/web/${link?.url?.replace(
|
|
||||||
/(^\w+:|^)\/\//,
|
|
||||||
""
|
|
||||||
)}`}
|
|
||||||
target="_blank"
|
|
||||||
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
|
|
||||||
screenshotAvailable(link) && pdfAvailable(link) ? "sm:mt-3" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="whitespace-nowrap">
|
|
||||||
View Latest Snapshot on archive.org
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
|
||||||
import AddOrEditLink from "./AddOrEditLink";
|
|
||||||
import PreservedFormats from "./PreservedFormats";
|
|
||||||
|
|
||||||
type Props =
|
|
||||||
| {
|
|
||||||
toggleLinkModal: Function;
|
|
||||||
method: "CREATE";
|
|
||||||
activeLink?: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
toggleLinkModal: Function;
|
|
||||||
method: "UPDATE";
|
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
toggleLinkModal: Function;
|
|
||||||
method: "FORMATS";
|
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function LinkModal({
|
|
||||||
className,
|
|
||||||
toggleLinkModal,
|
|
||||||
activeLink,
|
|
||||||
method,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{method === "CREATE" ? (
|
|
||||||
<>
|
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
|
||||||
Create a New Link
|
|
||||||
</p>
|
|
||||||
<AddOrEditLink toggleLinkModal={toggleLinkModal} method="CREATE" />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{activeLink && method === "UPDATE" ? (
|
|
||||||
<>
|
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">Edit Link</p>
|
|
||||||
<AddOrEditLink
|
|
||||||
toggleLinkModal={toggleLinkModal}
|
|
||||||
method="UPDATE"
|
|
||||||
activeLink={activeLink}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{method === "FORMATS" ? (
|
|
||||||
<>
|
|
||||||
<p className="ml-10 mt-[0.1rem] text-xl mb-3 font-thin">
|
|
||||||
Preserved Formats
|
|
||||||
</p>
|
|
||||||
<PreservedFormats />
|
|
||||||
</>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -19,17 +19,6 @@ export default function EditCollectionSharingModal({
|
||||||
onClose,
|
onClose,
|
||||||
activeCollection,
|
activeCollection,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
useEffect(() => {
|
|
||||||
const fetchOwner = async () => {
|
|
||||||
const owner = await getPublicUserData(collection.ownerId as number);
|
|
||||||
setCollectionOwner(owner);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOwner();
|
|
||||||
|
|
||||||
setCollection(activeCollection);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [collection, setCollection] =
|
const [collection, setCollection] =
|
||||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||||
|
|
||||||
|
@ -70,12 +59,25 @@ export default function EditCollectionSharingModal({
|
||||||
const [memberUsername, setMemberUsername] = useState("");
|
const [memberUsername, setMemberUsername] = useState("");
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
id: null,
|
id: null as unknown as number,
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOwner = async () => {
|
||||||
|
const owner = await getPublicUserData(collection.ownerId as number);
|
||||||
|
setCollectionOwner(owner);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOwner();
|
||||||
|
|
||||||
|
setCollection(activeCollection);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const setMemberState = (newMember: Member) => {
|
const setMemberState = (newMember: Member) => {
|
||||||
if (!collection) return null;
|
if (!collection) return null;
|
||||||
|
|
||||||
|
@ -174,7 +176,7 @@ export default function EditCollectionSharingModal({
|
||||||
setMemberState
|
setMemberState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="btn btn-accent text-white btn-square btn-sm h-10 w-10"
|
className="btn btn-accent dark:border-violet-400 text-white btn-square btn-sm h-10 w-10"
|
||||||
>
|
>
|
||||||
<i className="bi-person-add text-xl"></i>
|
<i className="bi-person-add text-xl"></i>
|
||||||
</div>
|
</div>
|
||||||
|
@ -231,7 +233,9 @@ export default function EditCollectionSharingModal({
|
||||||
>
|
>
|
||||||
{permissions === true && (
|
{permissions === true && (
|
||||||
<i
|
<i
|
||||||
className={"bi-x text-xl absolute right-1 top-1 text-neutral hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"}
|
className={
|
||||||
|
"bi-x text-xl absolute right-1 top-1 text-neutral hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
|
||||||
|
}
|
||||||
title="Remove Member"
|
title="Remove Member"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedMembers = collection.members.filter(
|
const updatedMembers = collection.members.filter(
|
||||||
|
|
|
@ -1,13 +1,22 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { ArchivedFormat, LinkIncludingShortenedCollectionAndTags, } from "@/types/global";
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Modal from "../Modal";
|
import Modal from "../Modal";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { pdfAvailable, readabilityAvailable, screenshotAvailable, } from "@/lib/shared/getArchiveValidity";
|
import {
|
||||||
|
pdfAvailable,
|
||||||
|
readabilityAvailable,
|
||||||
|
screenshotAvailable,
|
||||||
|
} from "@/lib/shared/getArchiveValidity";
|
||||||
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
||||||
|
import useAccountStore from "@/store/account";
|
||||||
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
|
@ -18,11 +27,56 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { getLink } = useLinkStore();
|
const { getLink } = useLinkStore();
|
||||||
|
|
||||||
|
const { account } = useAccountStore();
|
||||||
|
|
||||||
const [link, setLink] =
|
const [link, setLink] =
|
||||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
|
id: null as unknown as number,
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
image: "",
|
||||||
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOwner = async () => {
|
||||||
|
if (link.collection.ownerId !== account.id) {
|
||||||
|
const owner = await getPublicUserData(
|
||||||
|
link.collection.ownerId as number
|
||||||
|
);
|
||||||
|
setCollectionOwner(owner);
|
||||||
|
} else if (link.collection.ownerId === account.id) {
|
||||||
|
setCollectionOwner({
|
||||||
|
id: account.id as number,
|
||||||
|
name: account.name,
|
||||||
|
username: account.username as string,
|
||||||
|
image: account.image as string,
|
||||||
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOwner();
|
||||||
|
}, [link.collection.ownerId]);
|
||||||
|
|
||||||
|
const isReady = () => {
|
||||||
|
return (
|
||||||
|
collectionOwner.archiveAsScreenshot ===
|
||||||
|
(link && link.pdfPath && link.pdfPath !== "pending") &&
|
||||||
|
collectionOwner.archiveAsPDF ===
|
||||||
|
(link && link.pdfPath && link.pdfPath !== "pending") &&
|
||||||
|
link &&
|
||||||
|
link.readabilityPath &&
|
||||||
|
link.readabilityPath !== "pending"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isPublicRoute = router.pathname.startsWith("/public")
|
let isPublicRoute = router.pathname.startsWith("/public")
|
||||||
? true
|
? true
|
||||||
|
@ -36,7 +90,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
})();
|
})();
|
||||||
|
|
||||||
let interval: any;
|
let interval: any;
|
||||||
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
|
||||||
|
if (!isReady()) {
|
||||||
interval = setInterval(async () => {
|
interval = setInterval(async () => {
|
||||||
const data = await getLink(link.id as number, isPublicRoute);
|
const data = await getLink(link.id as number, isPublicRoute);
|
||||||
setLink(
|
setLink(
|
||||||
|
@ -69,7 +124,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
toast.success(`Link is being archived...`);
|
toast.success(`Link is being archived...`);
|
||||||
getLink(link?.id as number);
|
await getLink(link?.id as number);
|
||||||
} else toast.error(data.response);
|
} else toast.error(data.response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,69 +134,94 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
|
|
||||||
<div className="divider mb-2 mt-1"></div>
|
<div className="divider mb-2 mt-1"></div>
|
||||||
|
|
||||||
{screenshotAvailable(link) ||
|
{isReady() &&
|
||||||
|
(screenshotAvailable(link) ||
|
||||||
pdfAvailable(link) ||
|
pdfAvailable(link) ||
|
||||||
readabilityAvailable(link) ? (
|
readabilityAvailable(link)) ? (
|
||||||
<p className="mb-3">
|
<p className="mb-3">
|
||||||
The following formats are available for this link:
|
The following formats are available for this link:
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mb-3">No preserved formats available.</p>
|
""
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex flex-col gap-3`}>
|
<div className={`flex flex-col gap-3`}>
|
||||||
|
{isReady() ? (
|
||||||
|
<>
|
||||||
{readabilityAvailable(link) ? (
|
{readabilityAvailable(link) ? (
|
||||||
<PreservedFormatRow name={'Readable'} icon={'bi-file-earmark-text'} format={ArchivedFormat.readability}
|
<PreservedFormatRow
|
||||||
activeLink={link}/>
|
name={"Readable"}
|
||||||
|
icon={"bi-file-earmark-text"}
|
||||||
|
format={ArchivedFormat.readability}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{screenshotAvailable(link) ? (
|
{screenshotAvailable(link) ? (
|
||||||
<PreservedFormatRow name={'Screenshot'} icon={'bi-file-earmark-image'} format={ArchivedFormat.png}
|
<PreservedFormatRow
|
||||||
activeLink={link} downloadable={true}/>
|
name={"Screenshot"}
|
||||||
|
icon={"bi-file-earmark-image"}
|
||||||
|
format={ArchivedFormat.png}
|
||||||
|
activeLink={link}
|
||||||
|
downloadable={true}
|
||||||
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{pdfAvailable(link) ? (
|
{pdfAvailable(link) ? (
|
||||||
<PreservedFormatRow name={'PDF'} icon={'bi-file-earmark-pdf'} format={ArchivedFormat.pdf}
|
<PreservedFormatRow
|
||||||
activeLink={link} downloadable={true}/>
|
name={"PDF"}
|
||||||
|
icon={"bi-file-earmark-pdf"}
|
||||||
|
format={ArchivedFormat.pdf}
|
||||||
|
activeLink={link}
|
||||||
|
downloadable={true}
|
||||||
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
</>
|
||||||
<div className="flex flex-col-reverse sm:flex-row sm:gap-3 items-center justify-center">
|
) : (
|
||||||
{link?.collection.ownerId === session.data?.user.id ? (
|
|
||||||
<div
|
<div
|
||||||
className={`btn btn-accent w-1/2 dark:border-violet-400 text-white ${
|
className={`w-full h-full flex flex-col justify-center p-10 skeleton bg-base-200`}
|
||||||
screenshotAvailable(link) &&
|
|
||||||
pdfAvailable(link) &&
|
|
||||||
readabilityAvailable(link)
|
|
||||||
? "mt-3"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => updateArchive()}
|
|
||||||
>
|
>
|
||||||
<div>
|
<i className="bi-stack drop-shadow text-primary text-8xl mx-auto mb-5"></i>
|
||||||
<p>Update Preserved Formats</p>
|
<p className="text-center text-2xl">
|
||||||
<p className="text-xs">(Refresh Link)</p>
|
The Link preservation is in the queue
|
||||||
|
</p>
|
||||||
|
<p className="text-center text-lg">
|
||||||
|
Please check back later to see the result
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : undefined}
|
|
||||||
|
<div
|
||||||
|
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
||||||
|
isReady() ? "sm:mt " : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`https://web.archive.org/web/${link?.url?.replace(
|
href={`https://web.archive.org/web/${link?.url?.replace(
|
||||||
/(^\w+:|^)\/\//,
|
/(^\w+:|^)\/\//,
|
||||||
""
|
""
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-1/2 justify-center items-center text-sm ${
|
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-1/2 justify-center items-center text-sm`}
|
||||||
screenshotAvailable(link) &&
|
|
||||||
pdfAvailable(link) &&
|
|
||||||
readabilityAvailable(link)
|
|
||||||
? "sm:mt-3"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<p className="whitespace-nowrap">
|
<p className="whitespace-nowrap">
|
||||||
View latest snapshot on archive.org
|
View latest snapshot on archive.org
|
||||||
</p>
|
</p>
|
||||||
<i className="bi-box-arrow-up-right" />
|
<i className="bi-box-arrow-up-right" />
|
||||||
</Link>
|
</Link>
|
||||||
|
{link?.collection.ownerId === session.data?.user.id ? (
|
||||||
|
<div
|
||||||
|
className={`btn w-1/2 btn-outline`}
|
||||||
|
onClick={() => updateArchive()}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>Refresh Preserved Formats</p>
|
||||||
|
<p className="text-xs">
|
||||||
|
This deletes the current preservations
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import LinkModal from "./Modal/Link";
|
|
||||||
import {
|
|
||||||
CollectionIncludingMembersAndLinkCount,
|
|
||||||
LinkIncludingShortenedCollectionAndTags,
|
|
||||||
} from "@/types/global";
|
|
||||||
import CollectionModal from "./Modal/Collection";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
export default function ModalManagement() {
|
|
||||||
const { modal, setModal } = useModalStore();
|
|
||||||
|
|
||||||
const toggleModal = () => {
|
|
||||||
setModal(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
useEffect(() => {
|
|
||||||
toggleModal();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
if (modal && modal.modal === "LINK")
|
|
||||||
return (
|
|
||||||
<Modal toggleModal={toggleModal}>
|
|
||||||
<LinkModal
|
|
||||||
toggleLinkModal={toggleModal}
|
|
||||||
method={modal.method}
|
|
||||||
activeLink={modal.active as LinkIncludingShortenedCollectionAndTags}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
else if (modal && modal.modal === "COLLECTION")
|
|
||||||
return (
|
|
||||||
<Modal toggleModal={toggleModal}>
|
|
||||||
<CollectionModal
|
|
||||||
toggleCollectionModal={toggleModal}
|
|
||||||
method={modal.method}
|
|
||||||
isOwner={modal.isOwner as boolean}
|
|
||||||
defaultIndex={modal.defaultIndex}
|
|
||||||
activeCollection={
|
|
||||||
modal.active as CollectionIncludingMembersAndLinkCount
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
else return <></>;
|
|
||||||
}
|
|
|
@ -10,14 +10,20 @@ import { useRouter } from "next/router";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
name: string;
|
||||||
icon: string,
|
icon: string;
|
||||||
format: ArchivedFormat,
|
format: ArchivedFormat;
|
||||||
activeLink: LinkIncludingShortenedCollectionAndTags,
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
downloadable?: boolean,
|
downloadable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PreservedFormatRow({ name, icon, format, activeLink, downloadable }: Props) {
|
export default function PreservedFormatRow({
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
format,
|
||||||
|
activeLink,
|
||||||
|
downloadable,
|
||||||
|
}: Props) {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const { getLink } = useLinkStore();
|
const { getLink } = useLinkStore();
|
||||||
|
|
||||||
|
@ -84,7 +90,7 @@ export default function PreservedFormatRow({ name, icon, format, activeLink, dow
|
||||||
// Create a temporary link and click it to trigger the download
|
// Create a temporary link and click it to trigger the download
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = path;
|
link.href = path;
|
||||||
link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
|
link.download = format === ArchivedFormat.pdf ? "PDF" : "Screenshot";
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to download file");
|
console.error("Failed to download file");
|
||||||
|
@ -108,26 +114,16 @@ export default function PreservedFormatRow({ name, icon, format, activeLink, dow
|
||||||
{downloadable || false ? (
|
{downloadable || false ? (
|
||||||
<div
|
<div
|
||||||
onClick={() => handleDownload()}
|
onClick={() => handleDownload()}
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
className="btn btn-sm btn-square"
|
||||||
>
|
>
|
||||||
<i className="bi-cloud-arrow-down text-xl text-neutral" />
|
<i className="bi-cloud-arrow-down text-xl text-neutral" />
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={`/preserved/${link?.id}?format=${format}`}
|
||||||
`${
|
|
||||||
format === ArchivedFormat.readability
|
|
||||||
? `/preserved/${link?.id}?format=${format}`
|
|
||||||
: `/api/v1/archives/${link?.id}?format=${
|
|
||||||
link.screenshotPath?.endsWith("png")
|
|
||||||
? ArchivedFormat.png
|
|
||||||
: ArchivedFormat.jpeg
|
|
||||||
}`
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
className="btn btn-sm btn-square"
|
||||||
>
|
>
|
||||||
<i className="bi-box-arrow-up-right text-xl text-neutral" />
|
<i className="bi-box-arrow-up-right text-xl text-neutral" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -31,9 +31,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-person text-primary text-2xl"></i>
|
||||||
className="bi-person text-primary text-2xl"
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<p className="truncate w-full pr-7">Account</p>
|
<p className="truncate w-full pr-7">Account</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,9 +45,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-palette text-primary text-2xl"></i>
|
||||||
className="bi-palette text-primary text-2xl"
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<p className="truncate w-full pr-7">Appearance</p>
|
<p className="truncate w-full pr-7">Appearance</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,9 +59,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-archive text-primary text-2xl"></i>
|
||||||
className="bi-archive text-primary text-2xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">Archive</p>
|
<p className="truncate w-full pr-7">Archive</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -78,9 +72,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-key text-primary text-2xl"></i>
|
||||||
className="bi-key text-primary text-2xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">API Keys</p>
|
<p className="truncate w-full pr-7">API Keys</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -93,9 +85,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-lock text-primary text-2xl"></i>
|
||||||
className="bi-lock text-primary text-2xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">Password</p>
|
<p className="truncate w-full pr-7">Password</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -109,9 +99,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
: "hover:bg-neutral/20"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-credit-card text-primary text-xl"></i>
|
||||||
className="bi-credit-card text-primary text-xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">Billing</p>
|
<p className="truncate w-full pr-7">Billing</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -130,9 +118,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-question-circle text-primary text-xl"></i>
|
||||||
className="bi-question-circle text-primary text-xl"
|
|
||||||
></i>
|
|
||||||
|
|
||||||
<p className="truncate w-full pr-7">Help</p>
|
<p className="truncate w-full pr-7">Help</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,9 +128,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-github text-primary text-xl"></i>
|
||||||
className="bi-github text-primary text-xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">GitHub</p>
|
<p className="truncate w-full pr-7">GitHub</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -153,9 +137,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-twitter-x text-primary text-xl"></i>
|
||||||
className="bi-twitter-x text-primary text-xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">Twitter</p>
|
<p className="truncate w-full pr-7">Twitter</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -164,9 +146,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<i
|
<i className="bi-mastodon text-primary text-xl"></i>
|
||||||
className="bi-mastodon text-primary text-xl"
|
|
||||||
></i>
|
|
||||||
<p className="truncate w-full pr-7">Mastodon</p>
|
<p className="truncate w-full pr-7">Mastodon</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
className={`bg-base-200 h-full w-64 xl:w-80 overflow-y-auto border-solid border border-base-200 border-r-neutral-content p-2 z-20 ${
|
className={`bg-base-200 h-full w-72 lg:w-80 overflow-y-auto border-solid border border-base-200 border-r-neutral-content p-2 z-20 ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -70,7 +70,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<SidebarHighlightLink
|
<SidebarHighlightLink
|
||||||
title={"All Collections"}
|
title={"All Collections"}
|
||||||
href={`/collections`}
|
href={`/collections`}
|
||||||
icon={"bi-folder2"}
|
icon={"bi-folder"}
|
||||||
active={active === `/collections`}
|
active={active === `/collections`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,9 +28,7 @@ export default function SidebarHighlightLink({
|
||||||
<i className={`${icon} text-primary text-2xl drop-shadow`}></i>
|
<i className={`${icon} text-primary text-2xl drop-shadow`}></i>
|
||||||
</div>
|
</div>
|
||||||
<div className={"mt-1"}>
|
<div className={"mt-1"}>
|
||||||
<p className="truncate w-full text-xs font-semibold xl:text-sm">
|
<p className="truncate w-full font-semibold text-sm">{title}</p>
|
||||||
{title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import Navbar from "@/components/Navbar";
|
||||||
import AnnouncementBar from "@/components/AnnouncementBar";
|
import AnnouncementBar from "@/components/AnnouncementBar";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import ModalManagement from "@/components/ModalManagement";
|
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import getLatestVersion from "@/lib/client/getLatestVersion";
|
import getLatestVersion from "@/lib/client/getLatestVersion";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -11,14 +9,6 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MainLayout({ children }: Props) {
|
export default function MainLayout({ children }: Props) {
|
||||||
const { modal } = useModalStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
modal
|
|
||||||
? (document.body.style.overflow = "hidden")
|
|
||||||
: (document.body.style.overflow = "auto");
|
|
||||||
}, [modal]);
|
|
||||||
|
|
||||||
const showAnnouncementBar = localStorage.getItem("showAnnouncementBar");
|
const showAnnouncementBar = localStorage.getItem("showAnnouncementBar");
|
||||||
const [showAnnouncement, setShowAnnouncement] = useState(
|
const [showAnnouncement, setShowAnnouncement] = useState(
|
||||||
showAnnouncementBar ? showAnnouncementBar === "true" : true
|
showAnnouncementBar ? showAnnouncementBar === "true" : true
|
||||||
|
@ -44,8 +34,6 @@ export default function MainLayout({ children }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
|
||||||
|
|
||||||
{showAnnouncement ? (
|
{showAnnouncement ? (
|
||||||
<AnnouncementBar toggleAnnouncementBar={toggleAnnouncementBar} />
|
<AnnouncementBar toggleAnnouncementBar={toggleAnnouncementBar} />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -60,7 +48,7 @@ export default function MainLayout({ children }: Props) {
|
||||||
<div
|
<div
|
||||||
className={`w-full flex flex-col min-h-${
|
className={`w-full flex flex-col min-h-${
|
||||||
showAnnouncement ? "full" : "screen"
|
showAnnouncement ? "full" : "screen"
|
||||||
} lg:ml-64 xl:ml-80 ${showAnnouncement ? "mt-10" : ""}`}
|
} lg:ml-80 ${showAnnouncement ? "mt-10" : ""}`}
|
||||||
>
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import SettingsSidebar from "@/components/SettingsSidebar";
|
import SettingsSidebar from "@/components/SettingsSidebar";
|
||||||
import React, { ReactNode, useEffect, useState } from "react";
|
import React, { ReactNode, useEffect, useState } from "react";
|
||||||
import ModalManagement from "@/components/ModalManagement";
|
|
||||||
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 Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -12,16 +10,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsLayout({ children }: Props) {
|
export default function SettingsLayout({ children }: Props) {
|
||||||
const { modal } = useModalStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
modal
|
|
||||||
? (document.body.style.overflow = "hidden")
|
|
||||||
: (document.body.style.overflow = "auto");
|
|
||||||
}, [modal]);
|
|
||||||
|
|
||||||
const [sidebar, setSidebar] = useState(false);
|
const [sidebar, setSidebar] = useState(false);
|
||||||
|
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
|
@ -40,8 +30,6 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
|
||||||
|
|
||||||
<div className="flex max-w-screen-md mx-auto">
|
<div className="flex max-w-screen-md mx-auto">
|
||||||
<div className="hidden lg:block fixed h-screen">
|
<div className="hidden lg:block fixed h-screen">
|
||||||
<SettingsSidebar />
|
<SettingsSidebar />
|
||||||
|
|
|
@ -45,9 +45,18 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
|
||||||
where: { id: link.id },
|
where: { id: link.id },
|
||||||
data: {
|
data: {
|
||||||
type: linkType,
|
type: linkType,
|
||||||
screenshotPath: user.archiveAsScreenshot ? "pending" : undefined,
|
screenshotPath:
|
||||||
pdfPath: user.archiveAsPDF ? "pending" : undefined,
|
user.archiveAsScreenshot &&
|
||||||
readabilityPath: "pending",
|
!link.screenshotPath?.startsWith("archive")
|
||||||
|
? "pending"
|
||||||
|
: undefined,
|
||||||
|
pdfPath:
|
||||||
|
user.archiveAsPDF && !link.pdfPath?.startsWith("archive")
|
||||||
|
? "pending"
|
||||||
|
: undefined,
|
||||||
|
readabilityPath: !link.readabilityPath?.startsWith("archive")
|
||||||
|
? "pending"
|
||||||
|
: undefined,
|
||||||
lastPreserved: new Date().toISOString(),
|
lastPreserved: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,6 +74,8 @@ export default async function getPublicUser(
|
||||||
name: lessSensitiveInfo.name,
|
name: lessSensitiveInfo.name,
|
||||||
username: lessSensitiveInfo.username,
|
username: lessSensitiveInfo.username,
|
||||||
image: lessSensitiveInfo.image,
|
image: lessSensitiveInfo.image,
|
||||||
|
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
||||||
|
archiveAsPdf: lessSensitiveInfo.archiveAsPDF,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { response: data, status: 200 };
|
return { response: data, status: 200 };
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import archiveHandler from "@/lib/api/archiveHandler";
|
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import verifyUser from "@/lib/api/verifyUser";
|
import verifyUser from "@/lib/api/verifyUser";
|
||||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
|
import removeFile from "@/lib/api/storage/removeFile";
|
||||||
|
import { Collection, Link } from "@prisma/client";
|
||||||
|
|
||||||
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
|
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
|
||||||
|
|
||||||
|
@ -42,20 +43,17 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
|
||||||
} minutes or create a new one.`,
|
} minutes or create a new one.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (link.url && isValidUrl(link.url)) {
|
if (!link.url || !isValidUrl(link.url))
|
||||||
archiveHandler(link);
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
response: "Link is not a webpage to be archived.",
|
response: "Invalid URL.",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
await deleteArchivedFiles(link);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
response: "Link is being archived.",
|
response: "Link is being archived.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - Later?
|
|
||||||
// else if (req.method === "DELETE") {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTimezoneDifferenceInMinutes = (future: Date, past: Date) => {
|
const getTimezoneDifferenceInMinutes = (future: Date, past: Date) => {
|
||||||
|
@ -68,3 +66,26 @@ const getTimezoneDifferenceInMinutes = (future: Date, past: Date) => {
|
||||||
|
|
||||||
return diffInMinutes;
|
return diffInMinutes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteArchivedFiles = async (link: Link & { collection: Collection }) => {
|
||||||
|
await prisma.link.update({
|
||||||
|
where: {
|
||||||
|
id: link.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
screenshotPath: null,
|
||||||
|
pdfPath: null,
|
||||||
|
readabilityPath: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await removeFile({
|
||||||
|
filePath: `archives/${link.collection.id}/${link.id}.pdf`,
|
||||||
|
});
|
||||||
|
await removeFile({
|
||||||
|
filePath: `archives/${link.collection.id}/${link.id}.png`,
|
||||||
|
});
|
||||||
|
await removeFile({
|
||||||
|
filePath: `archives/${link.collection.id}/${link.id}_readability.json`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -55,6 +55,8 @@ export default function Index() {
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -70,6 +72,8 @@ export default function Index() {
|
||||||
name: account.name,
|
name: account.name,
|
||||||
username: account.username as string,
|
username: account.username as string,
|
||||||
image: account.image as string,
|
image: account.image as string,
|
||||||
|
archiveAsScreenshot: account.archiveAsScreenshot as boolean,
|
||||||
|
archiveAsPDF: account.archiveAsPDF as boolean,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -118,7 +122,7 @@ export default function Index() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dropdown dropdown-bottom dropdown-end">
|
<div className="dropdown dropdown-bottom dropdown-end mt-2">
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
|
|
|
@ -56,19 +56,11 @@ export default function Collections() {
|
||||||
|
|
||||||
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] ? (
|
{sortedCollections.filter((e) => e.ownerId !== data?.user.id)[0] ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-3 my-5">
|
<PageHeader
|
||||||
<i className="bi-folder text-3xl sm:text-2xl text-primary drop-shadow"></i>
|
icon={"bi-folder"}
|
||||||
|
title={"Other Collections"}
|
||||||
<div>
|
description={"Shared collections you're a member of"}
|
||||||
<p className="text-3xl capitalize font-thin">
|
/>
|
||||||
Other Collections
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="sm:text-sm text-xs">
|
|
||||||
Shared collections you're a member of
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="grid xl:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
{sortedCollections
|
{sortedCollections
|
||||||
|
|
|
@ -8,7 +8,6 @@ import useLinks from "@/hooks/useLinks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||||
import DashboardItem from "@/components/DashboardItem";
|
import DashboardItem from "@/components/DashboardItem";
|
||||||
|
@ -20,8 +19,6 @@ export default function Dashboard() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
const { tags } = useTagStore();
|
const { tags } = useTagStore();
|
||||||
|
|
||||||
const { setModal } = useModalStore();
|
|
||||||
|
|
||||||
const [numberOfLinks, setNumberOfLinks] = useState(0);
|
const [numberOfLinks, setNumberOfLinks] = useState(0);
|
||||||
|
|
||||||
const [showLinks, setShowLinks] = useState(3);
|
const [showLinks, setShowLinks] = useState(3);
|
||||||
|
@ -100,14 +97,14 @@ export default function Dashboard() {
|
||||||
description={"A brief overview of your data"}
|
description={"A brief overview of your data"}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
|
<div className="flex justify-evenly flex-col xl:flex-row xl:items-center gap-2 xl:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={numberOfLinks === 1 ? "Link" : "Links"}
|
name={numberOfLinks === 1 ? "Link" : "Links"}
|
||||||
value={numberOfLinks}
|
value={numberOfLinks}
|
||||||
icon={"bi-link-45deg"}
|
icon={"bi-link-45deg"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="divider md:divider-horizontal"></div>
|
<div className="divider xl:divider-horizontal"></div>
|
||||||
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={collections.length === 1 ? "Collection" : "Collections"}
|
name={collections.length === 1 ? "Collection" : "Collections"}
|
||||||
|
@ -115,7 +112,7 @@ export default function Dashboard() {
|
||||||
icon={"bi-folder"}
|
icon={"bi-folder"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="divider md:divider-horizontal"></div>
|
<div className="divider xl:divider-horizontal"></div>
|
||||||
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={tags.length === 1 ? "Tag" : "Tags"}
|
name={tags.length === 1 ? "Tag" : "Tags"}
|
||||||
|
@ -127,8 +124,11 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<i className="bi-clock-history text-primary text-2xl drop-shadow"></i>
|
<PageHeader
|
||||||
<p className="text-2xl">Recent</p>
|
icon={"bi-clock-history"}
|
||||||
|
title={"Recent"}
|
||||||
|
description={"Recently added Links"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/links"
|
href="/links"
|
||||||
|
@ -171,7 +171,7 @@ export default function Dashboard() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNewLinkModal(true);
|
setNewLinkModal(true);
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-2 text-sm btn btn-accent dark:border-accent text-white"
|
className="inline-flex items-center gap-2 text-sm btn btn-accent dark:border-violet-400 text-white"
|
||||||
>
|
>
|
||||||
<i className="bi-plus-lg text-xl duration-100"></i>
|
<i className="bi-plus-lg text-xl duration-100"></i>
|
||||||
<span className="group-hover:opacity-0 text-right duration-100">
|
<span className="group-hover:opacity-0 text-right duration-100">
|
||||||
|
@ -239,8 +239,11 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<i className="bi-pin-angle text-primary text-2xl drop-shadow"></i>
|
<PageHeader
|
||||||
<p className="text-2xl">Pinned</p>
|
icon={"bi-pin-angle"}
|
||||||
|
title={"Pinned"}
|
||||||
|
description={"Your pinned Links"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/links/pinned"
|
href="/links/pinned"
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
import ReadableView from "@/components/ReadableView";
|
import ReadableView from "@/components/ReadableView";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
@ -30,7 +33,22 @@ export default function Index() {
|
||||||
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
|
{/* <div className="fixed left-1/2 transform -translate-x-1/2 w-fit py-1 px-3 bg-base-200 border border-neutral-content rounded-md">
|
||||||
Readable
|
Readable
|
||||||
</div> */}
|
</div> */}
|
||||||
{link && <ReadableView link={link} />}
|
{link && Number(router.query.format) === ArchivedFormat.readability && (
|
||||||
|
<ReadableView link={link} />
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.pdf && (
|
||||||
|
<iframe
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.pdf}`}
|
||||||
|
className="w-full h-screen border-none"
|
||||||
|
></iframe>
|
||||||
|
)}
|
||||||
|
{link && Number(router.query.format) === ArchivedFormat.png && (
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src={`/api/v1/archives/${link.id}?format=${ArchivedFormat.png}`}
|
||||||
|
className="object-contain w-full h-screen"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import Head from "next/head";
|
||||||
import useLinks from "@/hooks/useLinks";
|
import useLinks from "@/hooks/useLinks";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import ModalManagement from "@/components/ModalManagement";
|
|
||||||
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
@ -42,10 +41,12 @@ export default function PublicCollections() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [collectionOwner, setCollectionOwner] = useState({
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
id: null,
|
id: null as unknown as number,
|
||||||
name: "",
|
name: "",
|
||||||
username: "",
|
username: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
archiveAsScreenshot: undefined as unknown as boolean,
|
||||||
|
archiveAsPDF: undefined as unknown as boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState({
|
const [searchFilter, setSearchFilter] = useState({
|
||||||
|
@ -102,8 +103,6 @@ export default function PublicCollections() {
|
||||||
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalManagement />
|
|
||||||
|
|
||||||
{collection ? (
|
{collection ? (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{collection.name} | Linkwarden</title>
|
<title>{collection.name} | Linkwarden</title>
|
||||||
|
|
|
@ -26,7 +26,7 @@ const s3Client =
|
||||||
async function checkFileExistence(path) {
|
async function checkFileExistence(path) {
|
||||||
if (s3Client) {
|
if (s3Client) {
|
||||||
const bucketParams = {
|
const bucketParams = {
|
||||||
Bucket: process.env.BUCKET_NAME,
|
Bucket: process.env.SPACES_BUCKET_NAME,
|
||||||
Key: path,
|
Key: path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Ŝarĝante…
Reference in New Issue