diff --git a/components/Modal/Link/LinkDetails.tsx b/components/Modal/Link/LinkDetails.tsx index 317286e..7060ee8 100644 --- a/components/Modal/Link/LinkDetails.tsx +++ b/components/Modal/Link/LinkDetails.tsx @@ -79,25 +79,29 @@ export default function LinkDetails({ link, isOwnerOrMod }: Props) { const bannerInner = document.getElementById("link-banner-inner"); if (colorPalette && banner && bannerInner) { - banner.style.background = `linear-gradient(to right, ${rgbToHex( - colorPalette[0][0], - colorPalette[0][1], - colorPalette[0][2] - )}, ${rgbToHex( - colorPalette[1][0], - colorPalette[1][1], - colorPalette[1][2] - )})`; + if (colorPalette[0] && colorPalette[1]) { + banner.style.background = `linear-gradient(to right, ${rgbToHex( + colorPalette[0][0], + colorPalette[0][1], + colorPalette[0][2] + )}, ${rgbToHex( + colorPalette[1][0], + colorPalette[1][1], + colorPalette[1][2] + )})`; + } - bannerInner.style.background = `linear-gradient(to right, ${rgbToHex( - colorPalette[2][0], - colorPalette[2][1], - colorPalette[2][2] - )}, ${rgbToHex( - colorPalette[3][0], - colorPalette[3][1], - colorPalette[3][2] - )})`; + if (colorPalette[2] && colorPalette[3]) { + bannerInner.style.background = `linear-gradient(to right, ${rgbToHex( + colorPalette[2][0], + colorPalette[2][1], + colorPalette[2][2] + )}, ${rgbToHex( + colorPalette[3][0], + colorPalette[3][1], + colorPalette[3][2] + )})`; + } } }, [colorPalette, theme]); diff --git a/components/Modal/User/ProfileSettings.tsx b/components/Modal/User/ProfileSettings.tsx index 95ce4b8..0bb48eb 100644 --- a/components/Modal/User/ProfileSettings.tsx +++ b/components/Modal/User/ProfileSettings.tsx @@ -109,7 +109,7 @@ export default function ProfileSettings({
{profileStatus && ( @@ -120,7 +120,7 @@ export default function ProfileSettings({ profilePic: "", }) } - className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 bg-white border-slate-200 rounded-full text-gray-500 hover:text-red-500 duration-100 cursor-pointer" + 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" >
diff --git a/components/Navbar.tsx b/components/Navbar.tsx index ee300e0..f780f32 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -19,8 +19,6 @@ export default function Navbar() { const [profileDropdown, setProfileDropdown] = useState(false); - const [sidebar, setSidebar] = useState(false); - const router = useRouter(); const { theme, setTheme } = useTheme(); @@ -33,6 +31,8 @@ export default function Navbar() { } }; + const [sidebar, setSidebar] = useState(false); + window.addEventListener("resize", () => setSidebar(false)); useEffect(() => { @@ -79,6 +79,7 @@ export default function Navbar() { >

- +
diff --git a/components/ProfilePhoto.tsx b/components/ProfilePhoto.tsx index f9c99eb..e2f2b39 100644 --- a/components/ProfilePhoto.tsx +++ b/components/ProfilePhoto.tsx @@ -9,6 +9,7 @@ type Props = { className?: string; emptyImage?: boolean; status?: Function; + priority?: boolean; }; export default function ProfilePhoto({ @@ -16,6 +17,7 @@ export default function ProfilePhoto({ className, emptyImage, status, + priority, }: Props) { const [error, setError] = useState(emptyImage || true); @@ -43,6 +45,7 @@ export default function ProfilePhoto({ src={src} height={112} width={112} + priority={priority} className={`h-10 w-10 bg-sky-600 dark:bg-sky-600 shadow rounded-full aspect-square border border-slate-200 dark:border-neutral-700 ${className}`} /> ); diff --git a/components/SettingsSidebar.tsx b/components/SettingsSidebar.tsx new file mode 100644 index 0000000..eb26f78 --- /dev/null +++ b/components/SettingsSidebar.tsx @@ -0,0 +1,219 @@ +import useCollectionStore from "@/store/collections"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faUser, + faPalette, + faBoxArchive, + faLock, + faKey, +} from "@fortawesome/free-solid-svg-icons"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { + faCircleQuestion, + faCreditCard, +} from "@fortawesome/free-regular-svg-icons"; +import { + faGithub, + faMastodon, + faXTwitter, +} from "@fortawesome/free-brands-svg-icons"; + +export default function SettingsSidebar({ className }: { className?: string }) { + const { collections } = useCollectionStore(); + + const router = useRouter(); + + const [active, setActive] = useState(""); + + useEffect(() => { + setActive(router.asPath); + }, [router, collections]); + + return ( +
+
+ +
+ + +

+ Profile +

+
+ + + +
+ + +

+ Appearance +

+
+ + + +
+ + +

+ Archive +

+
+ + + +
+ + +

+ Privacy +

+
+ + + +
+ + +

+ Password +

+
+ + + {process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE ? ( + +
+ + +

+ Billing +

+
+ + ) : undefined} +
+ +
+ +
+ + +

+ Twitter +

+
+ + + +
+ + +

+ Mastodon +

+
+ + + +
+ + +

+ GitHub +

+
+ + + +
+ + +

+ Help +

+
+ +
+
+ ); +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index d121ae2..7254bde 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -12,7 +12,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Disclosure, Transition } from "@headlessui/react"; -import Image from "next/image"; export default function Sidebar({ className }: { className?: string }) { const [tagDisclosure, setTagDisclosure] = useState(() => { diff --git a/layouts/MainLayout.tsx b/layouts/MainLayout.tsx index 87d15e6..48ecbb8 100644 --- a/layouts/MainLayout.tsx +++ b/layouts/MainLayout.tsx @@ -1,10 +1,6 @@ import Navbar from "@/components/Navbar"; import Sidebar from "@/components/Sidebar"; import { ReactNode, useEffect } from "react"; -import { useSession } from "next-auth/react"; -import Loader from "../components/Loader"; -import useRedirect from "@/hooks/useRedirect"; -import { useRouter } from "next/router"; import ModalManagement from "@/components/ModalManagement"; import useModalStore from "@/store/modals"; @@ -13,11 +9,6 @@ interface Props { } export default function MainLayout({ children }: Props) { - const { status, data } = useSession(); - const router = useRouter(); - const redirect = useRedirect(); - const routeExists = router.route === "/_error" ? false : true; - const { modal } = useModalStore(); useEffect(() => { @@ -26,24 +17,20 @@ export default function MainLayout({ children }: Props) { : (document.body.style.overflow = "auto"); }, [modal]); - if (status === "authenticated" && !redirect && routeExists) - return ( - <> - + return ( + <> + -
-
- -
- -
- - {children} -
+
+
+
- - ); - else if ((status === "unauthenticated" && !redirect) || !routeExists) - return <>{children}; - else return <>; + +
+ + {children} +
+
+ + ); } diff --git a/layouts/SettingsLayout.tsx b/layouts/SettingsLayout.tsx new file mode 100644 index 0000000..ae5c08f --- /dev/null +++ b/layouts/SettingsLayout.tsx @@ -0,0 +1,78 @@ +import SettingsSidebar from "@/components/SettingsSidebar"; +import { ReactNode, useEffect, useState } from "react"; +import ModalManagement from "@/components/ModalManagement"; +import useModalStore from "@/store/modals"; +import { useRouter } from "next/router"; +import ClickAwayHandler from "@/components/ClickAwayHandler"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBars } from "@fortawesome/free-solid-svg-icons"; + +interface Props { + children: ReactNode; +} + +export default function SettingsLayout({ children }: Props) { + const { modal } = useModalStore(); + + const router = useRouter(); + + useEffect(() => { + modal + ? (document.body.style.overflow = "hidden") + : (document.body.style.overflow = "auto"); + }, [modal]); + + const [sidebar, setSidebar] = useState(false); + + window.addEventListener("resize", () => setSidebar(false)); + + useEffect(() => { + setSidebar(false); + }, [router]); + + const toggleSidebar = () => { + setSidebar(!sidebar); + }; + + return ( + <> + + +
+
+ +
+ +
+
+
+ +
+ +

+ {router.asPath.split("/").pop()} Settings +

+
+
+ {children} + + {sidebar ? ( +
+ +
+ +
+
+
+ ) : null} +
+
+ + ); +} diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts index be02293..ac0d057 100644 --- a/lib/api/controllers/links/getLinks.ts +++ b/lib/api/controllers/links/getLinks.ts @@ -3,138 +3,130 @@ import { LinkRequestQuery, Sort } from "@/types/global"; export default async function getLink(userId: number, body: string) { const query: LinkRequestQuery = JSON.parse(decodeURIComponent(body)); - console.log(query); const POSTGRES_IS_ENABLED = process.env.DATABASE_URL.startsWith("postgresql"); - // Sorting logic + let order: any; - if (query.sort === Sort.DateNewestFirst) - order = { - createdAt: "desc", - }; - else if (query.sort === Sort.DateOldestFirst) - order = { - createdAt: "asc", - }; - else if (query.sort === Sort.NameAZ) - order = { - name: "asc", - }; - else if (query.sort === Sort.NameZA) - order = { - name: "desc", - }; - else if (query.sort === Sort.DescriptionAZ) - order = { - description: "asc", - }; - else if (query.sort === Sort.DescriptionZA) - order = { - description: "desc", - }; + if (query.sort === Sort.DateNewestFirst) order = { createdAt: "desc" }; + else if (query.sort === Sort.DateOldestFirst) order = { createdAt: "asc" }; + else if (query.sort === Sort.NameAZ) order = { name: "asc" }; + else if (query.sort === Sort.NameZA) order = { name: "desc" }; + else if (query.sort === Sort.DescriptionAZ) order = { description: "asc" }; + else if (query.sort === Sort.DescriptionZA) order = { description: "desc" }; + + const searchConditions = []; + + if (query.searchQuery) { + if (query.searchFilter?.name) { + searchConditions.push({ + name: { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchFilter?.url) { + searchConditions.push({ + url: { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchFilter?.description) { + searchConditions.push({ + description: { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + }); + } + + if (query.searchFilter?.tags) { + searchConditions.push({ + tags: { + some: { + name: { + contains: query.searchQuery, + mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + }, + OR: [ + { ownerId: userId }, + { + links: { + some: { + collection: { + members: { + some: { userId }, + }, + }, + }, + }, + }, + ], + }, + }, + }); + } + } + + const tagCondition = []; + + if (query.tagId) { + tagCondition.push({ + tags: { + some: { + id: query.tagId, + }, + }, + }); + } + + const collectionCondition = []; + + if (query.collectionId) { + collectionCondition.push({ + collection: { + id: query.collectionId, + }, + }); + } const links = await prisma.link.findMany({ take: Number(process.env.PAGINATION_TAKE_COUNT) || 20, skip: query.cursor ? 1 : undefined, - cursor: query.cursor - ? { - id: query.cursor, - } - : undefined, + cursor: query.cursor ? { id: query.cursor } : undefined, where: { - collection: { - id: query.collectionId ? query.collectionId : undefined, // If collectionId was defined, filter by collection - OR: [ - { - ownerId: userId, - }, - { - members: { - some: { - userId, + AND: [ + { + collection: { + OR: [ + { ownerId: userId }, + { + members: { + some: { userId }, + }, }, - }, - }, - ], - }, - [query.searchQuery ? "OR" : "AND"]: [ - { - pinnedBy: query.pinnedOnly ? { some: { id: userId } } : undefined, - }, - { - name: { - contains: - query.searchQuery && query.searchFilter?.name - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, + ], }, }, + ...collectionCondition, { - url: { - contains: - query.searchQuery && query.searchFilter?.url - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, - }, - }, - { - description: { - contains: - query.searchQuery && query.searchFilter?.description - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED ? "insensitive" : undefined, - }, - }, - { - tags: - query.searchQuery && !query.searchFilter?.tags - ? undefined - : { - some: query.tagId - ? { - // If tagId was defined, filter by tag - id: query.tagId, - name: - query.searchQuery && query.searchFilter?.tags - ? { - contains: query.searchQuery, - mode: POSTGRES_IS_ENABLED - ? "insensitive" - : undefined, - } - : undefined, - OR: [ - { ownerId: userId }, // Tags owned by the user - { - links: { - some: { - name: { - contains: - query.searchQuery && - query.searchFilter?.tags - ? query.searchQuery - : undefined, - mode: POSTGRES_IS_ENABLED - ? "insensitive" - : undefined, - }, - collection: { - members: { - some: { - userId, // Tags from collections where the user is a member - }, - }, - }, - }, - }, - }, - ], - } + OR: [ + ...tagCondition, + { + [query.searchQuery ? "OR" : "AND"]: [ + { + pinnedBy: query.pinnedOnly + ? { some: { id: userId } } : undefined, }, + ...searchConditions, + ], + }, + ], }, ], }, @@ -146,9 +138,7 @@ export default async function getLink(userId: number, body: string) { select: { id: true }, }, }, - orderBy: order || { - createdAt: "desc", - }, + orderBy: order || { createdAt: "desc" }, }); return { response: links, status: 200 }; diff --git a/lib/client/avatarExists.ts b/lib/client/avatarExists.ts index 2e82fa6..f81f1e9 100644 --- a/lib/client/avatarExists.ts +++ b/lib/client/avatarExists.ts @@ -1,4 +1,13 @@ +const avatarCache = new Map(); + export default async function avatarExists(fileUrl: string): Promise { + if (avatarCache.has(fileUrl)) { + return avatarCache.get(fileUrl); + } + const response = await fetch(fileUrl, { method: "HEAD" }); - return !(response.headers.get("content-type") === "text/html"); + const exists = !(response.headers.get("content-type") === "text/html"); + + avatarCache.set(fileUrl, exists); + return exists; } diff --git a/package.json b/package.json index 22e782b..93867e6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@auth/prisma-adapter": "^1.0.1", "@aws-sdk/client-s3": "^3.379.1", "@fortawesome/fontawesome-svg-core": "^6.4.0", + "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index 34626f8..d3f6f56 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -229,11 +229,9 @@ export default function Index() {
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
- {links - .filter((e) => e.collectionId === Number(router.query.id)) - .map((e, i) => { - return ; - })} + {links.map((e, i) => { + return ; + })}
) : ( diff --git a/pages/index.tsx b/pages/index.tsx index 7c4db0c..e4679ca 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,7 +1,7 @@ import { useRouter } from "next/router"; import { useEffect } from "react"; -export default function Home() { +export default function Index() { const router = useRouter(); useEffect(() => { diff --git a/pages/settings/appearance.tsx b/pages/settings/appearance.tsx new file mode 100644 index 0000000..5eb17f9 --- /dev/null +++ b/pages/settings/appearance.tsx @@ -0,0 +1,10 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import React from "react"; + +export default function appearance() { + return ( + +
appearance
+
+ ); +} diff --git a/pages/settings/archive.tsx b/pages/settings/archive.tsx new file mode 100644 index 0000000..c0faa5a --- /dev/null +++ b/pages/settings/archive.tsx @@ -0,0 +1,10 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import React from "react"; + +export default function archive() { + return ( + +
archive
+
+ ); +} diff --git a/pages/settings/billing.tsx b/pages/settings/billing.tsx new file mode 100644 index 0000000..cc9f040 --- /dev/null +++ b/pages/settings/billing.tsx @@ -0,0 +1,18 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +export default function billing() { + const router = useRouter(); + + useEffect(() => { + if (!process.env.NEXT_PUBLIC_STRIPE_IS_ACTIVE) + router.push("/settings/profile"); + }, []); + + return ( + +
Billing
+
+ ); +} diff --git a/pages/settings/index.tsx b/pages/settings/index.tsx new file mode 100644 index 0000000..3adb156 --- /dev/null +++ b/pages/settings/index.tsx @@ -0,0 +1,10 @@ +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +export default function Settings() { + const router = useRouter(); + + useEffect(() => { + router.push("/settings/profile"); + }, []); +} diff --git a/pages/settings/password.tsx b/pages/settings/password.tsx new file mode 100644 index 0000000..bffea57 --- /dev/null +++ b/pages/settings/password.tsx @@ -0,0 +1,10 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import React from "react"; + +export default function password() { + return ( + +
password
+
+ ); +} diff --git a/pages/settings/privacy.tsx b/pages/settings/privacy.tsx new file mode 100644 index 0000000..b57f9d5 --- /dev/null +++ b/pages/settings/privacy.tsx @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..ae776ca --- /dev/null +++ b/pages/settings/profile.tsx @@ -0,0 +1,10 @@ +import SettingsLayout from "@/layouts/SettingsLayout"; +import React from "react"; + +export default function profile() { + return ( + +
profile
+
+ ); +} diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx index 97a9228..4acd069 100644 --- a/pages/tags/[id].tsx +++ b/pages/tags/[id].tsx @@ -67,11 +67,9 @@ export default function Index() {
- {links - .filter((e) => e.tags.some((e) => e.id === Number(router.query.id))) - .map((e, i) => { - return ; - })} + {links.map((e, i) => { + return ; + })}
diff --git a/yarn.lock b/yarn.lock index 83ac24e..bb2f10a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -779,6 +779,11 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.0.tgz#88da2b70d6ca18aaa6ed3687832e11f39e80624b" integrity sha512-HNii132xfomg5QVZw0HwXXpN22s7VBHQBv9CeOu9tfJnhsWQNd2lmTNi8CSrnw5B+5YOmzu1UoPAyxaXsJ6RgQ== +"@fortawesome/fontawesome-common-types@6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" + integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== + "@fortawesome/fontawesome-svg-core@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.0.tgz#3727552eff9179506e9203d72feb5b1063c11a21" @@ -786,6 +791,13 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.4.0" +"@fortawesome/free-brands-svg-icons@^6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz#9b8e78066ea6dd563da5dfa686615791d0f7cc71" + integrity sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + "@fortawesome/free-regular-svg-icons@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.0.tgz#cacc53bd8d832d46feead412d9ea9ce80a55e13a"