Accepted incoming changes
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import toast from "react-hot-toast";
|
||||
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
||||
import {
|
||||
AccountSettings,
|
||||
CollectionIncludingMembersAndLinkCount,
|
||||
Member,
|
||||
} from "@/types/global";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import ProfilePhoto from "../ProfilePhoto";
|
||||
@@ -11,6 +15,8 @@ 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";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
@@ -40,6 +46,7 @@ export default function EditCollectionSharingModal({
|
||||
|
||||
await updateCollection.mutateAsync(collection, {
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
@@ -50,8 +57,6 @@ export default function EditCollectionSharingModal({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
setSubmitLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,17 +67,11 @@ export default function EditCollectionSharingModal({
|
||||
|
||||
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
|
||||
|
||||
const [memberUsername, setMemberUsername] = useState("");
|
||||
const [memberIdentifier, setMemberIdentifier] = useState("");
|
||||
|
||||
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,
|
||||
});
|
||||
const [collectionOwner, setCollectionOwner] = useState<
|
||||
Partial<AccountSettings>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOwner = async () => {
|
||||
@@ -93,19 +92,25 @@ export default function EditCollectionSharingModal({
|
||||
members: [...collection.members, newMember],
|
||||
});
|
||||
|
||||
setMemberUsername("");
|
||||
setMemberIdentifier("");
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">
|
||||
{permissions === true ? t("share_and_collaborate") : t("team")}
|
||||
{permissions === true && !isPublicRoute
|
||||
? t("share_and_collaborate")
|
||||
: t("team")}
|
||||
</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{permissions === true && (
|
||||
{permissions === true && !isPublicRoute && (
|
||||
<div>
|
||||
<p>{t("make_collection_public")}</p>
|
||||
|
||||
@@ -132,43 +137,35 @@ export default function EditCollectionSharingModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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"
|
||||
>
|
||||
{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">
|
||||
{publicCollectionURL}
|
||||
<CopyButton text={publicCollectionURL} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{permissions === true && <div className="divider my-3"></div>}
|
||||
{permissions === true && !isPublicRoute && (
|
||||
<div className="divider my-3"></div>
|
||||
)}
|
||||
|
||||
{permissions === true && (
|
||||
{permissions === true && !isPublicRoute && (
|
||||
<>
|
||||
<p>{t("members")}</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={memberUsername || ""}
|
||||
value={memberIdentifier || ""}
|
||||
className="bg-base-200"
|
||||
placeholder={t("members_username_placeholder")}
|
||||
onChange={(e) => setMemberUsername(e.target.value)}
|
||||
placeholder={t("add_member_placeholder")}
|
||||
onChange={(e) => setMemberIdentifier(e.target.value)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
addMemberToCollection(
|
||||
user.username as string,
|
||||
memberUsername || "",
|
||||
user,
|
||||
memberIdentifier.replace(/^@/, "") || "",
|
||||
collection,
|
||||
setMemberState,
|
||||
t
|
||||
@@ -179,8 +176,8 @@ export default function EditCollectionSharingModal({
|
||||
<div
|
||||
onClick={() =>
|
||||
addMemberToCollection(
|
||||
user.username as string,
|
||||
memberUsername || "",
|
||||
user,
|
||||
memberIdentifier.replace(/^@/, "") || "",
|
||||
collection,
|
||||
setMemberState,
|
||||
t
|
||||
@@ -266,7 +263,7 @@ export default function EditCollectionSharingModal({
|
||||
</div>
|
||||
|
||||
<div className={"flex items-center gap-2"}>
|
||||
{permissions === true ? (
|
||||
{permissions === true && !isPublicRoute ? (
|
||||
<div className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
@@ -427,7 +424,7 @@ export default function EditCollectionSharingModal({
|
||||
</p>
|
||||
)}
|
||||
|
||||
{permissions === true && (
|
||||
{permissions === true && !isPublicRoute && (
|
||||
<i
|
||||
className={
|
||||
"bi-x text-xl btn btn-sm btn-square btn-ghost text-neutral hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
|
||||
@@ -458,7 +455,7 @@ export default function EditCollectionSharingModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{permissions === true && (
|
||||
{permissions === true && !isPublicRoute && (
|
||||
<button
|
||||
className="btn btn-accent dark:border-violet-400 text-white w-fit ml-auto mt-3"
|
||||
onClick={submit}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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>{t("invite_user_note")}</p>
|
||||
<p className="mb-1">
|
||||
{t("invite_user_price", {
|
||||
price: 4,
|
||||
priceAnnual: 36,
|
||||
})}
|
||||
</p>
|
||||
<Link
|
||||
href="https://docs.linkwarden.app/billing/seats#how-seats-affect-billing"
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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) && !isPublicRoute && (
|
||||
<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">
|
||||
{!isPublicRoute && (
|
||||
<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)}
|
||||
onUpdateArchive={onUpdateArchive}
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from "react";
|
||||
import Modal from "../Modal";
|
||||
import Button from "../ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
submit: Function;
|
||||
};
|
||||
|
||||
export default function SurveyModal({ onClose, submit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [referer, setReferrer] = useState("rather_not_say");
|
||||
const [other, setOther] = useState("");
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl font-thin">{t("quick_survey")}</p>
|
||||
|
||||
<div className="divider mb-3 mt-1"></div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>{t("how_did_you_discover_linkwarden")}</p>
|
||||
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setReferrer(e.target.value);
|
||||
setOther("");
|
||||
}}
|
||||
className="select border border-neutral-content focus:outline-none focus:border-primary duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
|
||||
>
|
||||
<option value="rather_not_say">{t("rather_not_say")}</option>
|
||||
<option value="search_engine">{t("search_engine")}</option>
|
||||
<option value="people_recommendation">
|
||||
{t("people_recommendation")}
|
||||
</option>
|
||||
<option value="reddit">{t("reddit")}</option>
|
||||
<option value="github">{t("github")}</option>
|
||||
<option value="twitter">{t("twitter")}</option>
|
||||
<option value="mastodon">{t("mastodon")}</option>
|
||||
<option value="lemmy">{t("lemmy")}</option>
|
||||
<option value="other">{t("other")}</option>
|
||||
</select>
|
||||
|
||||
{referer === "other" && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("please_specify")}
|
||||
onChange={(e) => {
|
||||
setOther(e.target.value);
|
||||
}}
|
||||
value={other}
|
||||
className="input border border-neutral-content focus:border-primary focus:outline-none duration-100 w-full bg-base-200 rounded-[0.375rem] min-h-0 h-[2.625rem] leading-4 p-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="ml-auto mt-3"
|
||||
intent="accent"
|
||||
onClick={() => submit(referer, other)}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user