+
-
+
+
+
+
+
{router.asPath.split("/").pop()} Settings
-
+
+
+
{children}
{sidebar ? (
diff --git a/lib/api/archive.ts b/lib/api/archive.ts
index 685413e..fce0454 100644
--- a/lib/api/archive.ts
+++ b/lib/api/archive.ts
@@ -1,55 +1,72 @@
-import { Page, chromium, devices } from "playwright";
+import { chromium, devices } from "playwright";
import { prisma } from "@/lib/api/db";
import createFile from "@/lib/api/storage/createFile";
import sendToWayback from "./sendToWayback";
-export default async function archive(linkId: number, url: string) {
- const browser = await chromium.launch();
- const context = await browser.newContext(devices["Desktop Chrome"]);
- const page = await context.newPage();
+export default async function archive(
+ linkId: number,
+ url: string,
+ userId: number
+) {
+ const user = await prisma.user.findUnique({
+ where: {
+ id: userId,
+ },
+ });
- sendToWayback(url);
+ if (user?.archiveAsWaybackMachine) sendToWayback(url);
- try {
- await page.goto(url, { waitUntil: "domcontentloaded" });
+ if (user?.archiveAsPDF || user?.archiveAsScreenshot) {
+ const browser = await chromium.launch();
+ const context = await browser.newContext(devices["Desktop Chrome"]);
+ const page = await context.newPage();
- await page.evaluate(
- autoScroll,
- Number(process.env.AUTOSCROLL_TIMEOUT) || 30
- );
+ try {
+ await page.goto(url, { waitUntil: "domcontentloaded" });
- const linkExists = await prisma.link.findUnique({
- where: {
- id: linkId,
- },
- });
+ await page.evaluate(
+ autoScroll,
+ Number(process.env.AUTOSCROLL_TIMEOUT) || 30
+ );
- if (linkExists) {
- const pdf = await page.pdf({
- width: "1366px",
- height: "1931px",
- printBackground: true,
- margin: { top: "15px", bottom: "15px" },
- });
- const screenshot = await page.screenshot({
- fullPage: true,
+ const linkExists = await prisma.link.findUnique({
+ where: {
+ id: linkId,
+ },
});
- createFile({
- data: screenshot,
- filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
- });
+ if (linkExists) {
+ if (user.archiveAsScreenshot) {
+ const screenshot = await page.screenshot({
+ fullPage: true,
+ });
- createFile({
- data: pdf,
- filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
- });
+ createFile({
+ data: screenshot,
+ filePath: `archives/${linkExists.collectionId}/${linkId}.png`,
+ });
+ }
+
+ if (user.archiveAsPDF) {
+ const pdf = await page.pdf({
+ width: "1366px",
+ height: "1931px",
+ printBackground: true,
+ margin: { top: "15px", bottom: "15px" },
+ });
+
+ createFile({
+ data: pdf,
+ filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
+ });
+ }
+ }
+
+ await browser.close();
+ } catch (err) {
+ console.log(err);
+ await browser.close();
}
-
- await browser.close();
- } catch (err) {
- console.log(err);
- await browser.close();
}
}
diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts
index 8c849bb..33b4ba2 100644
--- a/lib/api/controllers/links/postLink.ts
+++ b/lib/api/controllers/links/postLink.ts
@@ -2,7 +2,7 @@ import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
import getTitle from "@/lib/api/getTitle";
import archive from "@/lib/api/archive";
-import { Collection, Link, UsersAndCollections } from "@prisma/client";
+import { Collection, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import createFolder from "@/lib/api/storage/createFolder";
@@ -94,7 +94,7 @@ export default async function postLink(
createFolder({ filePath: `archives/${newLink.collectionId}` });
- archive(newLink.id, newLink.url);
+ archive(newLink.id, newLink.url, userId);
return { response: newLink, status: 200 };
}
diff --git a/lib/api/controllers/users/updateUser.ts b/lib/api/controllers/users/updateUser.ts
index 5e1d0da..6bb4c22 100644
--- a/lib/api/controllers/users/updateUser.ts
+++ b/lib/api/controllers/users/updateUser.ts
@@ -108,6 +108,9 @@ export default async function updateUser(
username: user.username.toLowerCase(),
email: user.email?.toLowerCase(),
isPrivate: user.isPrivate,
+ archiveAsScreenshot: user.archiveAsScreenshot,
+ archiveAsPDF: user.archiveAsPDF,
+ archiveAsWaybackMachine: user.archiveAsWaybackMachine,
password:
user.newPassword && user.newPassword !== ""
? newHashedPassword
@@ -161,7 +164,7 @@ export default async function updateUser(
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
- if (STRIPE_SECRET_KEY && emailEnabled)
+ if (STRIPE_SECRET_KEY && emailEnabled && sessionUser.email !== user.email)
await updateCustomerEmail(
STRIPE_SECRET_KEY,
sessionUser.email,
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 7e8dd7c..f5966b4 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -52,7 +52,7 @@ export default function App({
reverseOrder={false}
toastOptions={{
className:
- "border border-sky-100 dark:dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
+ "border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
}}
/>
diff --git a/pages/choose-username.tsx b/pages/choose-username.tsx
index c4f2f9e..75d6aa0 100644
--- a/pages/choose-username.tsx
+++ b/pages/choose-username.tsx
@@ -1,15 +1,13 @@
import SubmitButton from "@/components/SubmitButton";
import { signOut } from "next-auth/react";
-import Image from "next/image";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { toast } from "react-hot-toast";
import { useSession } from "next-auth/react";
-import { useRouter } from "next/router";
import useAccountStore from "@/store/account";
import CenteredForm from "@/layouts/CenteredForm";
import TextInput from "@/components/TextInput";
-export default function Subscribe() {
+export default function ChooseUsername() {
const [submitLoader, setSubmitLoader] = useState(false);
const [inputedUsername, setInputedUsername] = useState("");
diff --git a/pages/forgot.tsx b/pages/forgot.tsx
index 25389ad..eb60eb7 100644
--- a/pages/forgot.tsx
+++ b/pages/forgot.tsx
@@ -2,7 +2,6 @@ import SubmitButton from "@/components/SubmitButton";
import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react";
-import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { toast } from "react-hot-toast";
diff --git a/pages/login.tsx b/pages/login.tsx
index f06d3b4..f6968cc 100644
--- a/pages/login.tsx
+++ b/pages/login.tsx
@@ -2,7 +2,6 @@ import SubmitButton from "@/components/SubmitButton";
import TextInput from "@/components/TextInput";
import CenteredForm from "@/layouts/CenteredForm";
import { signIn } from "next-auth/react";
-import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { toast } from "react-hot-toast";
diff --git a/pages/settings/account.tsx b/pages/settings/account.tsx
new file mode 100644
index 0000000..9cd52b6
--- /dev/null
+++ b/pages/settings/account.tsx
@@ -0,0 +1,393 @@
+import { useState, useEffect } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faClose, faPenToSquare } from "@fortawesome/free-solid-svg-icons";
+import useAccountStore from "@/store/account";
+import { AccountSettings } from "@/types/global";
+import { toast } from "react-hot-toast";
+import SettingsLayout from "@/layouts/SettingsLayout";
+import TextInput from "@/components/TextInput";
+import { resizeImage } from "@/lib/client/resizeImage";
+import ProfilePhoto from "@/components/ProfilePhoto";
+import SubmitButton from "@/components/SubmitButton";
+import { useSession, signOut } from "next-auth/react";
+import React from "react";
+import { MigrationFormat, MigrationRequest } from "@/types/global";
+import Link from "next/link";
+import ClickAwayHandler from "@/components/ClickAwayHandler";
+import Checkbox from "@/components/Checkbox";
+
+export default function Account() {
+ const { update, data } = useSession();
+
+ const emailEnabled = process.env.NEXT_PUBLIC_EMAIL_PROVIDER;
+
+ const [profileStatus, setProfileStatus] = useState(true);
+ const [submitLoader, setSubmitLoader] = useState(false);
+
+ const handleProfileStatus = (e: boolean) => {
+ setProfileStatus(!e);
+ };
+
+ const { account, updateAccount } = useAccountStore();
+
+ const [user, setUser] = useState
(
+ !objectIsEmpty(account)
+ ? account
+ : ({
+ // @ts-ignore
+ id: null,
+ name: "",
+ username: "",
+ email: "",
+ emailVerified: null,
+ image: null,
+ isPrivate: true,
+ // @ts-ignore
+ createdAt: null,
+ whitelistedUsers: [],
+ profilePic: "",
+ } as unknown as AccountSettings)
+ );
+
+ function objectIsEmpty(obj: object) {
+ return Object.keys(obj).length === 0;
+ }
+
+ useEffect(() => {
+ if (!objectIsEmpty(account)) setUser({ ...account });
+ }, [account]);
+
+ 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)) {
+ const resizedFile = await resizeImage(file);
+ if (
+ resizedFile.size < 1048576 // 1048576 Bytes == 1MB
+ ) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ setUser({ ...user, profilePic: reader.result as string });
+ };
+ reader.readAsDataURL(resizedFile);
+ } else {
+ toast.error("Please select a PNG or JPEG file thats less than 1MB.");
+ }
+ } else {
+ toast.error("Invalid file format.");
+ }
+ };
+
+ const submit = async () => {
+ setSubmitLoader(true);
+
+ const load = toast.loading("Applying...");
+
+ const response = await updateAccount({
+ ...user,
+ });
+
+ toast.dismiss(load);
+
+ if (response.ok) {
+ toast.success("Settings Applied!");
+
+ if (user.email !== account.email) {
+ update({
+ id: data?.user.id,
+ });
+
+ signOut();
+ } else if (
+ user.username !== account.username ||
+ user.name !== account.name
+ )
+ update({
+ id: data?.user.id,
+ });
+ } else toast.error(response.data as string);
+ setSubmitLoader(false);
+ };
+
+ const [importDropdown, setImportDropdown] = useState(false);
+
+ const importBookmarks = async (e: any, format: MigrationFormat) => {
+ const file: File = e.target.files[0];
+
+ if (file) {
+ var reader = new FileReader();
+ reader.readAsText(file, "UTF-8");
+ reader.onload = async function (e) {
+ const load = toast.loading("Importing...");
+
+ const request: string = e.target?.result as string;
+
+ const body: MigrationRequest = {
+ format,
+ data: request,
+ };
+
+ const response = await fetch("/api/migration", {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+
+ const data = await response.json();
+
+ toast.dismiss(load);
+
+ toast.success("Imported the Bookmarks! Reloading the page...");
+
+ setImportDropdown(false);
+
+ setTimeout(() => {
+ location.reload();
+ }, 2000);
+ };
+ reader.onerror = function (e) {
+ console.log("Error:", e);
+ };
+ }
+ };
+
+ const [whitelistedUsersTextbox, setWhiteListedUsersTextbox] = useState("");
+
+ useEffect(() => {
+ setWhiteListedUsersTextbox(account?.whitelistedUsers?.join(", "));
+ }, [account]);
+
+ useEffect(() => {
+ setUser({
+ ...user,
+ whitelistedUsers: stringToArray(whitelistedUsersTextbox),
+ });
+ }, [whitelistedUsersTextbox]);
+
+ const stringToArray = (str: string) => {
+ const stringWithoutSpaces = str?.replace(/\s+/g, "");
+
+ const wordsArray = stringWithoutSpaces?.split(",");
+
+ return wordsArray;
+ };
+
+ return (
+
+
+
+
+
+
+ Display Name
+
+
setUser({ ...user, name: e.target.value })}
+ />
+
+
+
+ Username
+
+
setUser({ ...user, username: e.target.value })}
+ />
+
+
+ {emailEnabled ? (
+
+
Email
+
setUser({ ...user, email: e.target.value })}
+ />
+
+ ) : undefined}
+
+ {user.email !== account.email ? (
+
+ You will need to log back in after you apply this Email.
+
+ ) : undefined}
+
+
+
+
+ Profile Photo
+
+
+
+ {profileStatus && (
+
+ setUser({
+ ...user,
+ profilePic: "",
+ })
+ }
+ className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500"
+ >
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Import your data from other platforms.
+
+
+
+
+
+
+ Download your data instantly.
+
+
+
+ Export Data
+
+
+
+
+
+
+
+
+
+ Profile Visibility
+
+
+
+
+
+
setUser({ ...user, isPrivate: !user.isPrivate })}
+ />
+
+
+ This will limit who can find and add you to other Collections.
+
+
+ {user.isPrivate && (
+
+
+ Whitelisted Users
+
+
+ Please provide the Username of the users you wish to grant
+ visibility to your profile. Separated by comma.
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/pages/settings/appearance.tsx b/pages/settings/appearance.tsx
index 5eb17f9..4f333bc 100644
--- a/pages/settings/appearance.tsx
+++ b/pages/settings/appearance.tsx
@@ -1,10 +1,39 @@
import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
+import { useTheme } from "next-themes";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
+
+export default function Appearance() {
+ const { theme, setTheme } = useTheme();
-export default function appearance() {
return (
- appearance
+ Select Theme
+
+
setTheme("dark")}
+ >
+
+
Dark Theme
+
+ {/*
*/}
+
+
setTheme("light")}
+ >
+
+
Light Theme
+ {/*
*/}
+
+
);
}
diff --git a/pages/settings/archive.tsx b/pages/settings/archive.tsx
index c0faa5a..db1c6f1 100644
--- a/pages/settings/archive.tsx
+++ b/pages/settings/archive.tsx
@@ -1,10 +1,88 @@
+import Checkbox from "@/components/Checkbox";
+import SubmitButton from "@/components/SubmitButton";
import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
+import React, { useEffect, useState } from "react";
+import useAccountStore from "@/store/account";
+import { toast } from "react-hot-toast";
+import { AccountSettings } from "@/types/global";
+
+export default function Archive() {
+ const [submitLoader, setSubmitLoader] = useState(false);
+ const { account, updateAccount } = useAccountStore();
+ const [user, setUser] = useState(account);
+
+ const [archiveAsScreenshot, setArchiveAsScreenshot] =
+ useState(false);
+ const [archiveAsPDF, setArchiveAsPDF] = useState(false);
+ const [archiveAsWaybackMachine, setArchiveAsWaybackMachine] =
+ useState(false);
+
+ useEffect(() => {
+ setUser({
+ ...account,
+ archiveAsScreenshot,
+ archiveAsPDF,
+ archiveAsWaybackMachine,
+ });
+ }, [account, archiveAsScreenshot, archiveAsPDF, archiveAsWaybackMachine]);
+
+ function objectIsEmpty(obj: object) {
+ return Object.keys(obj).length === 0;
+ }
+
+ useEffect(() => {
+ if (!objectIsEmpty(account)) {
+ setArchiveAsScreenshot(account.archiveAsScreenshot);
+ setArchiveAsPDF(account.archiveAsPDF);
+ setArchiveAsWaybackMachine(account.archiveAsWaybackMachine);
+ }
+ }, [account]);
+
+ const submit = async () => {
+ setSubmitLoader(true);
+
+ const load = toast.loading("Applying...");
+
+ const response = await updateAccount({
+ ...user,
+ });
+
+ toast.dismiss(load);
+
+ if (response.ok) {
+ toast.success("Settings Applied!");
+ } else toast.error(response.data as string);
+ setSubmitLoader(false);
+ };
-export default function archive() {
return (
- archive
+ Formats to Archive webpages:
+
+ setArchiveAsScreenshot(!archiveAsScreenshot)}
+ />
+ setArchiveAsPDF(!archiveAsPDF)}
+ />
+
+ setArchiveAsWaybackMachine(!archiveAsWaybackMachine)}
+ />
+
+
+
);
}
diff --git a/pages/settings/billing.tsx b/pages/settings/billing.tsx
index cc9f040..185f2a7 100644
--- a/pages/settings/billing.tsx
+++ b/pages/settings/billing.tsx
@@ -2,7 +2,7 @@ import SettingsLayout from "@/layouts/SettingsLayout";
import { useRouter } from "next/router";
import { useEffect } from "react";
-export default function billing() {
+export default function Billing() {
const router = useRouter();
useEffect(() => {
@@ -12,7 +12,29 @@ export default function billing() {
return (
- Billing
+
);
}
diff --git a/pages/settings/migration.tsx b/pages/settings/migration.tsx
deleted file mode 100644
index 023c2ca..0000000
--- a/pages/settings/migration.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
-
-export default function appearance() {
- return (
-
- Migration
-
- );
-}
diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx
index bffea57..b68228c 100644
--- a/pages/settings/password.tsx
+++ b/pages/settings/password.tsx
@@ -1,10 +1,83 @@
import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
+import { useState } from "react";
+import useAccountStore from "@/store/account";
+import { faPenToSquare } from "@fortawesome/free-regular-svg-icons";
+import SubmitButton from "@/components/SubmitButton";
+import { toast } from "react-hot-toast";
+import TextInput from "@/components/TextInput";
+
+export default function Password() {
+ const [newPassword, setNewPassword1] = useState("");
+ const [newPassword2, setNewPassword2] = useState("");
+
+ const [submitLoader, setSubmitLoader] = useState(false);
+
+ const { account, updateAccount } = useAccountStore();
+
+ const submit = async () => {
+ if (newPassword == "" || newPassword2 == "") {
+ toast.error("Please fill all the fields.");
+ }
+
+ if (newPassword !== newPassword2)
+ return toast.error("Passwords do not match.");
+ else if (newPassword.length < 8)
+ return toast.error("Passwords must be at least 8 characters.");
+
+ setSubmitLoader(true);
+
+ const load = toast.loading("Applying...");
+
+ const response = await updateAccount({
+ ...account,
+ newPassword,
+ });
+
+ toast.dismiss(load);
+
+ if (response.ok) {
+ toast.success("Settings Applied!");
+ setNewPassword1("");
+ setNewPassword2("");
+ } else toast.error(response.data as string);
+
+ setSubmitLoader(false);
+ };
-export default function password() {
return (
- password
+
+ To change your password, please fill out the following. Your password
+ should be at least 8 characters.
+
+
+
New Password
+
+
setNewPassword1(e.target.value)}
+ placeholder="••••••••••••••"
+ type="password"
+ />
+
+
+ Confirm New Password
+
+
+ setNewPassword2(e.target.value)}
+ placeholder="••••••••••••••"
+ type="password"
+ />
+
+
+
);
}
diff --git a/pages/settings/privacy.tsx b/pages/settings/privacy.tsx
deleted file mode 100644
index b57f9d5..0000000
--- a/pages/settings/privacy.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
-
-export default function privacy() {
- return (
-
- privacy
-
- );
-}
diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx
deleted file mode 100644
index ae776ca..0000000
--- a/pages/settings/profile.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import SettingsLayout from "@/layouts/SettingsLayout";
-import React from "react";
-
-export default function profile() {
- return (
-
- profile
-
- );
-}
diff --git a/pages/subscribe.tsx b/pages/subscribe.tsx
index a3c1279..b8b478a 100644
--- a/pages/subscribe.tsx
+++ b/pages/subscribe.tsx
@@ -1,6 +1,6 @@
import SubmitButton from "@/components/SubmitButton";
import { signOut } from "next-auth/react";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { toast } from "react-hot-toast";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
diff --git a/prisma/migrations/20231019032936_modify_archive_formats/migration.sql b/prisma/migrations/20231019032936_modify_archive_formats/migration.sql
new file mode 100644
index 0000000..89d866c
--- /dev/null
+++ b/prisma/migrations/20231019032936_modify_archive_formats/migration.sql
@@ -0,0 +1,4 @@
+-- AlterTable
+ALTER TABLE "User" ADD COLUMN "archiveAsPDF" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "archiveAsScreenshot" BOOLEAN NOT NULL DEFAULT true,
+ADD COLUMN "archiveAsWaybackMachine" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index eca457b..ab67eb6 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -54,6 +54,10 @@ model User {
pinnedLinks Link[]
+ archiveAsScreenshot Boolean @default(true)
+ archiveAsPDF Boolean @default(true)
+ archiveAsWaybackMachine Boolean @default(false)
+
collectionsJoined UsersAndCollections[]
isPrivate Boolean @default(false)
whitelistedUsers WhitelistedUser[]
diff --git a/store/modals.ts b/store/modals.ts
index 047bc28..71c336f 100644
--- a/store/modals.ts
+++ b/store/modals.ts
@@ -1,17 +1,10 @@
import {
- AccountSettings,
CollectionIncludingMembersAndLinkCount,
LinkIncludingShortenedCollectionAndTags,
} from "@/types/global";
import { create } from "zustand";
type Modal =
- | {
- modal: "ACCOUNT";
- state: boolean;
- active: AccountSettings;
- defaultIndex?: number;
- }
| {
modal: "LINK";
state: boolean;