update global (texte + logo)
This commit is contained in:
75
Linkwarden/components/ModalContent/BulkDeleteLinksModal.tsx
Normal file
75
Linkwarden/components/ModalContent/BulkDeleteLinksModal.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function BulkDeleteLinksModal({ onClose }: Props) {
|
||||
const { selectedLinks, setSelectedLinks, deleteLinksById } = useLinkStore();
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading(
|
||||
`Deleting ${selectedLinks.length} Link${
|
||||
selectedLinks.length > 1 ? "s" : ""
|
||||
}...`
|
||||
);
|
||||
|
||||
const response = await deleteLinksById(
|
||||
selectedLinks.map((link) => link.id as number)
|
||||
);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
`Deleted ${selectedLinks.length} Link${
|
||||
selectedLinks.length > 1 ? "s" : ""
|
||||
}`
|
||||
);
|
||||
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin text-red-500">
|
||||
Delete {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{selectedLinks.length > 1 ? (
|
||||
<p>Are you sure you want to delete {selectedLinks.length} links?</p>
|
||||
) : (
|
||||
<p>Are you sure you want to delete this link?</p>
|
||||
)}
|
||||
|
||||
<div role="alert" className="alert alert-warning">
|
||||
<i className="bi-exclamation-triangle text-xl" />
|
||||
<span>
|
||||
<b>Warning:</b> This action is irreversible!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Hold the <kbd className="kbd kbd-sm">Shift</kbd> key while clicking
|
||||
'Delete' to bypass this confirmation in the future.
|
||||
</p>
|
||||
|
||||
<button
|
||||
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
|
||||
onClick={deleteLink}
|
||||
>
|
||||
<i className="bi-trash text-xl" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
102
Linkwarden/components/ModalContent/BulkEditLinksModal.tsx
Normal file
102
Linkwarden/components/ModalContent/BulkEditLinksModal.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React, { useState } from "react";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function BulkEditLinksModal({ onClose }: Props) {
|
||||
const { updateLinks, selectedLinks, setSelectedLinks } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const [removePreviousTags, setRemovePreviousTags] = useState(false);
|
||||
const [updatedValues, setUpdatedValues] = useState<
|
||||
Pick<LinkIncludingShortenedCollectionAndTags, "tags" | "collectionId">
|
||||
>({ tags: [] });
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
const collectionId = e?.value || null;
|
||||
console.log(updatedValues);
|
||||
setUpdatedValues((prevValues) => ({ ...prevValues, collectionId }));
|
||||
};
|
||||
|
||||
const setTags = (e: any) => {
|
||||
const tags = e.map((tag: any) => ({ name: tag.label }));
|
||||
setUpdatedValues((prevValues) => ({ ...prevValues, tags }));
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Updating...");
|
||||
|
||||
const response = await updateLinks(
|
||||
selectedLinks,
|
||||
removePreviousTags,
|
||||
updatedValues
|
||||
);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Updated!`);
|
||||
setSelectedLinks([]);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">
|
||||
Edit {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
|
||||
</p>
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
<div className="mt-5">
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p className="mb-2">Move to Collection</p>
|
||||
<CollectionSelection
|
||||
showDefaultValue={false}
|
||||
onChange={setCollection}
|
||||
creatable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2">Add Tags</p>
|
||||
<TagSelection onChange={setTags} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:ml-auto w-1/2 p-3">
|
||||
<label className="flex items-center gap-2 ">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary"
|
||||
checked={removePreviousTags}
|
||||
onChange={(e) => setRemovePreviousTags(e.target.checked)}
|
||||
/>
|
||||
Remove previous tags
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center mt-5">
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white"
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
116
Linkwarden/components/ModalContent/DeleteCollectionModal.tsx
Normal file
116
Linkwarden/components/ModalContent/DeleteCollectionModal.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import { useRouter } from "next/router";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||
};
|
||||
|
||||
export default function DeleteCollectionModal({
|
||||
onClose,
|
||||
activeCollection,
|
||||
}: Props) {
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(activeCollection);
|
||||
}, []);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { removeCollection } = useCollectionStore();
|
||||
const router = useRouter();
|
||||
const [inputField, setInputField] = useState("");
|
||||
|
||||
const permissions = usePermissions(collection.id as number);
|
||||
|
||||
const submit = async () => {
|
||||
if (permissions === true) if (collection.name !== inputField) return null;
|
||||
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
if (!collection) return null;
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
let response;
|
||||
|
||||
response = await removeCollection(collection.id as any);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Deleted.`);
|
||||
onClose();
|
||||
router.push("/collections");
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin text-red-500">
|
||||
{permissions === true ? "Delete" : "Leave"} Collection
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{permissions === true ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
To confirm, type "
|
||||
<span className="font-bold">{collection.name}</span>
|
||||
" in the box below:
|
||||
</p>
|
||||
|
||||
<TextInput
|
||||
value={inputField}
|
||||
onChange={(e) => setInputField(e.target.value)}
|
||||
placeholder={`Type "${collection.name}" Here.`}
|
||||
className="w-3/4 mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div role="alert" className="alert alert-warning">
|
||||
<i className="bi-exclamation-triangle text-xl"></i>
|
||||
<span>
|
||||
<b>Warning:</b> Deleting this collection will permanently erase
|
||||
all its contents, and it will become inaccessible to everyone,
|
||||
including members with previous access.
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p>Click the button below to leave the current collection.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
disabled={permissions === true && inputField !== collection.name}
|
||||
className={`ml-auto btn w-fit text-white flex items-center gap-2 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="bi-trash text-xl"></i>
|
||||
{permissions === true ? "Delete" : "Leave"} Collection
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
72
Linkwarden/components/ModalContent/DeleteLinkModal.tsx
Normal file
72
Linkwarden/components/ModalContent/DeleteLinkModal.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
};
|
||||
|
||||
export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
const { removeLink } = useLinkStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setLink(activeLink);
|
||||
}, []);
|
||||
|
||||
const deleteLink = async () => {
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
const response = await removeLink(link.id as number);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
|
||||
if (router.pathname.startsWith("/links/[id]")) {
|
||||
router.push("/dashboard");
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin text-red-500">Delete Link</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>Are you sure you want to delete this Link?</p>
|
||||
|
||||
<div role="alert" className="alert alert-warning">
|
||||
<i className="bi-exclamation-triangle text-xl" />
|
||||
<span>
|
||||
<b>Warning:</b> This action is irreversible!
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Hold the <kbd className="kbd kbd-sm">Shift</kbd> key while clicking
|
||||
'Delete' to bypass this confirmation in the future.
|
||||
</p>
|
||||
|
||||
<button
|
||||
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
|
||||
onClick={deleteLink}
|
||||
>
|
||||
<i className="bi-trash text-xl" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
118
Linkwarden/components/ModalContent/EditCollectionModal.tsx
Normal file
118
Linkwarden/components/ModalContent/EditCollectionModal.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||
};
|
||||
|
||||
export default function EditCollectionModal({
|
||||
onClose,
|
||||
activeCollection,
|
||||
}: Props) {
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { updateCollection } = useCollectionStore();
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
if (!collection) return null;
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Updating...");
|
||||
|
||||
let response;
|
||||
|
||||
response = await updateCollection(collection as any);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Updated!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">Edit Collection Info</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<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
|
||||
className="bg-base-200"
|
||||
value={collection.name}
|
||||
placeholder="e.g. Example Collection"
|
||||
onChange={(e) =>
|
||||
setCollection({ ...collection, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<p className="w-full mb-2">Color</p>
|
||||
<div className="color-picker flex justify-between">
|
||||
<div className="flex flex-col gap-2 items-center w-32">
|
||||
<i
|
||||
className="bi-folder-fill text-5xl drop-shadow"
|
||||
style={{ color: collection.color }}
|
||||
></i>
|
||||
<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>
|
||||
|
||||
<div className="w-full">
|
||||
<p className="mb-2">Description</p>
|
||||
<textarea
|
||||
className="w-full h-[13rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||
placeholder="The purpose of this Collection..."
|
||||
value={collection.description}
|
||||
onChange={(e) =>
|
||||
setCollection({
|
||||
...collection,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -0,0 +1,451 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import useAccountStore from "@/store/account";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import ProfilePhoto from "../ProfilePhoto";
|
||||
import addMemberToCollection from "@/lib/client/addMemberToCollection";
|
||||
import Modal from "../Modal";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||
};
|
||||
|
||||
export default function EditCollectionSharingModal({
|
||||
onClose,
|
||||
activeCollection,
|
||||
}: Props) {
|
||||
const [collection, setCollection] =
|
||||
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { updateCollection } = useCollectionStore();
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
if (!collection) return null;
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Updating...");
|
||||
|
||||
let response;
|
||||
|
||||
response = await updateCollection(collection as any);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Updated!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 as unknown as number,
|
||||
name: "",
|
||||
username: "",
|
||||
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) => {
|
||||
if (!collection) return null;
|
||||
|
||||
setCollection({
|
||||
...collection,
|
||||
members: [...collection.members, newMember],
|
||||
});
|
||||
|
||||
setMemberUsername("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">
|
||||
{permissions === true ? "Share and Collaborate" : "Team"}
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{permissions === true && (
|
||||
<div>
|
||||
<p>Make Public</p>
|
||||
|
||||
<label className="label cursor-pointer justify-start gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={collection.isPublic}
|
||||
onChange={() =>
|
||||
setCollection({
|
||||
...collection,
|
||||
isPublic: !collection.isPublic,
|
||||
})
|
||||
}
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
<span className="label-text">Make this a public collection</span>
|
||||
</label>
|
||||
|
||||
<p className="text-neutral text-sm">
|
||||
This will let <b>Anyone</b> to view this collection and it's
|
||||
users.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{collection.isPublic ? (
|
||||
<div className={permissions === true ? "pl-5" : ""}>
|
||||
<p className="mb-2">Sharable 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 bg-base-200 border-neutral-content border-solid border outline-none hover:border-primary dark:hover:border-primary duration-100 cursor-text"
|
||||
>
|
||||
{publicCollectionURL}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{permissions === true && <div className="divider my-3"></div>}
|
||||
|
||||
{permissions === true && (
|
||||
<>
|
||||
<p>Members</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={memberUsername || ""}
|
||||
className="bg-base-200"
|
||||
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="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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{collection?.members[0]?.user && (
|
||||
<>
|
||||
<div className="flex flex-col divide-y divide-neutral-content border border-neutral-content rounded-xl bg-base-200">
|
||||
<div
|
||||
className="relative p-3 bg-base-200 rounded-xl flex gap-2 justify-between"
|
||||
title={`@${collectionOwner.username} is the owner of this collection`}
|
||||
>
|
||||
<div className={"flex items-center justify-between w-full"}>
|
||||
<div className={"flex items-center"}>
|
||||
<div className={"shrink-0"}>
|
||||
<ProfilePhoto
|
||||
src={
|
||||
collectionOwner.image
|
||||
? collectionOwner.image
|
||||
: undefined
|
||||
}
|
||||
name={collectionOwner.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={"grow ml-2"}>
|
||||
<p className="text-sm font-semibold">
|
||||
{collectionOwner.name}
|
||||
</p>
|
||||
<p className="text-xs text-neutral">
|
||||
@{collectionOwner.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold">Owner</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider my-0 last:hidden h-[3px]"></div>
|
||||
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
const roleLabel =
|
||||
e.canCreate && e.canUpdate && e.canDelete
|
||||
? "Admin"
|
||||
: e.canCreate && !e.canUpdate && !e.canDelete
|
||||
? "Contributor"
|
||||
: !e.canCreate && !e.canUpdate && !e.canDelete
|
||||
? "Viewer"
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<div className="relative p-3 bg-base-200 rounded-xl flex gap-2 justify-between border-none">
|
||||
<div
|
||||
className={"flex items-center justify-between w-full"}
|
||||
>
|
||||
<div className={"flex items-center"}>
|
||||
<div className={"shrink-0"}>
|
||||
<ProfilePhoto
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
name={e.user.name}
|
||||
/>
|
||||
</div>
|
||||
<div className={"grow ml-2"}>
|
||||
<p className="text-sm font-semibold">
|
||||
{e.user.name}
|
||||
</p>
|
||||
<p className="text-xs text-neutral">
|
||||
@{e.user.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center gap-2"}>
|
||||
{permissions === true ? (
|
||||
<div className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onMouseDown={dropdownTriggerer}
|
||||
className="btn btn-sm btn-primary font-normal"
|
||||
>
|
||||
{roleLabel}
|
||||
<i className="bi-chevron-down"></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-64 mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`role-radio-${e.userId}`}
|
||||
className="radio checked:bg-primary"
|
||||
checked={
|
||||
!e.canCreate &&
|
||||
!e.canUpdate &&
|
||||
!e.canDelete
|
||||
}
|
||||
onChange={() => {
|
||||
const updatedMember = {
|
||||
...e,
|
||||
canCreate: false,
|
||||
canUpdate: false,
|
||||
canDelete: false,
|
||||
};
|
||||
const updatedMembers =
|
||||
collection.members.map((member) =>
|
||||
member.userId === e.userId
|
||||
? updatedMember
|
||||
: member
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
(
|
||||
document?.activeElement as HTMLElement
|
||||
)?.blur();
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">Viewer</p>
|
||||
<p>Read-only access</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`role-radio-${e.userId}`}
|
||||
className="radio checked:bg-primary"
|
||||
checked={
|
||||
e.canCreate &&
|
||||
!e.canUpdate &&
|
||||
!e.canDelete
|
||||
}
|
||||
onChange={() => {
|
||||
const updatedMember = {
|
||||
...e,
|
||||
canCreate: true,
|
||||
canUpdate: false,
|
||||
canDelete: false,
|
||||
};
|
||||
const updatedMembers =
|
||||
collection.members.map((member) =>
|
||||
member.userId === e.userId
|
||||
? updatedMember
|
||||
: member
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
(
|
||||
document?.activeElement as HTMLElement
|
||||
)?.blur();
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">Contributor</p>
|
||||
<p>Can view and create Links</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`role-radio-${e.userId}`}
|
||||
className="radio checked:bg-primary"
|
||||
checked={
|
||||
e.canCreate &&
|
||||
e.canUpdate &&
|
||||
e.canDelete
|
||||
}
|
||||
onChange={() => {
|
||||
const updatedMember = {
|
||||
...e,
|
||||
canCreate: true,
|
||||
canUpdate: true,
|
||||
canDelete: true,
|
||||
};
|
||||
const updatedMembers =
|
||||
collection.members.map((member) =>
|
||||
member.userId === e.userId
|
||||
? updatedMember
|
||||
: member
|
||||
);
|
||||
setCollection({
|
||||
...collection,
|
||||
members: updatedMembers,
|
||||
});
|
||||
(
|
||||
document?.activeElement as HTMLElement
|
||||
)?.blur();
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-bold">Admin</p>
|
||||
<p>Full access to all Links</p>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-neutral">
|
||||
{roleLabel}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{permissions === true && (
|
||||
<i
|
||||
className={
|
||||
"bi-x text-xl btn btn-sm btn-square btn-ghost 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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divider my-0 last:hidden h-[3px]"></div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{permissions === true && (
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto mt-3"
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
166
Linkwarden/components/ModalContent/EditLinkModal.tsx
Normal file
166
Linkwarden/components/ModalContent/EditLinkModal.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
};
|
||||
|
||||
export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
let shortendURL;
|
||||
|
||||
try {
|
||||
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
const { updateLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
|
||||
setLink({
|
||||
...link,
|
||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||
});
|
||||
};
|
||||
|
||||
const setTags = (e: any) => {
|
||||
const tagNames = e.map((e: any) => {
|
||||
return { name: e.label };
|
||||
});
|
||||
|
||||
setLink({ ...link, tags: tagNames });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLink(activeLink);
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
let response;
|
||||
|
||||
const load = toast.loading("Updating...");
|
||||
|
||||
response = await updateLink(link);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Updated!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">Edit Link</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
{link.url ? (
|
||||
<Link
|
||||
href={link.url}
|
||||
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
||||
title={link.url}
|
||||
target="_blank"
|
||||
>
|
||||
<i className="bi-link-45deg text-xl" />
|
||||
<p>{shortendURL}</p>
|
||||
</Link>
|
||||
) : undefined}
|
||||
|
||||
<div className="w-full">
|
||||
<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>
|
||||
|
||||
<div className="mt-5">
|
||||
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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",
|
||||
}
|
||||
}
|
||||
creatable={false}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<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="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>
|
||||
|
||||
<div className="flex justify-end items-center mt-5">
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white"
|
||||
onClick={submit}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
136
Linkwarden/components/ModalContent/NewCollectionModal.tsx
Normal file
136
Linkwarden/components/ModalContent/NewCollectionModal.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import toast from "react-hot-toast";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { Collection } from "@prisma/client";
|
||||
import Modal from "../Modal";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
parent?: CollectionIncludingMembersAndLinkCount;
|
||||
};
|
||||
|
||||
export default function NewCollectionModal({ onClose, parent }: Props) {
|
||||
const initial = {
|
||||
parentId: parent?.id,
|
||||
name: "",
|
||||
description: "",
|
||||
color: "#0ea5e9",
|
||||
} as Partial<Collection>;
|
||||
|
||||
const [collection, setCollection] = useState<Partial<Collection>>(initial);
|
||||
const { setAccount } = useAccountStore();
|
||||
const { data } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
setCollection(initial);
|
||||
}, []);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { addCollection } = useCollectionStore();
|
||||
|
||||
const submit = async () => {
|
||||
if (submitLoader) return;
|
||||
if (!collection) return null;
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Creating...");
|
||||
|
||||
let response = await addCollection(collection as any);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success("Created!");
|
||||
if (response.data) {
|
||||
// If the collection was created successfully, we need to get the new collection order
|
||||
setAccount(data?.user.id as number);
|
||||
onClose();
|
||||
}
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
{parent?.id ? (
|
||||
<>
|
||||
<p className="text-xl font-thin">New Sub-Collection</p>
|
||||
<p className="capitalize text-sm">For {parent.name}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xl font-thin">Create a New Collection</p>
|
||||
)}
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<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
|
||||
className="bg-base-200"
|
||||
value={collection.name}
|
||||
placeholder="e.g. Example Collection"
|
||||
onChange={(e) =>
|
||||
setCollection({ ...collection, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<p className="w-full mb-2">Color</p>
|
||||
<div className="color-picker flex justify-between">
|
||||
<div className="flex flex-col gap-2 items-center w-32">
|
||||
<i
|
||||
className={"bi-folder-fill text-5xl"}
|
||||
style={{ color: collection.color }}
|
||||
></i>
|
||||
<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>
|
||||
|
||||
<div className="w-full">
|
||||
<p className="mb-2">Description</p>
|
||||
<textarea
|
||||
className="w-full h-[13rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||
placeholder="The purpose of this Collection..."
|
||||
value={collection.description}
|
||||
onChange={(e) =>
|
||||
setCollection({
|
||||
...collection,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
|
||||
onClick={submit}
|
||||
>
|
||||
Create Collection
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
213
Linkwarden/components/ModalContent/NewLinkModal.tsx
Normal file
213
Linkwarden/components/ModalContent/NewLinkModal.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function NewLinkModal({ onClose }: Props) {
|
||||
const { data } = useSession();
|
||||
|
||||
const initial = {
|
||||
name: "",
|
||||
url: "",
|
||||
description: "",
|
||||
type: "url",
|
||||
tags: [],
|
||||
preview: "",
|
||||
image: "",
|
||||
pdf: "",
|
||||
readable: "",
|
||||
textContent: "",
|
||||
collection: {
|
||||
name: "",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
|
||||
const { addLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
|
||||
setLink({
|
||||
...link,
|
||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||
});
|
||||
};
|
||||
|
||||
const setTags = (e: any) => {
|
||||
const tagNames = e.map((e: any) => {
|
||||
return { name: e.label };
|
||||
});
|
||||
|
||||
setLink({ ...link, tags: tagNames });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.id) {
|
||||
const currentCollection = collections.find(
|
||||
(e) => e.id == Number(router.query.id)
|
||||
);
|
||||
|
||||
if (
|
||||
currentCollection &&
|
||||
currentCollection.ownerId &&
|
||||
router.asPath.startsWith("/collections/")
|
||||
)
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
id: currentCollection.id,
|
||||
name: currentCollection.name,
|
||||
ownerId: currentCollection.ownerId,
|
||||
},
|
||||
});
|
||||
} else
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
name: "Unorganized",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
let response;
|
||||
|
||||
const load = toast.loading("Creating...");
|
||||
|
||||
response = await addLink(link);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Created!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
setSubmitLoader(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">Create a New Link</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<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">Link</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,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={"mt-2"}>
|
||||
{optionsExpanded ? (
|
||||
<div className="mt-5">
|
||||
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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="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-primary border-solid border outline-none duration-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mt-5">
|
||||
<div
|
||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||
>
|
||||
<p className="font-normal">
|
||||
{optionsExpanded ? "Hide" : "More"} Options
|
||||
</p>
|
||||
<i
|
||||
className={`${
|
||||
optionsExpanded ? "bi-chevron-up" : "bi-chevron-down"
|
||||
}`}
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white"
|
||||
onClick={submit}
|
||||
>
|
||||
Create Link
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
227
Linkwarden/components/ModalContent/NewTokenModal.tsx
Normal file
227
Linkwarden/components/ModalContent/NewTokenModal.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import React, { useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import { TokenExpiry } from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import useTokenStore from "@/store/tokens";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function NewTokenModal({ onClose }: Props) {
|
||||
const [newToken, setNewToken] = useState("");
|
||||
|
||||
const { addToken } = useTokenStore();
|
||||
|
||||
const initial = {
|
||||
name: "",
|
||||
expires: 0 as TokenExpiry,
|
||||
};
|
||||
|
||||
const [token, setToken] = useState(initial as any);
|
||||
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading("Creating...");
|
||||
|
||||
const { ok, data } = await addToken(token);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (ok) {
|
||||
toast.success(`Created!`);
|
||||
setNewToken((data as any).secretKey);
|
||||
} else toast.error(data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
{newToken ? (
|
||||
<div className="flex flex-col justify-center space-y-4">
|
||||
<p className="text-xl font-thin">Access Token Created</p>
|
||||
<p>
|
||||
Your new token has been created. Please copy it and store it
|
||||
somewhere safe. You will not be able to see it again.
|
||||
</p>
|
||||
<TextInput
|
||||
spellCheck={false}
|
||||
value={newToken}
|
||||
onChange={() => {}}
|
||||
className="w-full"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newToken);
|
||||
toast.success("Copied to clipboard!");
|
||||
}}
|
||||
className="btn btn-primary w-fit mx-auto"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xl font-thin">Create an Access Token</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex sm:flex-row flex-col gap-2 items-center">
|
||||
<div className="w-full">
|
||||
<p className="mb-2">Name</p>
|
||||
|
||||
<TextInput
|
||||
value={token.name}
|
||||
onChange={(e) => setToken({ ...token, name: e.target.value })}
|
||||
placeholder="e.g. For the iOS shortcut"
|
||||
className="bg-base-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full sm:w-fit">
|
||||
<p className="mb-2">Expires in</p>
|
||||
|
||||
<div className="dropdown dropdown-bottom dropdown-end w-full">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onMouseDown={dropdownTriggerer}
|
||||
className="btn btn-outline w-full sm:w-36 flex items-center btn-sm h-10"
|
||||
>
|
||||
{token.expires === TokenExpiry.sevenDays && "7 Days"}
|
||||
{token.expires === TokenExpiry.oneMonth && "30 Days"}
|
||||
{token.expires === TokenExpiry.twoMonths && "60 Days"}
|
||||
{token.expires === TokenExpiry.threeMonths && "90 Days"}
|
||||
{token.expires === TokenExpiry.never && "No Expiration"}
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-full sm:w-52 mt-1">
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="sort-radio"
|
||||
className="radio checked:bg-primary"
|
||||
checked={token.expires === TokenExpiry.sevenDays}
|
||||
onChange={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setToken({
|
||||
...token,
|
||||
expires: TokenExpiry.sevenDays,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">7 Days</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="sort-radio"
|
||||
className="radio checked:bg-primary"
|
||||
checked={token.expires === TokenExpiry.oneMonth}
|
||||
onChange={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setToken({ ...token, expires: TokenExpiry.oneMonth });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">30 Days</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="sort-radio"
|
||||
className="radio checked:bg-primary"
|
||||
checked={token.expires === TokenExpiry.twoMonths}
|
||||
onChange={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setToken({
|
||||
...token,
|
||||
expires: TokenExpiry.twoMonths,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">60 Days</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="sort-radio"
|
||||
className="radio checked:bg-primary"
|
||||
checked={token.expires === TokenExpiry.threeMonths}
|
||||
onChange={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setToken({
|
||||
...token,
|
||||
expires: TokenExpiry.threeMonths,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">90 Days</span>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
className="label cursor-pointer flex justify-start"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="sort-radio"
|
||||
className="radio checked:bg-primary"
|
||||
checked={token.expires === TokenExpiry.never}
|
||||
onChange={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setToken({ ...token, expires: TokenExpiry.never });
|
||||
}}
|
||||
/>
|
||||
<span className="label-text">No Expiration</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center mt-5">
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white"
|
||||
onClick={submit}
|
||||
>
|
||||
Create Access Token
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
233
Linkwarden/components/ModalContent/PreservedFormatsModal.tsx
Normal file
233
Linkwarden/components/ModalContent/PreservedFormatsModal.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
ArchivedFormat,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import toast from "react-hot-toast";
|
||||
import Link from "next/link";
|
||||
import Modal from "../Modal";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import {
|
||||
pdfAvailable,
|
||||
readabilityAvailable,
|
||||
screenshotAvailable,
|
||||
} from "@/lib/shared/getArchiveValidity";
|
||||
import PreservedFormatRow from "@/components/PreserverdFormatRow";
|
||||
import useAccountStore from "@/store/account";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
};
|
||||
|
||||
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||
const session = useSession();
|
||||
const { getLink } = useLinkStore();
|
||||
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let isPublic = router.pathname.startsWith("/public") ? true : undefined;
|
||||
|
||||
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 (
|
||||
link &&
|
||||
(collectionOwner.archiveAsScreenshot === true
|
||||
? link.pdf && link.pdf !== "pending"
|
||||
: true) &&
|
||||
(collectionOwner.archiveAsPDF === true
|
||||
? link.pdf && link.pdf !== "pending"
|
||||
: true) &&
|
||||
link.readable &&
|
||||
link.readable !== "pending"
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const data = await getLink(link.id as number, isPublic);
|
||||
setLink(
|
||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
})();
|
||||
|
||||
let interval: any;
|
||||
|
||||
if (!isReady()) {
|
||||
interval = setInterval(async () => {
|
||||
const data = await getLink(link.id as number, isPublic);
|
||||
setLink(
|
||||
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
}, 5000);
|
||||
} else {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
}, [link?.image, link?.pdf, link?.readable]);
|
||||
|
||||
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) {
|
||||
const newLink = await getLink(link?.id as number);
|
||||
setLink(
|
||||
(newLink as any).response as LinkIncludingShortenedCollectionAndTags
|
||||
);
|
||||
toast.success(`Link is being archived...`);
|
||||
} else toast.error(data.response);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">Preserved Formats</p>
|
||||
|
||||
<div className="divider mb-2 mt-1"></div>
|
||||
|
||||
{isReady() &&
|
||||
(screenshotAvailable(link) ||
|
||||
pdfAvailable(link) ||
|
||||
readabilityAvailable(link)) ? (
|
||||
<p className="mb-3">
|
||||
The following formats are available for this link:
|
||||
</p>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
|
||||
<div className={`flex flex-col gap-3`}>
|
||||
{isReady() ? (
|
||||
<>
|
||||
{screenshotAvailable(link) ? (
|
||||
<PreservedFormatRow
|
||||
name={"Screenshot"}
|
||||
icon={"bi-file-earmark-image"}
|
||||
format={
|
||||
link?.image?.endsWith("png")
|
||||
? ArchivedFormat.png
|
||||
: ArchivedFormat.jpeg
|
||||
}
|
||||
activeLink={link}
|
||||
downloadable={true}
|
||||
/>
|
||||
) : undefined}
|
||||
|
||||
{pdfAvailable(link) ? (
|
||||
<PreservedFormatRow
|
||||
name={"PDF"}
|
||||
icon={"bi-file-earmark-pdf"}
|
||||
format={ArchivedFormat.pdf}
|
||||
activeLink={link}
|
||||
downloadable={true}
|
||||
/>
|
||||
) : undefined}
|
||||
|
||||
{readabilityAvailable(link) ? (
|
||||
<PreservedFormatRow
|
||||
name={"Readable"}
|
||||
icon={"bi-file-earmark-text"}
|
||||
format={ArchivedFormat.readability}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={`w-full h-full flex flex-col justify-center p-10 skeleton bg-base-200`}
|
||||
>
|
||||
<i className="bi-stack drop-shadow text-primary text-8xl mx-auto mb-5"></i>
|
||||
<p className="text-center text-2xl">
|
||||
Link preservation is in the queue
|
||||
</p>
|
||||
<p className="text-center text-lg">
|
||||
Please check back later to see the result
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex flex-col sm:flex-row gap-3 items-center justify-center ${
|
||||
isReady() ? "sm:mt " : ""
|
||||
}`}
|
||||
>
|
||||
<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-1/2 justify-center items-center text-sm`}
|
||||
>
|
||||
<p className="whitespace-nowrap">
|
||||
View latest snapshot on archive.org
|
||||
</p>
|
||||
<i className="bi-box-arrow-up-right" />
|
||||
</Link>
|
||||
{link?.collection.ownerId === session.data?.user.id ? (
|
||||
<div className={`btn 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>
|
||||
</Modal>
|
||||
);
|
||||
}
|
62
Linkwarden/components/ModalContent/RevokeTokenModal.tsx
Normal file
62
Linkwarden/components/ModalContent/RevokeTokenModal.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useLinkStore from "@/store/links";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
import { useRouter } from "next/router";
|
||||
import { AccessToken } from "@prisma/client";
|
||||
import useTokenStore from "@/store/tokens";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeToken: AccessToken;
|
||||
};
|
||||
|
||||
export default function DeleteTokenModal({ onClose, activeToken }: Props) {
|
||||
const [token, setToken] = useState<AccessToken>(activeToken);
|
||||
|
||||
const { revokeToken } = useTokenStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setToken(activeToken);
|
||||
}, []);
|
||||
|
||||
const deleteLink = async () => {
|
||||
console.log(token);
|
||||
const load = toast.loading("Deleting...");
|
||||
|
||||
const response = await revokeToken(token.id as number);
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Token Revoked.`);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin text-red-500">Revoke Token</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Are you sure you want to revoke this Access Token? Any apps or
|
||||
services using this token will no longer be able to access Linkwarden
|
||||
using it.
|
||||
</p>
|
||||
|
||||
<button
|
||||
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
|
||||
onClick={deleteLink}
|
||||
>
|
||||
<i className="bi-trash text-xl" />
|
||||
Revoke
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
246
Linkwarden/components/ModalContent/UploadFileModal.tsx
Normal file
246
Linkwarden/components/ModalContent/UploadFileModal.tsx
Normal file
@ -0,0 +1,246 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import {
|
||||
ArchivedFormat,
|
||||
LinkIncludingShortenedCollectionAndTags,
|
||||
} from "@/types/global";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../Modal";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
};
|
||||
|
||||
export default function UploadFileModal({ onClose }: Props) {
|
||||
const { data } = useSession();
|
||||
|
||||
const initial = {
|
||||
name: "",
|
||||
url: "",
|
||||
description: "",
|
||||
type: "url",
|
||||
tags: [],
|
||||
preview: "",
|
||||
image: "",
|
||||
pdf: "",
|
||||
readable: "",
|
||||
textContent: "",
|
||||
collection: {
|
||||
name: "",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
} as LinkIncludingShortenedCollectionAndTags;
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
|
||||
const [file, setFile] = useState<File>();
|
||||
|
||||
const { addLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const setCollection = (e: any) => {
|
||||
if (e?.__isNew__) e.value = null;
|
||||
|
||||
setLink({
|
||||
...link,
|
||||
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||
});
|
||||
};
|
||||
|
||||
const setTags = (e: any) => {
|
||||
const tagNames = e.map((e: any) => {
|
||||
return { name: e.label };
|
||||
});
|
||||
|
||||
setLink({ ...link, tags: tagNames });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOptionsExpanded(false);
|
||||
if (router.query.id) {
|
||||
const currentCollection = collections.find(
|
||||
(e) => e.id == Number(router.query.id)
|
||||
);
|
||||
|
||||
if (
|
||||
currentCollection &&
|
||||
currentCollection.ownerId &&
|
||||
router.asPath.startsWith("/collections/")
|
||||
)
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
id: currentCollection.id,
|
||||
name: currentCollection.name,
|
||||
ownerId: currentCollection.ownerId,
|
||||
},
|
||||
});
|
||||
} else
|
||||
setLink({
|
||||
...initial,
|
||||
collection: {
|
||||
name: "Unorganized",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const submit = async () => {
|
||||
if (!submitLoader && file) {
|
||||
let fileType: ArchivedFormat | null = null;
|
||||
let linkType: "url" | "image" | "pdf" | null = null;
|
||||
|
||||
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
|
||||
fileType = ArchivedFormat.jpeg;
|
||||
linkType = "image";
|
||||
} else if (file.type === "image/png") {
|
||||
fileType = ArchivedFormat.png;
|
||||
linkType = "image";
|
||||
} else if (file.type === "application/pdf") {
|
||||
fileType = ArchivedFormat.pdf;
|
||||
linkType = "pdf";
|
||||
}
|
||||
|
||||
if (fileType !== null && linkType !== null) {
|
||||
setSubmitLoader(true);
|
||||
|
||||
let response;
|
||||
|
||||
const load = toast.loading("Creating...");
|
||||
|
||||
response = await addLink({
|
||||
...link,
|
||||
type: linkType,
|
||||
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
|
||||
});
|
||||
|
||||
toast.dismiss(load);
|
||||
|
||||
if (response.ok) {
|
||||
const formBody = new FormData();
|
||||
file && formBody.append("file", file);
|
||||
|
||||
await fetch(
|
||||
`/api/v1/archives/${
|
||||
(response.data as LinkIncludingShortenedCollectionAndTags).id
|
||||
}?format=${fileType}`,
|
||||
{
|
||||
body: formBody,
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
toast.success(`Created!`);
|
||||
onClose();
|
||||
} else toast.error(response.data as string);
|
||||
|
||||
setSubmitLoader(false);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<div className="flex gap-2 items-start">
|
||||
<p className="text-xl font-thin">Upload File</p>
|
||||
</div>
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
<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">File</p>
|
||||
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
||||
<input
|
||||
type="file"
|
||||
accept=".pdf,.png,.jpg,.jpeg"
|
||||
className="cursor-pointer custom-file-input"
|
||||
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs font-semibold mt-2">
|
||||
PDF, PNG, JPG (Up to {process.env.NEXT_PUBLIC_MAX_FILE_SIZE || 30}
|
||||
MB)
|
||||
</p>
|
||||
</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,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{optionsExpanded ? (
|
||||
<div className="mt-5">
|
||||
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<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="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-center mt-5">
|
||||
<div
|
||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||
>
|
||||
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white"
|
||||
onClick={submit}
|
||||
>
|
||||
Create Link
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user