@@ -40,7 +40,7 @@ export default function AccessTokens() {
|
||||
{t("new_token")}
|
||||
</button>
|
||||
|
||||
{tokens.length > 0 && (
|
||||
{tokens.length > 0 ? (
|
||||
<table className="table mt-2 overflow-x-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -67,18 +67,10 @@ export default function AccessTokens() {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{new Date(token.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
{new Date(token.createdAt || "").toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
{new Date(token.expires).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
{new Date(token.expires || "").toLocaleDateString()}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
@@ -93,12 +85,12 @@ export default function AccessTokens() {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
{newTokenModal && (
|
||||
{newTokenModal ? (
|
||||
<NewTokenModal onClose={() => setNewTokenModal(false)} />
|
||||
)}
|
||||
) : undefined}
|
||||
{revokeTokenModal && selectedToken && (
|
||||
<RevokeTokenModal
|
||||
onClose={() => {
|
||||
|
||||
+13
-35
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, ChangeEvent } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { AccountSettings } from "@/types/global";
|
||||
import { toast } from "react-hot-toast";
|
||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||
@@ -17,7 +17,6 @@ import { i18n } from "next-i18next.config";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
||||
import { z } from "zod";
|
||||
|
||||
const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
|
||||
|
||||
@@ -56,10 +55,8 @@ export default function Account() {
|
||||
if (!objectIsEmpty(account)) setUser({ ...account });
|
||||
}, [account]);
|
||||
|
||||
const handleImageUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return toast.error(t("image_upload_no_file_error"));
|
||||
|
||||
const handleImageUpload = async (e: any) => {
|
||||
const file: File = e.target.files[0];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
const allowedExtensions = ["png", "jpeg", "jpg"];
|
||||
if (allowedExtensions.includes(fileExtension as string)) {
|
||||
@@ -81,16 +78,6 @@ export default function Account() {
|
||||
};
|
||||
|
||||
const submit = async (password?: string) => {
|
||||
if (!/^[a-z0-9_-]{3,50}$/.test(user.username || "")) {
|
||||
return toast.error(t("username_invalid_guide"));
|
||||
}
|
||||
|
||||
const emailSchema = z.string().trim().email().toLowerCase();
|
||||
const emailValidation = emailSchema.safeParse(user.email || "");
|
||||
if (!emailValidation.success) {
|
||||
return toast.error(t("email_invalid"));
|
||||
}
|
||||
|
||||
setSubmitLoader(true);
|
||||
|
||||
const load = toast.loading(t("applying_settings"));
|
||||
@@ -108,7 +95,6 @@ export default function Account() {
|
||||
}
|
||||
},
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
@@ -125,20 +111,12 @@ export default function Account() {
|
||||
}
|
||||
);
|
||||
|
||||
if (user.locale !== account.locale) {
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
const importBookmarks = async (
|
||||
e: ChangeEvent<HTMLInputElement>,
|
||||
format: MigrationFormat
|
||||
) => {
|
||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
||||
setSubmitLoader(true);
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
const file: File = e.target.files[0];
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
@@ -212,17 +190,16 @@ export default function Account() {
|
||||
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{emailEnabled && (
|
||||
{emailEnabled ? (
|
||||
<div>
|
||||
<p className="mb-2">{t("email")}</p>
|
||||
<TextInput
|
||||
value={user.email || ""}
|
||||
type="email"
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
) : undefined}
|
||||
<div>
|
||||
<p className="mb-2">{t("language")}</p>
|
||||
<select
|
||||
@@ -460,8 +437,9 @@ export default function Account() {
|
||||
|
||||
<p>
|
||||
{t("delete_account_warning")}
|
||||
{process.env.NEXT_PUBLIC_STRIPE &&
|
||||
" " + t("cancel_subscription_notice")}
|
||||
{process.env.NEXT_PUBLIC_STRIPE
|
||||
? " " + t("cancel_subscription_notice")
|
||||
: undefined}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -470,14 +448,14 @@ export default function Account() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{emailChangeVerificationModal && (
|
||||
{emailChangeVerificationModal ? (
|
||||
<EmailChangeVerificationModal
|
||||
onClose={() => setEmailChangeVerificationModal(false)}
|
||||
onSubmit={submit}
|
||||
oldEmail={account.email || ""}
|
||||
newEmail={user.email || ""}
|
||||
/>
|
||||
)}
|
||||
) : undefined}
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-226
@@ -1,57 +1,17 @@
|
||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||
import { useRouter } from "next/router";
|
||||
import InviteModal from "@/components/ModalContent/InviteModal";
|
||||
import { User as U } from "@prisma/client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { useUsers } from "@/hooks/store/admin/users";
|
||||
import DeleteUserModal from "@/components/ModalContent/DeleteUserModal";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||
import clsx from "clsx";
|
||||
import { signIn } from "next-auth/react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface User extends U {
|
||||
subscriptions: {
|
||||
active: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
type UserModal = {
|
||||
isOpen: boolean;
|
||||
userId: number | null;
|
||||
};
|
||||
|
||||
export default function Billing() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: account } = useUser();
|
||||
const { data: users = [] } = useUsers();
|
||||
|
||||
useEffect(() => {
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE || account.parentSubscriptionId)
|
||||
router.push("/settings/account");
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE) router.push("/settings/profile");
|
||||
}, []);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filteredUsers, setFilteredUsers] = useState<User[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (users.length > 0) {
|
||||
setFilteredUsers(users);
|
||||
}
|
||||
}, [users]);
|
||||
|
||||
const [deleteUserModal, setDeleteUserModal] = useState<UserModal>({
|
||||
isOpen: false,
|
||||
userId: null,
|
||||
});
|
||||
|
||||
const [inviteModal, setInviteModal] = useState(false);
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">
|
||||
@@ -80,190 +40,6 @@ export default function Billing() {
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8 mt-5">
|
||||
<p className="truncate w-full pr-7 text-3xl font-thin">
|
||||
{t("manage_seats")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 mb-3 relative">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="search-box"
|
||||
className="inline-flex items-center w-fit absolute left-1 pointer-events-none rounded-md p-1 text-primary"
|
||||
>
|
||||
<i className="bi-search"></i>
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="search-box"
|
||||
type="text"
|
||||
placeholder={t("search_users")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
|
||||
if (users) {
|
||||
setFilteredUsers(
|
||||
users.filter((user: any) =>
|
||||
JSON.stringify(user)
|
||||
.toLowerCase()
|
||||
.includes(e.target.value.toLowerCase())
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-full max-w-[15rem] md:w-[15rem] md:max-w-full duration-200 outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div
|
||||
onClick={() => setInviteModal(true)}
|
||||
className="flex items-center btn btn-accent dark:border-violet-400 text-white btn-sm px-2 h-[2.15rem] relative"
|
||||
>
|
||||
<p>{t("invite_user")}</p>
|
||||
<i className="bi-plus text-2xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md shadow border-neutral-content">
|
||||
<table className="table bg-base-300 rounded-md">
|
||||
<thead>
|
||||
<tr className="sm:table-row hidden border-b-neutral-content">
|
||||
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
|
||||
<th>{t("email")}</th>
|
||||
)}
|
||||
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
|
||||
<th>{t("status")}</th>
|
||||
)}
|
||||
<th>{t("date_added")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers?.map((user, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={clsx(
|
||||
"group border-b-neutral-content duration-100 w-full relative flex flex-col sm:table-row",
|
||||
user.id !== account.id &&
|
||||
"hover:bg-neutral-content hover:bg-opacity-30"
|
||||
)}
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
|
||||
<td className="truncate max-w-full" title={user.email || ""}>
|
||||
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||
{t("email")}
|
||||
</p>
|
||||
<p>{user.email}</p>
|
||||
</td>
|
||||
)}
|
||||
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
|
||||
<td>
|
||||
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||
{t("status")}
|
||||
</p>
|
||||
{user.emailVerified ? (
|
||||
<p className="font-bold px-2 bg-green-600 text-white rounded-md w-fit">
|
||||
{t("active")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="font-bold px-2 bg-neutral-content rounded-md w-fit">
|
||||
{t("pending")}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||
{t("date_added")}
|
||||
</p>
|
||||
<p className="whitespace-nowrap">
|
||||
{new Date(user.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</td>
|
||||
{user.id !== account.id && (
|
||||
<td>
|
||||
<div
|
||||
className={`dropdown dropdown-bottom font-normal dropdown-end absolute right-[0.35rem] top-[0.35rem]`}
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onMouseDown={dropdownTriggerer}
|
||||
className="btn btn-ghost btn-sm btn-square duration-100"
|
||||
>
|
||||
<i
|
||||
className={"bi bi-three-dots text-lg text-neutral"}
|
||||
></i>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
|
||||
{!user.emailVerified ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(
|
||||
document?.activeElement as HTMLElement
|
||||
)?.blur();
|
||||
signIn("invite", {
|
||||
email: user.email,
|
||||
callbackUrl: "/member-onboarding",
|
||||
redirect: false,
|
||||
}).then(() =>
|
||||
toast.success(t("resend_invite_success"))
|
||||
);
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("resend_invite")}
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setDeleteUserModal({
|
||||
isOpen: true,
|
||||
userId: user.id,
|
||||
});
|
||||
}}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{t("remove_user")}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-sm text-center font-bold mt-3">
|
||||
{t("seats_purchased", { count: account?.subscription?.quantity })}
|
||||
</p>
|
||||
{inviteModal && <InviteModal onClose={() => setInviteModal(false)} />}
|
||||
{deleteUserModal.isOpen && deleteUserModal.userId && (
|
||||
<DeleteUserModal
|
||||
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
|
||||
userId={deleteUserModal.userId}
|
||||
/>
|
||||
)}
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import Link from "next/link";
|
||||
import Button from "@/components/ui/Button";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
export default function Delete() {
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -16,7 +15,6 @@ export default function Delete() {
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
const { data } = useSession();
|
||||
const { t } = useTranslation();
|
||||
const { data: user } = useUser();
|
||||
|
||||
const submit = async () => {
|
||||
const body = {
|
||||
@@ -85,7 +83,7 @@ export default function Delete() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{process.env.NEXT_PUBLIC_STRIPE && !user.parentSubscriptionId && (
|
||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||
<fieldset className="border rounded-md p-2 border-primary">
|
||||
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
|
||||
<b>{t("optional")}</b> <i>{t("feedback_help")}</i>
|
||||
@@ -125,7 +123,7 @@ export default function Delete() {
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
) : undefined}
|
||||
|
||||
<Button
|
||||
className="mx-auto"
|
||||
|
||||
@@ -34,7 +34,6 @@ export default function Password() {
|
||||
},
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
@@ -48,6 +47,8 @@ export default function Password() {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -80,7 +80,6 @@ export default function Appearance() {
|
||||
{ ...user },
|
||||
{
|
||||
onSettled: (data, error) => {
|
||||
setSubmitLoader(false);
|
||||
toast.dismiss(load);
|
||||
|
||||
if (error) {
|
||||
@@ -91,6 +90,8 @@ export default function Appearance() {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setSubmitLoader(false);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user