finished adding profile deletion functionality + bug fix
This commit is contained in:
parent
97d8c35d2a
commit
cb8c2d5f10
|
@ -8,6 +8,7 @@ PAGINATION_TAKE_COUNT=
|
||||||
STORAGE_FOLDER=
|
STORAGE_FOLDER=
|
||||||
AUTOSCROLL_TIMEOUT=
|
AUTOSCROLL_TIMEOUT=
|
||||||
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
||||||
|
IMPORT_SIZE_LIMIT=
|
||||||
|
|
||||||
# AWS S3 Settings
|
# AWS S3 Settings
|
||||||
SPACES_KEY=
|
SPACES_KEY=
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default function Search() {
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
router.push("/search/" + encodeURIComponent(searchQuery))
|
router.push("/search?q=" + encodeURIComponent(searchQuery))
|
||||||
}
|
}
|
||||||
autoFocus={searchBox}
|
autoFocus={searchBox}
|
||||||
className="border border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
|
className="border border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default function CenteredForm({ text, children }: Props) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
|
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
|
||||||
<div className="m-auto flex flex-col gap-2">
|
<div className="m-auto flex flex-col gap-2 w-full">
|
||||||
{theme === "dark" ? (
|
{theme === "dark" ? (
|
||||||
<Image
|
<Image
|
||||||
src="/linkwarden_dark.png"
|
src="/linkwarden_dark.png"
|
||||||
|
@ -31,7 +31,7 @@ export default function CenteredForm({ text, children }: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{text ? (
|
{text ? (
|
||||||
<p className="text-lg sm:w-[30rem] w-80 mx-auto font-semibold text-black dark:text-white px-2 text-center">
|
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold text-black dark:text-white px-2 text-center">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
|
@ -2,14 +2,8 @@ import { prisma } from "@/lib/api/db";
|
||||||
import bcrypt from "bcrypt";
|
import bcrypt from "bcrypt";
|
||||||
import removeFolder from "@/lib/api/storage/removeFolder";
|
import removeFolder from "@/lib/api/storage/removeFolder";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
import { DeleteUserBody } from "@/types/global";
|
||||||
type DeleteUserBody = {
|
import removeFile from "@/lib/api/storage/removeFile";
|
||||||
password: string;
|
|
||||||
cancellation_details?: {
|
|
||||||
comment?: string;
|
|
||||||
feedback?: Stripe.SubscriptionCancelParams.CancellationDetails.Feedback;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function deleteUserById(
|
export default async function deleteUserById(
|
||||||
userId: number,
|
userId: number,
|
||||||
|
@ -22,7 +16,7 @@ export default async function deleteUserById(
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
response: "User not found.",
|
response: "Invalid credentials.",
|
||||||
status: 404,
|
status: 404,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -32,7 +26,7 @@ export default async function deleteUserById(
|
||||||
|
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
return {
|
return {
|
||||||
response: "Invalid password.",
|
response: "Invalid credentials.",
|
||||||
status: 401, // Unauthorized
|
status: 401, // Unauthorized
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -54,7 +48,7 @@ export default async function deleteUserById(
|
||||||
where: { ownerId: userId },
|
where: { ownerId: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete collections
|
// Find collections that the user owns
|
||||||
const collections = await prisma.collection.findMany({
|
const collections = await prisma.collection.findMany({
|
||||||
where: { ownerId: userId },
|
where: { ownerId: userId },
|
||||||
});
|
});
|
||||||
|
@ -65,7 +59,7 @@ export default async function deleteUserById(
|
||||||
where: { collectionId: collection.id },
|
where: { collectionId: collection.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optionally delete archive folders associated with collections
|
// Delete archive folders associated with collections
|
||||||
removeFolder({ filePath: `archives/${collection.id}` });
|
removeFolder({ filePath: `archives/${collection.id}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,8 +68,8 @@ export default async function deleteUserById(
|
||||||
where: { ownerId: userId },
|
where: { ownerId: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Optionally delete user's avatar
|
// Delete user's avatar
|
||||||
removeFolder({ filePath: `uploads/avatar/${userId}.jpg` });
|
removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
||||||
|
|
||||||
// Finally, delete the user
|
// Finally, delete the user
|
||||||
await prisma.user.delete({
|
await prisma.user.delete({
|
||||||
|
@ -88,26 +82,30 @@ export default async function deleteUserById(
|
||||||
apiVersion: "2022-11-15",
|
apiVersion: "2022-11-15",
|
||||||
});
|
});
|
||||||
|
|
||||||
const listByEmail = await stripe.customers.list({
|
try {
|
||||||
email: user.email?.toLowerCase(),
|
const listByEmail = await stripe.customers.list({
|
||||||
expand: ["data.subscriptions"],
|
email: user.email?.toLowerCase(),
|
||||||
});
|
expand: ["data.subscriptions"],
|
||||||
|
});
|
||||||
|
|
||||||
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
if (listByEmail.data[0].subscriptions?.data[0].id) {
|
||||||
const deleted = await stripe.subscriptions.cancel(
|
const deleted = await stripe.subscriptions.cancel(
|
||||||
listByEmail.data[0].subscriptions?.data[0].id,
|
listByEmail.data[0].subscriptions?.data[0].id,
|
||||||
{
|
{
|
||||||
cancellation_details: {
|
cancellation_details: {
|
||||||
comment: body.cancellation_details?.comment,
|
comment: body.cancellation_details?.comment,
|
||||||
feedback: body.cancellation_details?.feedback,
|
feedback: body.cancellation_details?.feedback,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: deleted,
|
response: deleted,
|
||||||
status: 200,
|
status: 200,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,14 @@ import importFromHTMLFile from "@/lib/api/controllers/migration/importFromHTMLFi
|
||||||
import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden";
|
import importFromLinkwarden from "@/lib/api/controllers/migration/importFromLinkwarden";
|
||||||
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: process.env.IMPORT_SIZE_LIMIT || "2mb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,13 @@ export default function ChooseUsername() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<CenteredForm>
|
||||||
<form onSubmit={submitUsername}>
|
<form onSubmit={submitUsername}>
|
||||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||||
<p className="text-2xl text-center text-black dark:text-white font-bold">
|
<p className="text-3xl text-center text-black dark:text-white font-extralight">
|
||||||
Choose a Username (Last step)
|
Choose a Username
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||||
Username
|
Username
|
||||||
|
|
|
@ -72,7 +72,7 @@ export default function Index() {
|
||||||
style={{ color: activeCollection?.color }}
|
style={{ color: activeCollection?.color }}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
||||||
/>
|
/>
|
||||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white w-full py-1 break-words hyphens-auto">
|
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white w-full py-1 break-words hyphens-auto font-thin">
|
||||||
{activeCollection?.name}
|
{activeCollection?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default function Collections() {
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||||
/>
|
/>
|
||||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
|
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
|
||||||
All Collections
|
All Collections
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,22 +5,21 @@ import React from "react";
|
||||||
export default function EmailConfirmaion() {
|
export default function EmailConfirmaion() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<CenteredForm>
|
||||||
<div className="p-4 sm:w-[30rem] w-80 rounded-2xl shadow-md m-auto border border-sky-100 dark:border-neutral-700 bg-slate-50 text-black dark:text-white dark:bg-neutral-800">
|
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-sky-100 dark:border-neutral-700 bg-slate-50 text-black dark:text-white dark:bg-neutral-800">
|
||||||
<p className="text-center text-xl font-bold mb-2">
|
<p className="text-center text-2xl sm:text-3xl font-extralight mb-2 ">
|
||||||
Please check your Email
|
Please check your Email
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700 my-3" />
|
||||||
|
|
||||||
<p>A sign in link has been sent to your email address.</p>
|
<p>A sign in link has been sent to your email address.</p>
|
||||||
<p>You can safely close this page.</p>
|
|
||||||
|
|
||||||
<hr className="my-5 dark:border-neutral-700" />
|
<p className="mt-3">
|
||||||
|
Didn't see the email? Check your spam folder or visit the{" "}
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Didn't find the email in your inbox? Check your spam folder or
|
|
||||||
visit the{" "}
|
|
||||||
<Link href="/forgot" className="font-bold underline">
|
<Link href="/forgot" className="font-bold underline">
|
||||||
Password Recovery
|
Password Recovery
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
page to resend the sign-in link by entering your email.
|
page to resend the link.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CenteredForm>
|
</CenteredForm>
|
||||||
|
|
|
@ -124,7 +124,7 @@ export default function Dashboard() {
|
||||||
icon={faChartSimple}
|
icon={faChartSimple}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||||
/>
|
/>
|
||||||
<p className="sm:text-4xl text-3xl text-black dark:text-white">
|
<p className="sm:text-4xl text-3xl text-black dark:text-white font-thin">
|
||||||
Dashboard
|
Dashboard
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,13 +43,16 @@ export default function Forgot() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<CenteredForm>
|
||||||
<form onSubmit={sendConfirmation}>
|
<form onSubmit={sendConfirmation}>
|
||||||
<div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||||
<p className="text-2xl text-center text-black dark:text-white font-bold">
|
<p className="text-3xl text-center text-black dark:text-white font-extralight">
|
||||||
Password Recovery
|
Password Recovery
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-md text-black dark:text-white">
|
<p className="text-black dark:text-white">
|
||||||
Enter your Email so we can send you a link to recover your
|
Enter your email so we can send you a link to recover your
|
||||||
account. Make sure to change your password in the profile settings
|
account. Make sure to change your password in the profile settings
|
||||||
afterwards.
|
afterwards.
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function Links() {
|
||||||
icon={faLink}
|
icon={faLink}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||||
/>
|
/>
|
||||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
|
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
|
||||||
All Links
|
All Links
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -50,11 +50,13 @@ export default function Login() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm text="Sign in to your account">
|
<CenteredForm text="Sign in to your account">
|
||||||
<form onSubmit={loginUser}>
|
<form onSubmit={loginUser}>
|
||||||
<div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||||
<p className="text-2xl text-black dark:text-white text-center font-bold">
|
<p className="text-3xl text-black dark:text-white text-center font-extralight">
|
||||||
Enter your credentials
|
Enter your credentials
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||||
Username
|
Username
|
||||||
|
@ -70,7 +72,7 @@ export default function Login() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="w-full">
|
||||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||||
Password
|
Password
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -32,62 +32,64 @@ export default function Register() {
|
||||||
async function registerUser(event: FormEvent<HTMLFormElement>) {
|
async function registerUser(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const checkFields = () => {
|
if (!submitLoader) {
|
||||||
if (emailEnabled) {
|
const checkFields = () => {
|
||||||
return (
|
if (emailEnabled) {
|
||||||
form.name !== "" &&
|
return (
|
||||||
form.email !== "" &&
|
form.name !== "" &&
|
||||||
form.password !== "" &&
|
form.email !== "" &&
|
||||||
form.passwordConfirmation !== ""
|
form.password !== "" &&
|
||||||
);
|
form.passwordConfirmation !== ""
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
form.name !== "" &&
|
||||||
|
form.username !== "" &&
|
||||||
|
form.password !== "" &&
|
||||||
|
form.passwordConfirmation !== ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (checkFields()) {
|
||||||
|
if (form.password !== form.passwordConfirmation)
|
||||||
|
return toast.error("Passwords do not match.");
|
||||||
|
else if (form.password.length < 8)
|
||||||
|
return toast.error("Passwords must be at least 8 characters.");
|
||||||
|
const { passwordConfirmation, ...request } = form;
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Creating Account...");
|
||||||
|
|
||||||
|
const response = await fetch("/api/v1/users", {
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
setSubmitLoader(false);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
if (form.email && emailEnabled)
|
||||||
|
await signIn("email", {
|
||||||
|
email: form.email,
|
||||||
|
callbackUrl: "/",
|
||||||
|
});
|
||||||
|
else if (!emailEnabled) router.push("/login");
|
||||||
|
|
||||||
|
toast.success("User Created!");
|
||||||
|
} else {
|
||||||
|
toast.error(data.response);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
toast.error("Please fill out all the fields.");
|
||||||
form.name !== "" &&
|
|
||||||
form.username !== "" &&
|
|
||||||
form.password !== "" &&
|
|
||||||
form.passwordConfirmation !== ""
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
if (checkFields()) {
|
|
||||||
if (form.password !== form.passwordConfirmation)
|
|
||||||
return toast.error("Passwords do not match.");
|
|
||||||
else if (form.password.length < 8)
|
|
||||||
return toast.error("Passwords must be at least 8 characters.");
|
|
||||||
const { passwordConfirmation, ...request } = form;
|
|
||||||
|
|
||||||
setSubmitLoader(true);
|
|
||||||
|
|
||||||
const load = toast.loading("Creating Account...");
|
|
||||||
|
|
||||||
const response = await fetch("/api/v1/users", {
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
setSubmitLoader(false);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
if (form.email && emailEnabled)
|
|
||||||
await signIn("email", {
|
|
||||||
email: form.email,
|
|
||||||
callbackUrl: "/",
|
|
||||||
});
|
|
||||||
else if (!emailEnabled) router.push("/login");
|
|
||||||
|
|
||||||
toast.success("User Created!");
|
|
||||||
} else {
|
|
||||||
toast.error(data.response);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error("Please fill out all the fields.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,14 +97,14 @@ export default function Register() {
|
||||||
<CenteredForm
|
<CenteredForm
|
||||||
text={
|
text={
|
||||||
process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE
|
process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE
|
||||||
? `Start using our Premium Services with a ${
|
? `Unlock ${
|
||||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||||
}-day free trial!`
|
} days of Premium Service at no cost!`
|
||||||
: "Create a new account"
|
: "Create a new account"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
||||||
<div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||||
<p>
|
<p>
|
||||||
Registration is disabled for this instance, please contact the admin
|
Registration is disabled for this instance, please contact the admin
|
||||||
in case of any issues.
|
in case of any issues.
|
||||||
|
@ -110,10 +112,13 @@ export default function Register() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={registerUser}>
|
<form onSubmit={registerUser}>
|
||||||
<div className="p-4 flex flex-col gap-3 justify-between sm:w-[30rem] w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||||
<p className="text-2xl text-black dark:text-white text-center font-bold">
|
<p className="text-3xl text-black dark:text-white text-center font-extralight">
|
||||||
Enter your details
|
Enter your details
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||||
Display Name
|
Display Name
|
||||||
|
@ -223,12 +228,12 @@ export default function Register() {
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<SubmitButton
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
label="Sign Up"
|
className={`border primary-btn-gradient select-none duration-100 bg-black border-[#0071B7] hover:border-[#059bf8] rounded-lg text-center px-4 py-2 text-slate-200 hover:text-white `}
|
||||||
className="mt-2 w-full text-center"
|
>
|
||||||
loading={submitLoader}
|
<p className="text-center w-full font-bold">Sign Up</p>
|
||||||
/>
|
</button>
|
||||||
<div className="flex items-baseline gap-1 justify-center">
|
<div className="flex items-baseline gap-1 justify-center">
|
||||||
<p className="w-fit text-gray-500 dark:text-gray-400">
|
<p className="w-fit text-gray-500 dark:text-gray-400">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
|
|
|
@ -1,117 +0,0 @@
|
||||||
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
|
|
||||||
import LinkCard from "@/components/LinkCard";
|
|
||||||
import SortDropdown from "@/components/SortDropdown";
|
|
||||||
import useLinks from "@/hooks/useLinks";
|
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
|
||||||
import useLinkStore from "@/store/links";
|
|
||||||
import { Sort } from "@/types/global";
|
|
||||||
import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export default function Links() {
|
|
||||||
const { links } = useLinkStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [searchFilter, setSearchFilter] = useState({
|
|
||||||
name: true,
|
|
||||||
url: true,
|
|
||||||
description: true,
|
|
||||||
tags: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [filterDropdown, setFilterDropdown] = useState(false);
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
|
||||||
|
|
||||||
useLinks({
|
|
||||||
sort: sortBy,
|
|
||||||
searchQueryString: router.query.query as string,
|
|
||||||
searchByName: searchFilter.name,
|
|
||||||
searchByUrl: searchFilter.url,
|
|
||||||
searchByDescription: searchFilter.description,
|
|
||||||
searchByTags: searchFilter.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MainLayout>
|
|
||||||
<div className="p-5 flex flex-col gap-5 w-full">
|
|
||||||
<div className="flex gap-3 items-center justify-between">
|
|
||||||
<div className="flex gap-3 items-center mb-5">
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSearch}
|
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
|
||||||
/>
|
|
||||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
|
|
||||||
Search Results
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
onClick={() => setFilterDropdown(!filterDropdown)}
|
|
||||||
id="filter-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faFilter}
|
|
||||||
id="filter-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filterDropdown ? (
|
|
||||||
<FilterSearchDropdown
|
|
||||||
setFilterDropdown={setFilterDropdown}
|
|
||||||
searchFilter={searchFilter}
|
|
||||||
setSearchFilter={setSearchFilter}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{links[0] ? (
|
|
||||||
<div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
|
|
||||||
{links.map((e, i) => {
|
|
||||||
return <LinkCard key={i} link={e} count={i} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-black dark:text-white">
|
|
||||||
Nothing found.{" "}
|
|
||||||
<span className="font-bold text-xl" title="Shruggie">
|
|
||||||
¯\_(ツ)_/¯
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</MainLayout>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,10 +1,117 @@
|
||||||
|
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
|
||||||
|
import LinkCard from "@/components/LinkCard";
|
||||||
|
import SortDropdown from "@/components/SortDropdown";
|
||||||
|
import useLinks from "@/hooks/useLinks";
|
||||||
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { Sort } from "@/types/global";
|
||||||
|
import { faFilter, faSearch, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function Search() {
|
||||||
|
const { links } = useLinkStore();
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
const [searchFilter, setSearchFilter] = useState({
|
||||||
router.push("/links");
|
name: true,
|
||||||
}, []);
|
url: true,
|
||||||
|
description: true,
|
||||||
|
tags: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filterDropdown, setFilterDropdown] = useState(false);
|
||||||
|
const [sortDropdown, setSortDropdown] = useState(false);
|
||||||
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
|
useLinks({
|
||||||
|
sort: sortBy,
|
||||||
|
searchQueryString: decodeURIComponent(router.query.q as string),
|
||||||
|
searchByName: searchFilter.name,
|
||||||
|
searchByUrl: searchFilter.url,
|
||||||
|
searchByDescription: searchFilter.description,
|
||||||
|
searchByTags: searchFilter.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<div className="p-5 flex flex-col gap-5 w-full">
|
||||||
|
<div className="flex gap-3 items-center justify-between">
|
||||||
|
<div className="flex gap-3 items-center mb-5">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSearch}
|
||||||
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||||
|
/>
|
||||||
|
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
|
||||||
|
Search Results
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
onClick={() => setFilterDropdown(!filterDropdown)}
|
||||||
|
id="filter-dropdown"
|
||||||
|
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFilter}
|
||||||
|
id="filter-dropdown"
|
||||||
|
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filterDropdown ? (
|
||||||
|
<FilterSearchDropdown
|
||||||
|
setFilterDropdown={setFilterDropdown}
|
||||||
|
searchFilter={searchFilter}
|
||||||
|
setSearchFilter={setSearchFilter}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
onClick={() => setSortDropdown(!sortDropdown)}
|
||||||
|
id="sort-dropdown"
|
||||||
|
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSort}
|
||||||
|
id="sort-dropdown"
|
||||||
|
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortDropdown ? (
|
||||||
|
<SortDropdown
|
||||||
|
sortBy={sortBy}
|
||||||
|
setSort={setSortBy}
|
||||||
|
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{links[0] ? (
|
||||||
|
<div className="grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5">
|
||||||
|
{links.map((e, i) => {
|
||||||
|
return <LinkCard key={i} link={e} count={i} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-black dark:text-white">
|
||||||
|
Nothing found.{" "}
|
||||||
|
<span className="font-bold text-xl" title="Shruggie">
|
||||||
|
¯\_(ツ)_/¯
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,7 +269,7 @@ export default function Account() {
|
||||||
Import your data from other platforms.
|
Import your data from other platforms.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
onClick={() => setImportDropdown(!importDropdown)}
|
onClick={() => setImportDropdown(true)}
|
||||||
className="w-fit relative"
|
className="w-fit relative"
|
||||||
id="import-dropdown"
|
id="import-dropdown"
|
||||||
>
|
>
|
||||||
|
@ -387,6 +387,33 @@ export default function Account() {
|
||||||
label="Save"
|
label="Save"
|
||||||
className="mt-2 mx-auto lg:mx-0"
|
className="mt-2 mx-auto lg:mx-0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||||
|
<p className="text-red-500 dark:text-red-500 truncate w-full pr-7 text-3xl font-thin">
|
||||||
|
Delete Account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This will permanently delete ALL the Links, Collections, Tags, and
|
||||||
|
archived data you own.{" "}
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE
|
||||||
|
? "It will also cancel your subscription. "
|
||||||
|
: undefined}{" "}
|
||||||
|
You will be prompted to enter your password before the deletion
|
||||||
|
process.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/settings/delete"
|
||||||
|
className="mx-auto lg:mx-0 text-white mt-3 flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
|
||||||
|
>
|
||||||
|
<p className="text-center w-full">Delete Your Account</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import CenteredForm from "@/layouts/CenteredForm";
|
||||||
|
import { signOut, useSession } from "next-auth/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
export default function Password() {
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [comment, setComment] = useState<string>();
|
||||||
|
const [feedback, setFeedback] = useState<string>();
|
||||||
|
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
const { data } = useSession();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
const body = {
|
||||||
|
password,
|
||||||
|
cancellation_details: {
|
||||||
|
comment,
|
||||||
|
feedback,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (password == "") {
|
||||||
|
return toast.error("Please fill the required fields.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Deleting everything, please wait...");
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/users/${data?.user.id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = (await response.json()).response;
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
signOut();
|
||||||
|
} else toast.error(message);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CenteredForm>
|
||||||
|
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 dark:border-neutral-700 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||||
|
<Link
|
||||||
|
href="/settings/account"
|
||||||
|
className="absolute top-4 left-4 gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||||
|
<p className="text-red-500 dark:text-red-500 truncate w-full text-3xl text-center">
|
||||||
|
Delete Account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This will permanently delete all the Links, Collections, Tags, and
|
||||||
|
archived data you own. It will also log you out
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE
|
||||||
|
? " and cancel your subscription"
|
||||||
|
: undefined}
|
||||||
|
. This action is irreversible!
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm mb-2 text-black dark:text-white">
|
||||||
|
Confirm Your Password
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
placeholder="••••••••••••••"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? (
|
||||||
|
<fieldset className="border rounded-md p-2 border-sky-500">
|
||||||
|
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-sky-500">
|
||||||
|
<b>Optional</b>{" "}
|
||||||
|
<i className="min-[390px]:text-sm text-xs">
|
||||||
|
(but it really helps us improve!)
|
||||||
|
</i>
|
||||||
|
</legend>
|
||||||
|
<label className="w-full flex min-[430px]:items-center items-start gap-2 mb-3 min-[430px]:flex-row flex-col">
|
||||||
|
<p className="text-sm">Reason for cancellation:</p>
|
||||||
|
<select
|
||||||
|
className="rounded-md p-1 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
|
||||||
|
value={feedback}
|
||||||
|
onChange={(e) => setFeedback(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value={undefined}>Please specify</option>
|
||||||
|
<option value="customer_service">Customer Service</option>
|
||||||
|
<option value="low_quality">Low Quality</option>
|
||||||
|
<option value="missing_features">Missing Features</option>
|
||||||
|
<option value="switched_service">Switched Service</option>
|
||||||
|
<option value="too_complex">Too Complex</option>
|
||||||
|
<option value="too_expensive">Too Expensive</option>
|
||||||
|
<option value="unused">Unused</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm mb-2 text-black dark:text-white">
|
||||||
|
More information (the more details, the more helpful it'd be)
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
placeholder="e.g. I needed a feature that..."
|
||||||
|
className="resize-none w-full rounded-md p-2 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`mx-auto lg:mx-0 text-white flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit ${
|
||||||
|
submitLoader
|
||||||
|
? "bg-red-400 cursor-auto"
|
||||||
|
: "bg-red-500 hover:bg-red-400 cursor-pointer"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-center w-full">Delete Your Account</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CenteredForm>
|
||||||
|
);
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ export default function Password() {
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (newPassword == "" || newPassword2 == "") {
|
if (newPassword == "" || newPassword2 == "") {
|
||||||
toast.error("Please fill all the fields.");
|
return toast.error("Please fill all the fields.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword !== newPassword2)
|
if (newPassword !== newPassword2)
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default function Subscribe() {
|
||||||
const { data, status } = useSession();
|
const { data, status } = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function loginUser() {
|
async function submit() {
|
||||||
setSubmitLoader(true);
|
setSubmitLoader(true);
|
||||||
|
|
||||||
const redirectionToast = toast.loading("Redirecting to Stripe...");
|
const redirectionToast = toast.loading("Redirecting to Stripe...");
|
||||||
|
@ -32,11 +32,13 @@ export default function Subscribe() {
|
||||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||||
}-day free trial, cancel anytime!`}
|
}-day free trial, cancel anytime!`}
|
||||||
>
|
>
|
||||||
<div className="p-2 mx-auto flex flex-col gap-3 justify-between sm:w-[30rem] dark:border-neutral-700 w-80 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
<div className="p-4 mx-auto flex flex-col gap-3 justify-between dark:border-neutral-700 max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||||
<p className="text-2xl text-center font-bold">
|
<p className="sm:text-3xl text-2xl text-center font-extralight">
|
||||||
Subscribe to Linkwarden!
|
Subscribe to Linkwarden!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
You will be redirected to Stripe, feel free to reach out to us at{" "}
|
You will be redirected to Stripe, feel free to reach out to us at{" "}
|
||||||
|
@ -84,24 +86,27 @@ export default function Subscribe() {
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
|
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<fieldset className="w-full px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700">
|
||||||
<p className="w-fit">Total:</p>
|
<legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl">
|
||||||
<div className="w-full p-1 rounded-md border border-solid border-sky-100 dark:border-neutral-700">
|
Total
|
||||||
<p className="text-sm">
|
</legend>
|
||||||
{process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS}-day free trial, then
|
|
||||||
${plan === Plan.monthly ? "4 per month" : "36 annually"}
|
<p className="text-sm">
|
||||||
</p>
|
{process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS}-day free trial, then $
|
||||||
<p className="text-sm">+ VAT if applicable</p>
|
{plan === Plan.monthly ? "4 per month" : "36 annually"}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<p className="text-sm">+ VAT if applicable</p>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SubmitButton
|
<button
|
||||||
onClick={loginUser}
|
className={`border primary-btn-gradient select-none duration-100 bg-black border-[#0071B7] hover:border-[#059bf8] rounded-lg text-center px-4 py-2 text-slate-200 hover:text-white `}
|
||||||
label="Complete your Subscription"
|
onClick={() => {
|
||||||
className="mt-2 w-full text-center"
|
if (!submitLoader) submit();
|
||||||
loading={submitLoader}
|
}}
|
||||||
/>
|
>
|
||||||
|
<p className="text-center w-full font-bold">Complete Subscription!</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => signOut()}
|
onClick={() => signOut()}
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default function Index() {
|
||||||
<div className="p-5 flex flex-col gap-5 w-full">
|
<div className="p-5 flex flex-col gap-5 w-full">
|
||||||
<div className="flex gap-3 items-center justify-between">
|
<div className="flex gap-3 items-center justify-between">
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
<div className="flex gap-2 items-end">
|
<div className="flex gap-2 items-end font-thin">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHashtag}
|
icon={faHashtag}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
|
||||||
|
@ -159,7 +159,7 @@ export default function Index() {
|
||||||
if (target.id !== "expand-dropdown")
|
if (target.id !== "expand-dropdown")
|
||||||
setExpandDropdown(false);
|
setExpandDropdown(false);
|
||||||
}}
|
}}
|
||||||
className="absolute top-8 left-0 w-36"
|
className="absolute top-8 left-0 w-36 font-normal"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -231,3 +231,11 @@
|
||||||
.sky-shadow {
|
.sky-shadow {
|
||||||
box-shadow: 0px 0px 3px #0ea5e9;
|
box-shadow: 0px 0px 3px #0ea5e9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-btn-gradient {
|
||||||
|
box-shadow: inset 0px -10px 10px #0071b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn-gradient:hover {
|
||||||
|
box-shadow: inset 0px -15px 10px #059bf8;
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ declare global {
|
||||||
PAGINATION_TAKE_COUNT?: string;
|
PAGINATION_TAKE_COUNT?: string;
|
||||||
STORAGE_FOLDER?: string;
|
STORAGE_FOLDER?: string;
|
||||||
AUTOSCROLL_TIMEOUT?: string;
|
AUTOSCROLL_TIMEOUT?: string;
|
||||||
|
IMPORT_SIZE_LIMIT?: string;
|
||||||
|
|
||||||
SPACES_KEY?: string;
|
SPACES_KEY?: string;
|
||||||
SPACES_SECRET?: string;
|
SPACES_SECRET?: string;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Collection, Link, Tag, User } from "@prisma/client";
|
import { Collection, Link, Tag, User } from "@prisma/client";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
type OptionalExcluding<T, TRequired extends keyof T> = Partial<T> &
|
type OptionalExcluding<T, TRequired extends keyof T> = Partial<T> &
|
||||||
Pick<T, TRequired>;
|
Pick<T, TRequired>;
|
||||||
|
@ -96,3 +97,11 @@ export enum Plan {
|
||||||
monthly,
|
monthly,
|
||||||
yearly,
|
yearly,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DeleteUserBody = {
|
||||||
|
password: string;
|
||||||
|
cancellation_details?: {
|
||||||
|
comment?: string;
|
||||||
|
feedback?: Stripe.SubscriptionCancelParams.CancellationDetails.Feedback;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Ŝarĝante…
Reference in New Issue