Revert "undo commit"

This reverts commit 9103f67db5.
This commit is contained in:
daniel31x13
2024-11-03 03:27:52 -05:00
parent e37702aa14
commit dbd096ab76
176 changed files with 2362 additions and 9401 deletions
@@ -46,7 +46,6 @@ export default function BulkEditLinksModal({ onClose }: Props) {
},
{
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -59,6 +58,8 @@ export default function BulkEditLinksModal({ onClose }: Props) {
},
}
);
setSubmitLoader(false);
}
};
@@ -44,7 +44,6 @@ export default function DeleteCollectionModal({
deleteCollection.mutateAsync(collection.id as number, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -56,6 +55,8 @@ export default function DeleteCollectionModal({
}
},
});
setSubmitLoader(false);
}
};
+10 -20
View File
@@ -3,7 +3,6 @@ import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { useDeleteUser } from "@/hooks/store/admin/users";
import { useState } from "react";
import { useSession } from "next-auth/react";
type Props = {
onClose: Function;
@@ -24,40 +23,31 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
onSuccess: () => {
onClose();
},
onSettled: (data, error) => {
setSubmitLoader(false);
},
});
setSubmitLoader(false);
}
};
const { data } = useSession();
const isAdmin = data?.user?.id === Number(process.env.NEXT_PUBLIC_ADMIN);
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin text-red-500">
{isAdmin ? t("delete_user") : t("remove_user")}
</p>
<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>
<p>{t("confirm_user_removal_desc")}</p>
{isAdmin && (
<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>
)}
<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={submit}>
<i className="bi-trash text-xl" />
{isAdmin ? t("delete_confirmation") : t("confirm")}
{t("delete_confirmation")}
</Button>
</div>
</Modal>
+33 -30
View File
@@ -1,12 +1,11 @@
import React, { useState } from "react";
import TextInput from "@/components/TextInput";
import { HexColorPicker } from "react-colorful";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useUpdateCollection } from "@/hooks/store/collections";
import toast from "react-hot-toast";
import IconPicker from "../IconPicker";
import { IconWeight } from "@phosphor-icons/react";
type Props = {
onClose: Function;
@@ -35,7 +34,6 @@ export default function EditCollectionModal({
await updateCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -46,6 +44,8 @@ export default function EditCollectionModal({
}
},
});
setSubmitLoader(false);
}
};
@@ -56,32 +56,10 @@ export default function EditCollectionModal({
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<div className="flex gap-3 items-end">
<IconPicker
color={collection.color}
setColor={(color: string) =>
setCollection({ ...collection, color })
}
weight={(collection.iconWeight || "regular") as IconWeight}
setWeight={(iconWeight: string) =>
setCollection({ ...collection, iconWeight })
}
iconName={collection.icon as string}
setIconName={(icon: string) =>
setCollection({ ...collection, icon })
}
reset={() =>
setCollection({
...collection,
color: "#0ea5e9",
icon: "",
iconWeight: "",
})
}
/>
<div className="w-full">
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col sm:flex-row gap-3">
<div className="w-full">
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col gap-3">
<TextInput
className="bg-base-200"
value={collection.name}
@@ -90,13 +68,38 @@ export default function EditCollectionModal({
setCollection({ ...collection, name: e.target.value })
}
/>
<div>
<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"
style={{ color: collection.color }}
></i>
<div
className="btn btn-ghost btn-xs"
onClick={() =>
setCollection({ ...collection, color: "#0ea5e9" })
}
>
{t("reset")}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="w-full">
<p className="mb-2">{t("description")}</p>
<textarea
className="w-full h-32 resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
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={t("collection_description_placeholder")}
value={collection.description}
onChange={(e) =>
@@ -1,11 +1,7 @@
import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput";
import toast from "react-hot-toast";
import {
AccountSettings,
CollectionIncludingMembersAndLinkCount,
Member,
} from "@/types/global";
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
import getPublicUserData from "@/lib/client/getPublicUserData";
import usePermissions from "@/hooks/usePermissions";
import ProfilePhoto from "../ProfilePhoto";
@@ -15,7 +11,6 @@ import { dropdownTriggerer } from "@/lib/client/utils";
import { useTranslation } from "next-i18next";
import { useUpdateCollection } from "@/hooks/store/collections";
import { useUser } from "@/hooks/store/user";
import CopyButton from "../CopyButton";
type Props = {
onClose: Function;
@@ -45,7 +40,6 @@ export default function EditCollectionSharingModal({
await updateCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -56,6 +50,8 @@ export default function EditCollectionSharingModal({
}
},
});
setSubmitLoader(false);
}
};
@@ -66,11 +62,17 @@ export default function EditCollectionSharingModal({
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
const [memberIdentifier, setMemberIdentifier] = useState("");
const [memberUsername, setMemberUsername] = useState("");
const [collectionOwner, setCollectionOwner] = useState<
Partial<AccountSettings>
>({});
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
archiveAsScreenshot: undefined as unknown as boolean,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
@@ -91,7 +93,7 @@ export default function EditCollectionSharingModal({
members: [...collection.members, newMember],
});
setMemberIdentifier("");
setMemberUsername("");
};
return (
@@ -130,15 +132,25 @@ export default function EditCollectionSharingModal({
</div>
)}
{collection.isPublic && (
<div>
<p className="mb-2">{t("sharable_link")}</p>
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between">
{collection.isPublic ? (
<div className={permissions === true ? "pl-5" : ""}>
<p className="mb-2">{t("sharable_link_guide")}</p>
<div
onClick={() => {
try {
navigator.clipboard
.writeText(publicCollectionURL)
.then(() => toast.success(t("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}
<CopyButton text={publicCollectionURL} />
</div>
</div>
)}
) : null}
{permissions === true && <div className="divider my-3"></div>}
@@ -148,15 +160,15 @@ export default function EditCollectionSharingModal({
<div className="flex items-center gap-2">
<TextInput
value={memberIdentifier || ""}
value={memberUsername || ""}
className="bg-base-200"
placeholder={t("add_member_placeholder")}
onChange={(e) => setMemberIdentifier(e.target.value)}
placeholder={t("members_username_placeholder")}
onChange={(e) => setMemberUsername(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" &&
addMemberToCollection(
user,
memberIdentifier.replace(/^@/, "") || "",
user.username as string,
memberUsername || "",
collection,
setMemberState,
t
@@ -167,8 +179,8 @@ export default function EditCollectionSharingModal({
<div
onClick={() =>
addMemberToCollection(
user,
memberIdentifier.replace(/^@/, "") || "",
user.username as string,
memberUsername || "",
collection,
setMemberState,
t
+154
View File
@@ -0,0 +1,154 @@
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 { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import Link from "next/link";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useUpdateLink } from "@/hooks/store/links";
import toast from "react-hot-toast";
type Props = {
onClose: Function;
activeLink: LinkIncludingShortenedCollectionAndTags;
};
export default function EditLinkModal({ onClose, activeLink }: Props) {
const { t } = useTranslation();
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
let shortenedURL;
try {
shortenedURL = new URL(link.url || "").host.toLowerCase();
} catch (error) {
console.log(error);
}
const [submitLoader, setSubmitLoader] = useState(false);
const updateLink = useUpdateLink();
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) => ({ name: e.label }));
setLink({ ...link, tags: tagNames });
};
useEffect(() => {
setLink(activeLink);
}, []);
const submit = async () => {
if (!submitLoader) {
setSubmitLoader(true);
const load = toast.loading(t("updating"));
await updateLink.mutateAsync(link, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
onClose();
toast.success(t("updated"));
}
},
});
setSubmitLoader(false);
}
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("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>{shortenedURL}</p>
</Link>
) : undefined}
<div className="w-full">
<p className="mb-2">{t("name")}</p>
<TextInput
value={link.name}
onChange={(e) => setLink({ ...link, name: e.target.value })}
placeholder={t("placeholder_example_link")}
className="bg-base-200"
/>
</div>
<div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3">
<div>
<p className="mb-2">{t("collection")}</p>
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
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">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
/>
</div>
<div className="sm:col-span-2">
<p className="mb-2">{t("description")}</p>
<textarea
value={unescapeString(link.description) as string}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
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>
</div>
</div>
<div className="flex justify-end items-center mt-5">
<button
className="btn btn-accent dark:border-violet-400 text-white"
onClick={submit}
>
{t("save_changes")}
</button>
</div>
</Modal>
);
}
-125
View File
@@ -1,125 +0,0 @@
import toast from "react-hot-toast";
import Modal from "../Modal";
import TextInput from "../TextInput";
import { FormEvent, useState } from "react";
import { useTranslation, Trans } from "next-i18next";
import { useAddUser } from "@/hooks/store/admin/users";
import Link from "next/link";
import { signIn } from "next-auth/react";
type Props = {
onClose: Function;
};
type FormData = {
username?: string;
email?: string;
invite: boolean;
};
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true";
export default function InviteModal({ onClose }: Props) {
const { t } = useTranslation();
const addUser = useAddUser();
const [form, setForm] = useState<FormData>({
username: emailEnabled ? undefined : "",
email: emailEnabled ? "" : undefined,
invite: true,
});
const [submitLoader, setSubmitLoader] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!submitLoader) {
const checkFields = () => {
if (emailEnabled) {
return form.email !== "";
} else {
return form.username !== "";
}
};
if (checkFields()) {
setSubmitLoader(true);
await addUser.mutateAsync(form, {
onSettled: () => {
setSubmitLoader(false);
},
onSuccess: async () => {
await signIn("invite", {
email: form.email,
callbackUrl: "/member-onboarding",
redirect: false,
});
onClose();
},
});
} else {
toast.error(t("fill_all_fields_error"));
}
}
}
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("invite_user")}</p>
<div className="divider mb-3 mt-1"></div>
<p className="mb-3">{t("invite_user_desc")}</p>
<form onSubmit={submit}>
{emailEnabled ? (
<div>
<TextInput
placeholder={t("placeholder_email")}
className="bg-base-200"
onChange={(e) => setForm({ ...form, email: e.target.value })}
value={form.email}
/>
</div>
) : (
<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 role="note" className="alert alert-note mt-5">
<i className="bi-exclamation-triangle text-xl" />
<span>
<p className="mb-1">{t("invite_user_note")}</p>
<Link
href=""
className="font-semibold whitespace-nowrap hover:opacity-80 duration-100"
target="_blank"
>
{t("learn_more")} <i className="bi-box-arrow-up-right"></i>
</Link>
</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("send_invitation")}
</button>
</div>
</form>
</Modal>
);
}
-183
View File
@@ -1,183 +0,0 @@
import React, { useState } from "react";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useTranslation } from "next-i18next";
import { useDeleteLink } from "@/hooks/store/links";
import Drawer from "../Drawer";
import LinkDetails from "../LinkDetails";
import Link from "next/link";
import usePermissions from "@/hooks/usePermissions";
import { useRouter } from "next/router";
import { dropdownTriggerer } from "@/lib/client/utils";
import toast from "react-hot-toast";
import clsx from "clsx";
type Props = {
onClose: Function;
onDelete: Function;
onUpdateArchive: Function;
onPin: Function;
link: LinkIncludingShortenedCollectionAndTags;
activeMode?: "view" | "edit";
};
export default function LinkModal({
onClose,
onDelete,
onUpdateArchive,
onPin,
link,
activeMode,
}: Props) {
const { t } = useTranslation();
const permissions = usePermissions(link.collection.id as number);
const router = useRouter();
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const deleteLink = useDeleteLink();
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
return (
<Drawer
toggleDrawer={onClose}
className="sm:h-screen items-center relative"
>
<div className="absolute top-3 left-0 right-0 flex justify-between px-3">
<div
className="bi-x text-xl btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
onClick={() => onClose()}
></div>
{(permissions === true || permissions?.canUpdate) && (
<div className="flex gap-1 h-8 rounded-full bg-neutral-content bg-opacity-50 text-base-content p-1 text-xs duration-100 select-none z-10">
<div
className={clsx(
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
mode === "view" && "bg-primary bg-opacity-50"
)}
onClick={() => {
setMode("view");
}}
>
View
</div>
<div
className={clsx(
"py-1 px-2 cursor-pointer duration-100 rounded-full font-semibold",
mode === "edit" && "bg-primary bg-opacity-50"
)}
onClick={() => {
setMode("edit");
}}
>
Edit
</div>
</div>
)}
<div className="flex gap-2">
<div className={`dropdown dropdown-end z-20`}>
<div
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
className="btn btn-sm btn-circle text-base-content opacity-50 hover:opacity-100 z-10"
>
<i title="More" className="bi-three-dots text-xl" />
</div>
<ul
className={`dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box`}
>
{
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
onPin();
}}
className="whitespace-nowrap"
>
{link?.pinnedBy && link.pinnedBy[0]
? t("unpin")
: t("pin_to_dashboard")}
</div>
</li>
}
{link.type === "url" &&
(permissions === true || permissions?.canUpdate) && (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
onUpdateArchive();
}}
className="whitespace-nowrap"
>
{t("refresh_preserved_formats")}
</div>
</li>
)}
{(permissions === true || permissions?.canDelete) && (
<li>
<div
role="button"
tabIndex={0}
onClick={async (e) => {
(document?.activeElement as HTMLElement)?.blur();
console.log(e.shiftKey);
if (e.shiftKey) {
const load = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
toast.success(t("deleted"));
}
},
});
onClose();
} else {
onDelete();
onClose();
}
}}
className="whitespace-nowrap"
>
{t("delete")}
</div>
</li>
)}
</ul>
</div>
{link.url && (
<Link
href={link.url}
target="_blank"
className="bi-box-arrow-up-right btn-circle text-base-content opacity-50 hover:opacity-100 btn btn-sm select-none z-10"
></Link>
)}
</div>
</div>
<div className="w-full">
<LinkDetails
activeLink={link}
className="sm:mt-0 -mt-11"
mode={mode}
setMode={(mode: "view" | "edit") => setMode(mode)}
/>
</div>
</Drawer>
);
}
+33 -30
View File
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from "react";
import TextInput from "@/components/TextInput";
import { HexColorPicker } from "react-colorful";
import { Collection } from "@prisma/client";
import Modal from "../Modal";
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
import { useTranslation } from "next-i18next";
import { useCreateCollection } from "@/hooks/store/collections";
import toast from "react-hot-toast";
import IconPicker from "../IconPicker";
import { IconWeight } from "@phosphor-icons/react";
type Props = {
onClose: Function;
@@ -43,7 +42,6 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
await createCollection.mutateAsync(collection, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -54,6 +52,8 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
}
},
});
setSubmitLoader(false);
};
return (
@@ -72,32 +72,10 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
<div className="divider mb-3 mt-1"></div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<div className="flex gap-3 items-end">
<IconPicker
color={collection.color || "#0ea5e9"}
setColor={(color: string) =>
setCollection({ ...collection, color })
}
weight={(collection.iconWeight || "regular") as IconWeight}
setWeight={(iconWeight: string) =>
setCollection({ ...collection, iconWeight })
}
iconName={collection.icon as string}
setIconName={(icon: string) =>
setCollection({ ...collection, icon })
}
reset={() =>
setCollection({
...collection,
color: "#0ea5e9",
icon: "",
iconWeight: "",
})
}
/>
<div className="w-full">
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col sm:flex-row gap-3">
<div className="w-full">
<p className="mb-2">{t("name")}</p>
<div className="flex flex-col gap-2">
<TextInput
className="bg-base-200"
value={collection.name}
@@ -106,13 +84,38 @@ export default function NewCollectionModal({ onClose, parent }: Props) {
setCollection({ ...collection, name: e.target.value })
}
/>
<div>
<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"}
style={{ color: collection.color }}
></i>
<div
className="btn btn-ghost btn-xs"
onClick={() =>
setCollection({ ...collection, color: "#0ea5e9" })
}
>
{t("reset")}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="w-full">
<p className="mb-2">{t("description")}</p>
<textarea
className="w-full h-32 resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
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={t("collection_description_placeholder")}
value={collection.description}
onChange={(e) =>
+35 -21
View File
@@ -3,13 +3,14 @@ import CollectionSelection from "@/components/InputSelect/CollectionSelection";
import TagSelection from "@/components/InputSelect/TagSelection";
import TextInput from "@/components/TextInput";
import unescapeString from "@/lib/client/unescapeString";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useAddLink } from "@/hooks/store/links";
import toast from "react-hot-toast";
import { PostLinkSchemaType } from "@/lib/shared/schemaValidation";
type Props = {
onClose: Function;
@@ -17,19 +18,27 @@ type Props = {
export default function NewLinkModal({ onClose }: Props) {
const { t } = useTranslation();
const { data } = useSession();
const initial = {
name: "",
url: "",
description: "",
type: "url",
tags: [],
preview: "",
image: "",
pdf: "",
readable: "",
monolith: "",
textContent: "",
collection: {
id: undefined,
name: "",
ownerId: data?.user.id as number,
},
} as PostLinkSchemaType;
} as LinkIncludingShortenedCollectionAndTags;
const [link, setLink] = useState<PostLinkSchemaType>(initial);
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const addLink = useAddLink();
@@ -39,10 +48,10 @@ export default function NewLinkModal({ onClose }: Props) {
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = undefined;
if (e?.__isNew__) e.value = null;
setLink({
...link,
collection: { id: e?.value, name: e?.label },
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
});
};
@@ -52,23 +61,27 @@ export default function NewLinkModal({ onClose }: Props) {
};
useEffect(() => {
if (router.pathname.startsWith("/collections/") && router.query.id) {
if (router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (currentCollection && currentCollection.ownerId)
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" },
collection: { name: "Unorganized", ownerId: data?.user.id as number },
});
}, []);
@@ -80,17 +93,18 @@ export default function NewLinkModal({ onClose }: Props) {
await addLink.mutateAsync(link, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
toast.error(t(error.message));
toast.error(error.message);
} else {
onClose();
toast.success(t("link_created"));
}
},
});
setSubmitLoader(false);
}
};
@@ -110,19 +124,19 @@ export default function NewLinkModal({ onClose }: Props) {
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p>
{link.collection?.name && (
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
defaultValue={{
value: link.collection?.id,
label: link.collection?.name || "Unorganized",
label: link.collection.name,
value: link.collection.id,
}}
/>
)}
) : null}
</div>
</div>
<div className={"mt-2"}>
{optionsExpanded && (
{optionsExpanded ? (
<div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3">
<div>
@@ -138,7 +152,7 @@ export default function NewLinkModal({ onClose }: Props) {
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags?.map((e) => ({
defaultValue={link.tags.map((e) => ({
label: e.name,
value: e.id,
}))}
@@ -147,17 +161,17 @@ export default function NewLinkModal({ onClose }: Props) {
<div className="sm:col-span-2">
<p className="mb-2">{t("description")}</p>
<textarea
value={unescapeString(link.description || "") || ""}
value={unescapeString(link.description) as string}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
placeholder={t("link_description_placeholder")}
className="resize-none w-full h-32 rounded-md p-2 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
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
+17 -10
View File
@@ -7,7 +7,6 @@ import { dropdownTriggerer } from "@/lib/client/utils";
import Button from "../ui/Button";
import { useTranslation } from "next-i18next";
import { useAddToken } from "@/hooks/store/tokens";
import CopyButton from "../CopyButton";
type Props = {
onClose: Function;
@@ -34,7 +33,6 @@ export default function NewTokenModal({ onClose }: Props) {
await addToken.mutateAsync(token, {
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -44,6 +42,8 @@ export default function NewTokenModal({ onClose }: Props) {
}
},
});
setSubmitLoader(false);
}
};
@@ -68,14 +68,21 @@ export default function NewTokenModal({ onClose }: Props) {
<div className="flex flex-col justify-center space-y-4">
<p className="text-xl font-thin">{t("access_token_created")}</p>
<p>{t("token_creation_notice")}</p>
<div className="relative">
<div className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border flex items-center gap-2 justify-between pr-14">
{newToken}
<div className="absolute right-0 px-2 border-neutral-content border-solid border-r bg-base-200">
<CopyButton text={newToken} />
</div>
</div>
</div>
<TextInput
spellCheck={false}
value={newToken}
onChange={() => {}}
className="w-full"
/>
<button
onClick={() => {
navigator.clipboard.writeText(newToken);
toast.success(t("copied_to_clipboard"));
}}
className="btn btn-primary w-fit mx-auto"
>
{t("copy_to_clipboard")}
</button>
</div>
) : (
<>
+4 -8
View File
@@ -35,9 +35,6 @@ export default function NewUserModal({ onClose }: Props) {
event.preventDefault();
if (!submitLoader) {
if (form.password.length < 8)
return toast.error(t("password_length_error"));
const checkFields = () => {
if (emailEnabled) {
return form.name !== "" && form.email !== "" && form.password !== "";
@@ -55,10 +52,9 @@ export default function NewUserModal({ onClose }: Props) {
onSuccess: () => {
onClose();
},
onSettled: () => {
setSubmitLoader(false);
},
});
setSubmitLoader(false);
} else {
toast.error(t("fill_all_fields_error"));
}
@@ -83,7 +79,7 @@ export default function NewUserModal({ onClose }: Props) {
/>
</div>
{emailEnabled && (
{emailEnabled ? (
<div>
<p className="mb-2">{t("email")}</p>
<TextInput
@@ -93,7 +89,7 @@ export default function NewUserModal({ onClose }: Props) {
value={form.email}
/>
</div>
)}
) : undefined}
<div>
<p className="mb-2">
@@ -0,0 +1,248 @@
import React, { useEffect, useState } from "react";
import {
LinkIncludingShortenedCollectionAndTags,
ArchivedFormat,
} 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,
monolithAvailable,
screenshotAvailable,
} from "@/lib/shared/getArchiveValidity";
import PreservedFormatRow from "@/components/PreserverdFormatRow";
import getPublicUserData from "@/lib/client/getPublicUserData";
import { useTranslation } from "next-i18next";
import { BeatLoader } from "react-spinners";
import { useUser } from "@/hooks/store/user";
import { useGetLink } from "@/hooks/store/links";
type Props = {
onClose: Function;
link: LinkIncludingShortenedCollectionAndTags;
};
export default function PreservedFormatsModal({ onClose, link }: Props) {
const { t } = useTranslation();
const session = useSession();
const getLink = useGetLink();
const { data: user = {} } = useUser();
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,
archiveAsMonolith: undefined as unknown as boolean,
archiveAsPDF: undefined as unknown as boolean,
});
useEffect(() => {
const fetchOwner = async () => {
if (link.collection.ownerId !== user.id) {
const owner = await getPublicUserData(
link.collection.ownerId as number
);
setCollectionOwner(owner);
} else if (link.collection.ownerId === user.id) {
setCollectionOwner({
id: user.id as number,
name: user.name,
username: user.username as string,
image: user.image as string,
archiveAsScreenshot: user.archiveAsScreenshot as boolean,
archiveAsMonolith: user.archiveAsScreenshot as boolean,
archiveAsPDF: user.archiveAsPDF as boolean,
});
}
};
fetchOwner();
}, [link.collection.ownerId]);
const isReady = () => {
return (
link &&
(collectionOwner.archiveAsScreenshot === true
? link.pdf && link.pdf !== "pending"
: true) &&
(collectionOwner.archiveAsMonolith === true
? link.monolith && link.monolith !== "pending"
: true) &&
(collectionOwner.archiveAsPDF === true
? link.pdf && link.pdf !== "pending"
: true) &&
link.readable &&
link.readable !== "pending"
);
};
const atLeastOneFormatAvailable = () => {
return (
screenshotAvailable(link) ||
pdfAvailable(link) ||
readabilityAvailable(link) ||
monolithAvailable(link)
);
};
useEffect(() => {
(async () => {
await getLink.mutateAsync(link.id as number);
})();
let interval: any;
if (!isReady()) {
interval = setInterval(async () => {
await getLink.mutateAsync(link.id as number);
}, 5000);
} else {
if (interval) {
clearInterval(interval);
}
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [link?.monolith]);
const updateArchive = async () => {
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) {
await getLink.mutateAsync(link?.id as number);
toast.success(t("link_being_archived"));
} else toast.error(data.response);
};
return (
<Modal toggleModal={onClose}>
<p className="text-xl font-thin">{t("preserved_formats")}</p>
<div className="divider mb-2 mt-1"></div>
{screenshotAvailable(link) ||
pdfAvailable(link) ||
readabilityAvailable(link) ||
monolithAvailable(link) ? (
<p className="mb-3">{t("available_formats")}</p>
) : (
""
)}
<div className={`flex flex-col gap-3`}>
{monolithAvailable(link) ? (
<PreservedFormatRow
name={t("webpage")}
icon={"bi-filetype-html"}
format={ArchivedFormat.monolith}
link={link}
downloadable={true}
/>
) : undefined}
{screenshotAvailable(link) ? (
<PreservedFormatRow
name={t("screenshot")}
icon={"bi-file-earmark-image"}
format={
link?.image?.endsWith("png")
? ArchivedFormat.png
: ArchivedFormat.jpeg
}
link={link}
downloadable={true}
/>
) : undefined}
{pdfAvailable(link) ? (
<PreservedFormatRow
name={t("pdf")}
icon={"bi-file-earmark-pdf"}
format={ArchivedFormat.pdf}
link={link}
downloadable={true}
/>
) : undefined}
{readabilityAvailable(link) ? (
<PreservedFormatRow
name={t("readable")}
icon={"bi-file-earmark-text"}
format={ArchivedFormat.readability}
link={link}
/>
) : undefined}
{!isReady() && !atLeastOneFormatAvailable() ? (
<div className={`w-full h-full flex flex-col justify-center p-10`}>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={30}
/>
<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`}>
<BeatLoader
color="oklch(var(--p))"
className="mx-auto mb-3"
size={20}
/>
<p className="text-center">{t("there_are_more_formats")}</p>
<p className="text-center text-sm">{t("check_back_later")}</p>
</div>
) : undefined}
<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">{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}>
<div>
<p>{t("refresh_preserved_formats")}</p>
<p className="text-xs">
{t("this_deletes_current_preservations")}
</p>
</div>
</div>
)}
</div>
</div>
</Modal>
);
}
+29 -21
View File
@@ -14,7 +14,6 @@ import Modal from "../Modal";
import { useTranslation } from "next-i18next";
import { useCollections } from "@/hooks/store/collections";
import { useUploadFile } from "@/hooks/store/links";
import { PostLinkSchemaType } from "@/lib/shared/schemaValidation";
type Props = {
onClose: Function;
@@ -26,16 +25,24 @@ export default function UploadFileModal({ onClose }: Props) {
const initial = {
name: "",
url: "",
description: "",
type: "url",
tags: [],
preview: "",
image: "",
pdf: "",
readable: "",
monolith: "",
textContent: "",
collection: {
id: undefined,
name: "",
ownerId: data?.user.id as number,
},
} as PostLinkSchemaType;
} as LinkIncludingShortenedCollectionAndTags;
const [link, setLink] = useState<PostLinkSchemaType>(initial);
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(initial);
const [file, setFile] = useState<File>();
const uploadFile = useUploadFile();
@@ -45,11 +52,11 @@ export default function UploadFileModal({ onClose }: Props) {
const { data: collections = [] } = useCollections();
const setCollection = (e: any) => {
if (e?.__isNew__) e.value = undefined;
if (e?.__isNew__) e.value = null;
setLink({
...link,
collection: { id: e?.value, name: e?.label },
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
});
};
@@ -63,11 +70,10 @@ export default function UploadFileModal({ onClose }: Props) {
useEffect(() => {
setOptionsExpanded(false);
if (router.pathname.startsWith("/collections/") && router.query.id) {
if (router.query.id) {
const currentCollection = collections.find(
(e) => e.id == Number(router.query.id)
);
if (
currentCollection &&
currentCollection.ownerId &&
@@ -78,12 +84,13 @@ export default function UploadFileModal({ onClose }: Props) {
collection: {
id: currentCollection.id,
name: currentCollection.name,
ownerId: currentCollection.ownerId,
},
});
} else
setLink({
...initial,
collection: { name: "Unorganized" },
collection: { name: "Unorganized", ownerId: data?.user.id as number },
});
}, [router, collections]);
@@ -115,7 +122,6 @@ export default function UploadFileModal({ onClose }: Props) {
{ link, file },
{
onSettled: (data, error) => {
setSubmitLoader(false);
toast.dismiss(load);
if (error) {
@@ -127,6 +133,8 @@ export default function UploadFileModal({ onClose }: Props) {
},
}
);
setSubmitLoader(false);
}
};
@@ -142,7 +150,7 @@ export default function UploadFileModal({ onClose }: Props) {
<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"
accept=".pdf,.png,.jpg,.jpeg,.html"
className="cursor-pointer custom-file-input"
onChange={(e) => e.target.files && setFile(e.target.files[0])}
/>
@@ -155,18 +163,18 @@ export default function UploadFileModal({ onClose }: Props) {
</div>
<div className="sm:col-span-2 col-span-5">
<p className="mb-2">{t("collection")}</p>
{link.collection?.name && (
{link.collection.name ? (
<CollectionSelection
onChange={setCollection}
defaultValue={{
value: link.collection?.id,
label: link.collection?.name || "Unorganized",
label: link.collection.name,
value: link.collection.id,
}}
/>
)}
) : null}
</div>
</div>
{optionsExpanded && (
{optionsExpanded ? (
<div className="mt-5">
<div className="grid sm:grid-cols-2 gap-3">
<div>
@@ -182,26 +190,26 @@ export default function UploadFileModal({ onClose }: Props) {
<p className="mb-2">{t("tags")}</p>
<TagSelection
onChange={setTags}
defaultValue={link.tags?.map((e) => ({
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">{t("description")}</p>
<textarea
value={unescapeString(link.description || "") || ""}
value={unescapeString(link.description) as string}
onChange={(e) =>
setLink({ ...link, description: e.target.value })
}
placeholder={t("description_placeholder")}
className="resize-none w-full h-32 rounded-md p-2 border-neutral-content bg-base-200 focus:border-primary border-solid border outline-none duration-100"
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)}