Merge branch 'dev' of https://github.com/linkwarden/linkwarden into feat/single-file

This commit is contained in:
daniel31x13
2024-06-18 12:19:52 -04:00
161 changed files with 7449 additions and 2996 deletions
@@ -2,20 +2,19 @@ import React from "react";
import useLinkStore from "@/store/links";
import toast from "react-hot-toast";
import Modal from "../Modal";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
};
export default function BulkDeleteLinksModal({ onClose }: Props) {
const { t } = useTranslation();
const { selectedLinks, setSelectedLinks, deleteLinksById } = useLinkStore();
const deleteLink = async () => {
const load = toast.loading(
`Deleting ${selectedLinks.length} Link${
selectedLinks.length > 1 ? "s" : ""
}...`
);
const load = toast.loading(t("deleting"));
const response = await deleteLinksById(
selectedLinks.map((link) => link.id as number)
@@ -24,12 +23,7 @@ export default function BulkDeleteLinksModal({ onClose }: Props) {
toast.dismiss(load);
if (response.ok) {
toast.success(
`Deleted ${selectedLinks.length} Link${
selectedLinks.length > 1 ? "s" : ""
}`
);
toast.success(t("deleted"));
setSelectedLinks([]);
onClose();
} else toast.error(response.data as string);
@@ -38,37 +32,33 @@ export default function BulkDeleteLinksModal({ onClose }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">
Delete {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
{selectedLinks.length === 1
? t("delete_link")
: t("delete_links", { count: selectedLinks.length })}
</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>
)}
<p>
{selectedLinks.length === 1
? t("link_deletion_confirmation_message")
: t("links_deletion_confirmation_message", {
count: selectedLinks.length,
})}
</p>
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>Warning:</b> This action is irreversible!
</span>
<span>{t("warning_irreversible")}</span>
</div>
<p>
Hold the <kbd className="kbd kbd-sm">Shift</kbd> key while clicking
&apos;Delete&apos; to bypass this confirmation in the future.
</p>
<p>{t("shift_key_tip")}</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}
>
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
<i className="bi-trash text-xl" />
Delete
</button>
{t("delete")}
</Button>
</div>
</Modal>
);
+11 -8
View File
@@ -5,12 +5,14 @@ import useLinkStore from "@/store/links";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
};
export default function BulkEditLinksModal({ onClose }: Props) {
const { t } = useTranslation();
const { updateLinks, selectedLinks, setSelectedLinks } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [removePreviousTags, setRemovePreviousTags] = useState(false);
@@ -20,7 +22,6 @@ export default function BulkEditLinksModal({ onClose }: Props) {
const setCollection = (e: any) => {
const collectionId = e?.value || null;
console.log(updatedValues);
setUpdatedValues((prevValues) => ({ ...prevValues, collectionId }));
};
@@ -33,7 +34,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
if (!submitLoader) {
setSubmitLoader(true);
const load = toast.loading("Updating...");
const load = toast.loading(t("updating"));
const response = await updateLinks(
selectedLinks,
@@ -44,7 +45,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
toast.dismiss(load);
if (response.ok) {
toast.success(`Updated!`);
toast.success(t("updated"));
setSelectedLinks([]);
onClose();
} else toast.error(response.data as string);
@@ -57,13 +58,15 @@ export default function BulkEditLinksModal({ onClose }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">
Edit {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
{selectedLinks.length === 1
? t("edit_link")
: t("edit_links", { count: selectedLinks.length })}
</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>
<p className="mb-2">{t("move_to_collection")}</p>
<CollectionSelection
showDefaultValue={false}
onChange={setCollection}
@@ -72,7 +75,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
</div>
<div>
<p className="mb-2">Add Tags</p>
<p className="mb-2">{t("add_tags")}</p>
<TagSelection onChange={setTags} />
</div>
</div>
@@ -84,7 +87,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
checked={removePreviousTags}
onChange={(e) => setRemovePreviousTags(e.target.checked)}
/>
Remove previous tags
{t("remove_previous_tags")}
</label>
</div>
</div>
@@ -94,7 +97,7 @@ export default function BulkEditLinksModal({ onClose }: Props) {
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Save Changes
{t("save_changes")}
</button>
</div>
</Modal>
@@ -6,6 +6,8 @@ import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import { useRouter } from "next/router";
import usePermissions from "@/hooks/usePermissions";
import Modal from "../Modal";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -16,42 +18,40 @@ export default function DeleteCollectionModal({
onClose,
activeCollection,
}: Props) {
const { t } = useTranslation();
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
const [submitLoader, setSubmitLoader] = useState(false);
const { removeCollection } = useCollectionStore();
const router = useRouter();
const [inputField, setInputField] = useState("");
const permissions = usePermissions(collection.id as number);
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 (permissions === true && collection.name !== inputField) return;
if (!submitLoader) {
setSubmitLoader(true);
if (!collection) return null;
setSubmitLoader(true);
const load = toast.loading("Deleting...");
const load = toast.loading(t("deleting_collection"));
let response;
response = await removeCollection(collection.id as any);
let response = await removeCollection(collection.id as number);
toast.dismiss(load);
if (response.ok) {
toast.success(`Deleted.`);
toast.success(t("deleted"));
onClose();
router.push("/collections");
} else toast.error(response.data as string);
} else {
toast.error(response.data as string);
}
setSubmitLoader(false);
}
@@ -60,7 +60,7 @@ export default function DeleteCollectionModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">
{permissions === true ? "Delete" : "Leave"} Collection
{permissions === true ? t("delete_collection") : t("leave_collection")}
</p>
<div className="divider mb-3 mt-1"></div>
@@ -68,48 +68,37 @@ export default function DeleteCollectionModal({
<div className="flex flex-col gap-3">
{permissions === true ? (
<>
<div className="flex flex-col gap-3">
<p>
To confirm, type &quot;
<span className="font-bold">{collection.name}</span>
&quot; 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>
<p>{t("confirm_deletion_prompt", { name: collection.name })}</p>
<TextInput
value={inputField}
onChange={(e) => setInputField(e.target.value)}
placeholder={t("type_name_placeholder", {
name: collection.name,
})}
className="w-3/4 mx-auto"
/>
<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.
<b>{t("warning")}: </b>
{t("deletion_warning")}
</span>
</div>
</>
) : (
<p>Click the button below to leave the current collection.</p>
<p>{t("leave_prompt")}</p>
)}
<button
<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}
intent="destructive"
className="ml-auto"
>
<i className="bi-trash text-xl"></i>
{permissions === true ? "Delete" : "Leave"} Collection
</button>
{permissions === true ? t("delete") : t("leave")}
</Button>
</div>
</Modal>
);
+12 -17
View File
@@ -4,6 +4,8 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useRouter } from "next/router";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -11,11 +13,10 @@ type Props = {
};
export default function DeleteLinkModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const { removeLink } = useLinkStore();
const router = useRouter();
useEffect(() => {
@@ -23,13 +24,13 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
}, []);
const deleteLink = async () => {
const load = toast.loading("Deleting...");
const load = toast.loading(t("deleting"));
const response = await removeLink(link.id as number);
toast.dismiss(load);
response.ok && toast.success(`Link Deleted.`);
response.ok && toast.success(t("deleted"));
if (router.pathname.startsWith("/links/[id]")) {
router.push("/dashboard");
@@ -40,32 +41,26 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">Delete Link</p>
<p className="text-xl font-thin text-red-500">{t("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>
<p>{t("link_deletion_confirmation_message")}</p>
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>Warning:</b> This action is irreversible!
<b>{t("warning")}:</b> {t("irreversible_warning")}
</span>
</div>
<p>
Hold the <kbd className="kbd kbd-sm">Shift</kbd> key while clicking
&apos;Delete&apos; to bypass this confirmation in the future.
</p>
<p>{t("shift_key_tip")}</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}
>
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
<i className="bi-trash text-xl" />
Delete
</button>
{t("delete")}
</Button>
</div>
</Modal>
);
@@ -0,0 +1,51 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import useUserStore from "@/store/admin/users";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
userId: number;
};
export default function DeleteUserModal({ onClose, userId }: Props) {
const { t } = useTranslation();
const { removeUser } = useUserStore();
const deleteUser = async () => {
const load = toast.loading(t("deleting_user"));
const response = await removeUser(userId);
toast.dismiss(load);
response.ok && toast.success(t("user_deleted"));
onClose();
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">{t("delete_user")}</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-3">
<p>{t("confirm_user_deletion")}</p>
<div role="alert" className="alert alert-warning">
<i className="bi-exclamation-triangle text-xl" />
<span>
<b>{t("warning")}:</b> {t("irreversible_action_warning")}
</span>
</div>
<Button className="ml-auto" intent="destructive" onClick={deleteUser}>
<i className="bi-trash text-xl" />
{t("delete_confirmation")}
</Button>
</div>
</Modal>
);
}
+22 -23
View File
@@ -5,6 +5,7 @@ import toast from "react-hot-toast";
import { HexColorPicker } from "react-colorful";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -15,6 +16,7 @@ export default function EditCollectionModal({
onClose,
activeCollection,
}: Props) {
const { t } = useTranslation();
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
@@ -28,16 +30,14 @@ export default function EditCollectionModal({
setSubmitLoader(true);
const load = toast.loading("Updating...");
const load = toast.loading(t("updating_collection"));
let response;
response = await updateCollection(collection as any);
let response = await updateCollection(collection as any);
toast.dismiss(load);
if (response.ok) {
toast.success(`Updated!`);
toast.success(t("updated"));
onClose();
} else toast.error(response.data as string);
@@ -47,29 +47,35 @@ export default function EditCollectionModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">Edit Collection Info</p>
<p className="text-xl font-thin">{t("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>
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col gap-3">
<TextInput
className="bg-base-200"
value={collection.name}
placeholder="e.g. Example Collection"
placeholder={t("collection_name_placeholder")}
onChange={(e) =>
setCollection({ ...collection, name: e.target.value })
}
/>
<div>
<p className="w-full mb-2">Color</p>
<div className="color-picker flex justify-between">
<p className="w-full mb-2">{t("color")}</p>
<div className="color-picker flex justify-between items-center">
<HexColorPicker
color={collection.color}
onChange={(color) =>
setCollection({ ...collection, color })
}
/>
<div className="flex flex-col gap-2 items-center w-32">
<i
className="bi-folder-fill text-5xl drop-shadow"
className="bi-folder-fill text-5xl"
style={{ color: collection.color }}
></i>
<div
@@ -78,29 +84,22 @@ export default function EditCollectionModal({
setCollection({ ...collection, color: "#0ea5e9" })
}
>
Reset
{t("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>
<p className="mb-2">{t("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..."
placeholder={t("collection_description_placeholder")}
value={collection.description}
onChange={(e) =>
setCollection({
...collection,
description: e.target.value,
})
setCollection({ ...collection, description: e.target.value })
}
/>
</div>
@@ -110,7 +109,7 @@ export default function EditCollectionModal({
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
onClick={submit}
>
Save Changes
{t("save_changes")}
</button>
</div>
</Modal>
@@ -10,6 +10,7 @@ import ProfilePhoto from "../ProfilePhoto";
import addMemberToCollection from "@/lib/client/addMemberToCollection";
import Modal from "../Modal";
import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -20,6 +21,8 @@ export default function EditCollectionSharingModal({
onClose,
activeCollection,
}: Props) {
const { t } = useTranslation();
const [collection, setCollection] =
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
@@ -33,7 +36,7 @@ export default function EditCollectionSharingModal({
setSubmitLoader(true);
const load = toast.loading("Updating...");
const load = toast.loading(t("updating"));
let response;
@@ -42,7 +45,7 @@ export default function EditCollectionSharingModal({
toast.dismiss(load);
if (response.ok) {
toast.success(`Updated!`);
toast.success(t("updated"));
onClose();
} else toast.error(response.data as string);
@@ -94,7 +97,7 @@ export default function EditCollectionSharingModal({
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">
{permissions === true ? "Share and Collaborate" : "Team"}
{permissions === true ? t("share_and_collaborate") : t("team")}
</p>
<div className="divider mb-3 mt-1"></div>
@@ -102,7 +105,7 @@ export default function EditCollectionSharingModal({
<div className="flex flex-col gap-3">
{permissions === true && (
<div>
<p>Make Public</p>
<p>{t("make_collection_public")}</p>
<label className="label cursor-pointer justify-start gap-2">
<input
@@ -116,25 +119,26 @@ export default function EditCollectionSharingModal({
}
className="checkbox checkbox-primary"
/>
<span className="label-text">Make this a public collection</span>
<span className="label-text">
{t("make_collection_public_checkbox")}
</span>
</label>
<p className="text-neutral text-sm">
This will let <b>Anyone</b> to view this collection and it&apos;s
users.
{t("make_collection_public_desc")}
</p>
</div>
)}
{collection.isPublic ? (
<div className={permissions === true ? "pl-5" : ""}>
<p className="mb-2">Sharable Link (Click to copy)</p>
<p className="mb-2">{t("sharable_link_guide")}</p>
<div
onClick={() => {
try {
navigator.clipboard
.writeText(publicCollectionURL)
.then(() => toast.success("Copied!"));
.then(() => toast.success(t("copied")));
} catch (err) {
console.log(err);
}
@@ -150,13 +154,13 @@ export default function EditCollectionSharingModal({
{permissions === true && (
<>
<p>Members</p>
<p>{t("members")}</p>
<div className="flex items-center gap-2">
<TextInput
value={memberUsername || ""}
className="bg-base-200"
placeholder="Username (without the '@')"
placeholder={t("members_username_placeholder")}
onChange={(e) => setMemberUsername(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" &&
@@ -164,7 +168,8 @@ export default function EditCollectionSharingModal({
account.username as string,
memberUsername || "",
collection,
setMemberState
setMemberState,
t
)
}
/>
@@ -175,7 +180,8 @@ export default function EditCollectionSharingModal({
account.username as string,
memberUsername || "",
collection,
setMemberState
setMemberState,
t
)
}
className="btn btn-accent dark:border-violet-400 text-white btn-square btn-sm h-10 w-10"
@@ -215,7 +221,7 @@ export default function EditCollectionSharingModal({
</div>
</div>
<div>
<p className="text-sm font-bold">Owner</p>
<p className="text-sm font-bold">{t("owner")}</p>
</div>
</div>
</div>
@@ -227,11 +233,11 @@ export default function EditCollectionSharingModal({
.map((e, i) => {
const roleLabel =
e.canCreate && e.canUpdate && e.canDelete
? "Admin"
? t("admin")
: e.canCreate && !e.canUpdate && !e.canDelete
? "Contributor"
? t("contributor")
: !e.canCreate && !e.canUpdate && !e.canDelete
? "Viewer"
? t("viewer")
: undefined;
return (
@@ -308,8 +314,10 @@ export default function EditCollectionSharingModal({
}}
/>
<div>
<p className="font-bold">Viewer</p>
<p>Read-only access</p>
<p className="font-bold">
{t("viewer")}
</p>
<p>{t("viewer_desc")}</p>
</div>
</label>
</li>
@@ -351,8 +359,10 @@ export default function EditCollectionSharingModal({
}}
/>
<div>
<p className="font-bold">Contributor</p>
<p>Can view and create Links</p>
<p className="font-bold">
{t("contributor")}
</p>
<p>{t("contributor_desc")}</p>
</div>
</label>
</li>
@@ -394,8 +404,10 @@ export default function EditCollectionSharingModal({
}}
/>
<div>
<p className="font-bold">Admin</p>
<p>Full access to all Links</p>
<p className="font-bold">
{t("admin")}
</p>
<p>{t("admin_desc")}</p>
</div>
</label>
</li>
@@ -412,7 +424,7 @@ export default function EditCollectionSharingModal({
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"
title={t("remove_member")}
onClick={() => {
const updatedMembers =
collection.members.filter((member) => {
@@ -443,7 +455,7 @@ export default function EditCollectionSharingModal({
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto mt-3"
onClick={submit}
>
Save Changes
{t("save_changes")}
</button>
)}
</div>
+26 -43
View File
@@ -8,6 +8,7 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -15,13 +16,13 @@ type Props = {
};
export default function EditLinkModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
let shortendURL;
let shortenedURL;
try {
shortendURL = new URL(link.url || "").host.toLowerCase();
shortenedURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
@@ -31,7 +32,6 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = null;
setLink({
...link,
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
@@ -39,10 +39,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
};
const setTags = (e: any) => {
const tagNames = e.map((e: any) => {
return { name: e.label };
});
const tagNames = e.map((e: any) => ({ name: e.label }));
setLink({ ...link, tags: tagNames });
};
@@ -53,29 +50,25 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
const submit = async () => {
if (!submitLoader) {
setSubmitLoader(true);
let response;
const load = toast.loading("Updating...");
response = await updateLink(link);
const load = toast.loading(t("updating"));
let response = await updateLink(link);
toast.dismiss(load);
if (response.ok) {
toast.success(`Updated!`);
toast.success(t("updated"));
onClose();
} else toast.error(response.data as string);
} else {
toast.error(response.data as string);
}
setSubmitLoader(false);
return response;
}
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">Edit Link</p>
<p className="text-xl font-thin">{t("edit_link")}</p>
<div className="divider mb-3 mt-1"></div>
@@ -87,42 +80,31 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
target="_blank"
>
<i className="bi-link-45deg text-xl" />
<p>{shortendURL}</p>
<p>{shortenedURL}</p>
</Link>
) : undefined}
<div className="w-full">
<p className="mb-2">Name</p>
<p className="mb-2">{t("name")}</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder="e.g. Example Link"
placeholder={t("placeholder_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>
<p className="mb-2">{t("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",
}
? { value: link.collection.id, label: link.collection.name }
: { value: null as unknown as number, label: "Unorganized" }
}
creatable={false}
/>
@@ -130,23 +112,24 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
</div>
<div>
<p className="mb-2">Tags</p>
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => {
return { label: e.name, value: e.id };
})}
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">Description</p>
<p className="mb-2">{t("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."
placeholder={t("link_description_placeholder")}
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>
@@ -158,7 +141,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Save Changes
{t("save_changes")}
</button>
</div>
</Modal>
@@ -0,0 +1,74 @@
import React, { useState } from "react";
import TextInput from "@/components/TextInput";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
onSubmit: Function;
oldEmail: string;
newEmail: string;
};
export default function EmailChangeVerificationModal({
onClose,
onSubmit,
oldEmail,
newEmail,
}: Props) {
const { t } = useTranslation();
const [password, setPassword] = useState("");
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("confirm_password")}</p>
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-5">
<p>
{t("password_change_warning")}
{process.env.NEXT_PUBLIC_STRIPE === "true" && t("stripe_update_note")}
</p>
<p>
{t("sso_will_be_removed_warning", {
service:
process.env.NEXT_PUBLIC_GOOGLE_ENABLED === "true" ? "Google" : "",
})}
</p>
<div>
<p>{t("old_email")}</p>
<p className="text-neutral">{oldEmail}</p>
</div>
<div>
<p>{t("new_email")}</p>
<p className="text-neutral">{newEmail}</p>
</div>
<div className="w-full">
<p className="mb-2">{t("password")}</p>
<TextInput
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••••"
className="bg-base-200"
type="password"
autoFocus
/>
</div>
<div className="flex justify-end items-center">
<button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={() => onSubmit(password)}
>
{t("confirm")}
</button>
</div>
</div>
</Modal>
);
}
+25 -23
View File
@@ -8,6 +8,7 @@ import Modal from "../Modal";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import useAccountStore from "@/store/account";
import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
@@ -15,6 +16,7 @@ type Props = {
};
export default function NewCollectionModal({ onClose, parent }: Props) {
const { t } = useTranslation();
const initial = {
parentId: parent?.id,
name: "",
@@ -39,15 +41,14 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
setSubmitLoader(true);
const load = toast.loading("Creating...");
const load = toast.loading(t("creating"));
let response = await addCollection(collection as any);
toast.dismiss(load);
if (response.ok) {
toast.success("Created!");
toast.success(t("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();
}
@@ -60,11 +61,13 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
<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">{t("new_sub_collection")}</p>
<p className="capitalize text-sm">
{t("for_collection", { name: parent.name })}
</p>
</>
) : (
<p className="text-xl font-thin">Create a New Collection</p>
<p className="text-xl font-thin">{t("create_new_collection")}</p>
)}
<div className="divider mb-3 mt-1"></div>
@@ -72,19 +75,25 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
<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">
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col gap-2">
<TextInput
className="bg-base-200"
value={collection.name}
placeholder="e.g. Example Collection"
placeholder={t("collection_name_placeholder")}
onChange={(e) =>
setCollection({ ...collection, name: e.target.value })
}
/>
<div>
<p className="w-full mb-2">Color</p>
<div className="color-picker flex justify-between">
<p className="w-full mb-2">{t("color")}</p>
<div className="color-picker flex justify-between items-center">
<HexColorPicker
color={collection.color}
onChange={(color) =>
setCollection({ ...collection, color })
}
/>
<div className="flex flex-col gap-2 items-center w-32">
<i
className={"bi-folder-fill text-5xl"}
@@ -96,29 +105,22 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
setCollection({ ...collection, color: "#0ea5e9" })
}
>
Reset
{t("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>
<p className="mb-2">{t("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..."
placeholder={t("collection_description_placeholder")}
value={collection.description}
onChange={(e) =>
setCollection({
...collection,
description: e.target.value,
})
setCollection({ ...collection, description: e.target.value })
}
/>
</div>
@@ -128,7 +130,7 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto"
onClick={submit}
>
Create Collection
{t("create_collection_button")}
</button>
</div>
</Modal>
+26 -55
View File
@@ -11,14 +11,15 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
};
export default function NewLinkModal({ onClose }: Props) {
const { t } = useTranslation();
const { data } = useSession();
const initial = {
name: "",
url: "",
@@ -39,18 +40,14 @@ export default function NewLinkModal({ onClose }: Props) {
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 },
@@ -58,10 +55,7 @@ export default function NewLinkModal({ onClose }: Props) {
};
const setTags = (e: any) => {
const tagNames = e.map((e: any) => {
return { name: e.label };
});
const tagNames = e.map((e: any) => ({ name: e.label }));
setLink({ ...link, tags: tagNames });
};
@@ -70,7 +64,6 @@ export default function NewLinkModal({ onClose }: Props) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
@@ -87,53 +80,42 @@ export default function NewLinkModal({ onClose }: Props) {
} else
setLink({
...initial,
collection: {
name: "Unorganized",
ownerId: data?.user.id as number,
},
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);
const load = toast.loading(t("creating_link"));
const response = await addLink(link);
toast.dismiss(load);
if (response.ok) {
toast.success(`Created!`);
toast.success(t("link_created"));
onClose();
} else toast.error(response.data as string);
} 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>
<p className="text-xl font-thin">{t("create_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>
<p className="mb-2">{t("link")}</p>
<TextInput
value={link.url || ""}
onChange={(e) => setLink({ ...link, url: e.target.value })}
placeholder="e.g. http://example.com/"
placeholder={t("link_url_placeholder")}
className="bg-base-200"
/>
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">Collection</p>
<p className="mb-2">{t("collection")}</p>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
@@ -145,40 +127,37 @@ export default function NewLinkModal({ onClose }: Props) {
) : 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>
<p className="mb-2">{t("name")}</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder="e.g. Example Link"
placeholder={t("link_name_placeholder")}
className="bg-base-200"
/>
</div>
<div>
<p className="mb-2">Tags</p>
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => {
return { label: e.name, value: e.id };
})}
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">Description</p>
<p className="mb-2">{t("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."
placeholder={t("link_description_placeholder")}
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>
@@ -186,27 +165,19 @@ export default function NewLinkModal({ onClose }: Props) {
</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>
<p>{optionsExpanded ? t("hide_options") : t("more_options")}</p>
<i className={`bi-chevron-${optionsExpanded ? "up" : "down"}`}></i>
</div>
<button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Link
{t("create_link")}
</button>
</div>
</Modal>
+39 -30
View File
@@ -5,14 +5,16 @@ import toast from "react-hot-toast";
import Modal from "../Modal";
import useTokenStore from "@/store/tokens";
import { dropdownTriggerer } from "@/lib/client/utils";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
};
export default function NewTokenModal({ onClose }: Props) {
const { t } = useTranslation();
const [newToken, setNewToken] = useState("");
const { addToken } = useTokenStore();
const initial = {
@@ -21,21 +23,19 @@ export default function NewTokenModal({ onClose }: Props) {
};
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 load = toast.loading(t("creating_token"));
const { ok, data } = await addToken(token);
toast.dismiss(load);
if (ok) {
toast.success(`Created!`);
toast.success(t("token_created"));
setNewToken((data as any).secretKey);
} else toast.error(data as string);
@@ -43,15 +43,27 @@ export default function NewTokenModal({ onClose }: Props) {
}
};
const getLabel = (expiry: TokenExpiry) => {
switch (expiry) {
case TokenExpiry.sevenDays:
return t("7_days");
case TokenExpiry.oneMonth:
return t("30_days");
case TokenExpiry.twoMonths:
return t("60_days");
case TokenExpiry.threeMonths:
return t("90_days");
case TokenExpiry.never:
return t("no_expiration");
}
};
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>
<p className="text-xl font-thin">{t("access_token_created")}</p>
<p>{t("token_creation_notice")}</p>
<TextInput
spellCheck={false}
value={newToken}
@@ -61,47 +73,44 @@ export default function NewTokenModal({ onClose }: Props) {
<button
onClick={() => {
navigator.clipboard.writeText(newToken);
toast.success("Copied to clipboard!");
toast.success(t("copied_to_clipboard"));
}}
className="btn btn-primary w-fit mx-auto"
>
Copy to Clipboard
{t("copy_to_clipboard")}
</button>
</div>
) : (
<>
<p className="text-xl font-thin">Create an Access Token</p>
<p className="text-xl font-thin">{t("create_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>
<p className="mb-2">{t("name")}</p>
<TextInput
value={token.name}
onChange={(e) => setToken({ ...token, name: e.target.value })}
placeholder="e.g. For the iOS shortcut"
placeholder={t("token_name_placeholder")}
className="bg-base-200"
/>
</div>
<div className="w-full sm:w-fit">
<p className="mb-2">Expires in</p>
<p className="mb-2">{t("expires_in")}</p>
<div className="dropdown dropdown-bottom dropdown-end w-full">
<div
<Button
tabIndex={0}
role="button"
intent="secondary"
onMouseDown={dropdownTriggerer}
className="btn btn-outline w-full sm:w-36 flex items-center btn-sm h-10"
className="whitespace-nowrap w-32"
>
{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>
{getLabel(token.expires)}
</Button>
<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
@@ -122,7 +131,7 @@ export default function NewTokenModal({ onClose }: Props) {
});
}}
/>
<span className="label-text">7 Days</span>
<span className="label-text">{t("7_days")}</span>
</label>
</li>
<li>
@@ -141,7 +150,7 @@ export default function NewTokenModal({ onClose }: Props) {
setToken({ ...token, expires: TokenExpiry.oneMonth });
}}
/>
<span className="label-text">30 Days</span>
<span className="label-text">{t("30_days")}</span>
</label>
</li>
<li>
@@ -163,7 +172,7 @@ export default function NewTokenModal({ onClose }: Props) {
});
}}
/>
<span className="label-text">60 Days</span>
<span className="label-text">{t("60_days")}</span>
</label>
</li>
<li>
@@ -185,7 +194,7 @@ export default function NewTokenModal({ onClose }: Props) {
});
}}
/>
<span className="label-text">90 Days</span>
<span className="label-text">{t("90_days")}</span>
</label>
</li>
<li>
@@ -204,7 +213,7 @@ export default function NewTokenModal({ onClose }: Props) {
setToken({ ...token, expires: TokenExpiry.never });
}}
/>
<span className="label-text">No Expiration</span>
<span className="label-text">{t("no_expiration")}</span>
</label>
</li>
</ul>
@@ -217,7 +226,7 @@ export default function NewTokenModal({ onClose }: Props) {
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Access Token
{t("create_token")}
</button>
</div>
</>
+148
View File
@@ -0,0 +1,148 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import useUserStore from "@/store/admin/users";
import TextInput from "../TextInput";
import { FormEvent, useState } from "react";
import { useTranslation, Trans } from "next-i18next";
type Props = {
onClose: Function;
};
type FormData = {
name: string;
username?: string;
email?: string;
password: string;
};
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
export default function NewUserModal({ onClose }: Props) {
const { t } = useTranslation();
const { addUser } = useUserStore();
const [form, setForm] = useState<FormData>({
name: "",
username: "",
email: emailEnabled ? "" : undefined,
password: "",
});
const [submitLoader, setSubmitLoader] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!submitLoader) {
const checkFields = () => {
if (emailEnabled) {
return form.name !== "" && form.email !== "" && form.password !== "";
} else {
return (
form.name !== "" && form.username !== "" && form.password !== ""
);
}
};
if (checkFields()) {
if (form.password.length < 8)
return toast.error(t("password_length_error"));
setSubmitLoader(true);
const load = toast.loading(t("creating_account"));
const response = await addUser(form);
toast.dismiss(load);
setSubmitLoader(false);
if (response.ok) {
toast.success(t("user_created"));
onClose();
} else {
toast.error(response.data as string);
}
} else {
toast.error(t("fill_all_fields_error"));
}
}
}
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("create_new_user")}</p>
<div className="divider mb-3 mt-1"></div>
<form onSubmit={submit}>
<div className="grid sm:grid-cols-2 gap-3">
<div>
<p className="mb-2">{t("display_name")}</p>
<TextInput
placeholder={t("placeholder_johnny")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, name: e.target.value })}
value={form.name}
/>
</div>
{emailEnabled ? (
<div>
<p className="mb-2">{t("email")}</p>
<TextInput
placeholder={t("placeholder_email")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, email: e.target.value })}
value={form.email}
/>
</div>
) : undefined}
<div>
<p className="mb-2">
{t("username")}{" "}
{emailEnabled && (
<span className="text-xs text-neutral">{t("optional")}</span>
)}
</p>
<TextInput
placeholder={t("placeholder_john")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, username: e.target.value })}
value={form.username}
/>
</div>
<div>
<p className="mb-2">{t("password")}</p>
<TextInput
placeholder="••••••••••••••"
className="bg-base-200"
onChange={(e) => setForm({ ...form, password: e.target.value })}
value={form.password}
/>
</div>
</div>
<div role="note" className="alert alert-note mt-5">
<i className="bi-exclamation-triangle text-xl" />
<span>
<Trans
i18nKey="password_change_note"
components={[<b key={0} />]}
/>
</span>
</div>
<div className="flex justify-between items-center mt-5">
<button
className="btn btn-accent dark:border-violet-400 text-white ml-auto"
type="submit"
>
{t("create_user")}
</button>
</div>
</form>
</Modal>
);
}
@@ -1,8 +1,8 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import {
ArchivedFormat,
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
} from "@/types/global";
import toast from "react-hot-toast";
import Link from "next/link";
@@ -18,6 +18,7 @@ import {
import PreservedFormatRow from "@/components/PreserverdFormatRow";
import useAccountStore from "@/store/account";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { BeatLoader } from "react-spinners";
type Props = {
@@ -26,14 +27,12 @@ type Props = {
};
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
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;
@@ -125,17 +124,16 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
clearInterval(interval);
}
};
}, [link?.image, link?.pdf, link?.readable, link?.singlefile]);
}, [link, getLink, link?.singlefile]);
const updateArchive = async () => {
const load = toast.loading("Sending request...");
const load = toast.loading(t("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) {
@@ -143,23 +141,19 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
setLink(
(newLink as any).response as LinkIncludingShortenedCollectionAndTags
);
toast.success(`Link is being archived...`);
toast.success(t("link_being_archived"));
} else toast.error(data.response);
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">Preserved Formats</p>
<p className="text-xl font-thin">{t("preserved_formats")}</p>
<div className="divider mb-2 mt-1"></div>
{screenshotAvailable(link) ||
pdfAvailable(link) ||
readabilityAvailable(link) ||
singlefileAvailable(link) ? (
<p className="mb-3">
The following formats are available for this link:
</p>
<p className="mb-3">{t("available_formats")}</p>
) : (
""
)}
@@ -167,7 +161,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
<div className={`flex flex-col gap-3`}>
{screenshotAvailable(link) ? (
<PreservedFormatRow
name={"Screenshot"}
name={t("screenshot")}
icon={"bi-file-earmark-image"}
format={
link?.image?.endsWith("png")
@@ -181,7 +175,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
{pdfAvailable(link) ? (
<PreservedFormatRow
name={"PDF"}
name={t("pdf")}
icon={"bi-file-earmark-pdf"}
format={ArchivedFormat.pdf}
activeLink={link}
@@ -189,19 +183,9 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
/>
) : undefined}
{singlefileAvailable(link) ? (
<PreservedFormatRow
name={"SingleFile (Full Copy)"}
icon={"bi-filetype-html"}
format={ArchivedFormat.singlefile}
activeLink={link}
downloadable={true}
/>
) : undefined}
{readabilityAvailable(link) ? (
<PreservedFormatRow
name={"Readable"}
name={t("readable")}
icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability}
activeLink={link}
@@ -216,12 +200,8 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
size={30}
/>
<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>
<p className="text-center text-2xl">{t("preservation_in_queue")}</p>
<p className="text-center text-lg">{t("check_back_later")}</p>
</div>
) : !isReady() && atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-5`}>
@@ -234,9 +214,7 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
<p className="text-center">
There are more preserved formats in the queue
</p>
<p className="text-center text-sm">
Please check back later to see the result
</p>
<p className="text-center text-sm">{t("check_back_later")}</p>
</div>
) : undefined}
@@ -251,23 +229,21 @@ export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
""
)}`}
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"
>
<p className="whitespace-nowrap">
View latest snapshot on archive.org
</p>
<p className="whitespace-nowrap">{t("view_latest_snapshot")}</p>
<i className="bi-box-arrow-up-right" />
</Link>
{link?.collection.ownerId === session.data?.user.id ? (
<div className={`btn btn-outline`} onClick={() => updateArchive()}>
{link?.collection.ownerId === session.data?.user.id && (
<div className="btn btn-outline" onClick={updateArchive}>
<div>
<p>Refresh Preserved Formats</p>
<p>{t("refresh_preserved_formats")}</p>
<p className="text-xs">
This deletes the current preservations
{t("this_deletes_current_preservations")}
</p>
</div>
</div>
) : undefined}
)}
</div>
</div>
</Modal>
+14 -22
View File
@@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react";
import useLinkStore from "@/store/links";
import useTokenStore from "@/store/tokens";
import toast from "react-hot-toast";
import Modal from "../Modal";
import { useRouter } from "next/router";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { AccessToken } from "@prisma/client";
import useTokenStore from "@/store/tokens";
type Props = {
onClose: Function;
@@ -12,50 +12,42 @@ type Props = {
};
export default function DeleteTokenModal({ onClose, activeToken }: Props) {
const { t } = useTranslation();
const [token, setToken] = useState<AccessToken>(activeToken);
const { revokeToken } = useTokenStore();
const [submitLoader, setSubmitLoader] = useState(false);
const router = useRouter();
useEffect(() => {
setToken(activeToken);
}, []);
}, [activeToken]);
const deleteLink = async () => {
console.log(token);
const load = toast.loading("Deleting...");
const load = toast.loading(t("deleting"));
const response = await revokeToken(token.id as number);
toast.dismiss(load);
response.ok && toast.success(`Token Revoked.`);
if (response.ok) {
toast.success(t("token_revoked"));
}
onClose();
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">Revoke Token</p>
<p className="text-xl font-thin text-red-500">{t("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>
<p>{t("revoke_confirmation")}</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}
>
<Button className="ml-auto" intent="destructive" onClick={deleteLink}>
<i className="bi-trash text-xl" />
Revoke
</button>
{t("revoke")}
</Button>
</div>
</Modal>
);
+36 -61
View File
@@ -5,20 +5,19 @@ 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 { 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";
import { useTranslation } from "next-i18next";
type Props = {
onClose: Function;
};
export default function UploadFileModal({ onClose }: Props) {
const { t } = useTranslation();
const { data } = useSession();
const initial = {
@@ -41,14 +40,11 @@ export default function UploadFileModal({ onClose }: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [file, setFile] = useState<File>();
const { addLink } = useLinkStore();
const { uploadFile } = useLinkStore();
const [submitLoader, setSubmitLoader] = useState(false);
const [optionsExpanded, setOptionsExpanded] = useState(false);
const router = useRouter();
const { collections } = useCollectionStore();
@@ -75,7 +71,6 @@ export default function UploadFileModal({ onClose }: Props) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
@@ -92,12 +87,9 @@ export default function UploadFileModal({ onClose }: Props) {
} else
setLink({
...initial,
collection: {
name: "Unorganized",
ownerId: data?.user.id as number,
},
collection: { name: "Unorganized", ownerId: data?.user.id as number },
});
}, []);
}, [router, collections]);
const submit = async () => {
if (!submitLoader && file) {
@@ -123,49 +115,32 @@ export default function UploadFileModal({ onClose }: Props) {
let response;
const load = toast.loading("Creating...");
const load = toast.loading(t("creating"));
response = await addLink({
...link,
type: linkType,
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
});
const response = await uploadFile(link, file);
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;
toast.dismiss(load);
if (response.ok) {
toast.success(t("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>
<p className="text-xl font-thin">{t("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>
<p className="mb-2">{t("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"
@@ -175,12 +150,13 @@ export default function UploadFileModal({ onClose }: Props) {
/>
</label>
<p className="text-xs font-semibold mt-2">
PDF, PNG, JPG, HTML (Up to {process.env.NEXT_PUBLIC_MAX_FILE_SIZE || 30}
MB)
{t("file_types", {
size: process.env.NEXT_PUBLIC_MAX_FILE_SIZE || 30,
})}
</p>
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">Collection</p>
<p className="mb-2">{t("collection")}</p>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
@@ -194,36 +170,34 @@ export default function UploadFileModal({ onClose }: Props) {
</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>
<p className="mb-2">{t("name")}</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder="e.g. Example Link"
placeholder={t("example_link")}
className="bg-base-200"
/>
</div>
<div>
<p className="mb-2">Tags</p>
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => {
return { label: e.name, value: e.id };
})}
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">Description</p>
<p className="mb-2">{t("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."
placeholder={t("description_placeholder")}
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>
@@ -235,14 +209,15 @@ export default function UploadFileModal({ onClose }: Props) {
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>
<p>
{optionsExpanded ? t("hide") : t("more")} {t("options")}
</p>
</div>
<button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
Create Link
{t("upload_file")}
</button>
</div>
</Modal>