Merge branch 'feat/extra-login-providers' into main

This commit is contained in:
Daniel
2023-12-07 09:14:25 +03:30
committed by GitHub
97 changed files with 4416 additions and 2124 deletions
+10 -20
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
import "@/styles/globals.css";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
@@ -6,7 +6,6 @@ import Head from "next/head";
import AuthRedirect from "@/layouts/AuthRedirect";
import { Toaster } from "react-hot-toast";
import { Session } from "next-auth";
import { ThemeProvider } from "next-themes";
export default function App({
Component,
@@ -14,13 +13,6 @@ export default function App({
}: AppProps<{
session: Session;
}>) {
const defaultTheme: "light" | "dark" = "dark";
useEffect(() => {
if (!localStorage.getItem("theme"))
localStorage.setItem("theme", defaultTheme);
}, []);
return (
<SessionProvider
session={pageProps.session}
@@ -50,17 +42,15 @@ export default function App({
<link rel="manifest" href="/site.webmanifest" />
</Head>
<AuthRedirect>
<ThemeProvider attribute="class">
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{
className:
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
}}
/>
<Component {...pageProps} />
</ThemeProvider>
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{
className:
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white",
}}
/>
<Component {...pageProps} />
</AuthRedirect>
</SessionProvider>
);
+120 -25
View File
@@ -3,47 +3,142 @@ import readFile from "@/lib/api/storage/readFile";
import { getToken } from "next-auth/jwt";
import { prisma } from "@/lib/api/db";
import { ArchivedFormat } from "@/types/global";
import verifyUser from "@/lib/api/verifyUser";
import getPermission from "@/lib/api/getPermission";
import { UsersAndCollections } from "@prisma/client";
import formidable from "formidable";
import createFile from "@/lib/api/storage/createFile";
import fs from "fs";
export const config = {
api: {
bodyParser: false,
},
};
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
const linkId = Number(req.query.linkId);
const format = Number(req.query.format);
let suffix;
let suffix: string;
if (format === ArchivedFormat.screenshot) suffix = ".png";
if (format === ArchivedFormat.png) suffix = ".png";
else if (format === ArchivedFormat.jpeg) suffix = ".jpeg";
else if (format === ArchivedFormat.pdf) suffix = ".pdf";
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
//@ts-ignore
if (!linkId || !suffix)
return res.status(401).json({ response: "Invalid parameters." });
const token = await getToken({ req });
const userId = token?.id;
if (req.method === "GET") {
const token = await getToken({ req });
const userId = token?.id;
const collectionIsAccessible = await prisma.collection.findFirst({
where: {
links: {
some: {
id: linkId,
const collectionIsAccessible = await prisma.collection.findFirst({
where: {
links: {
some: {
id: linkId,
},
},
OR: [
{ ownerId: userId || -1 },
{ members: { some: { userId: userId || -1 } } },
{ isPublic: true },
],
},
OR: [
{ ownerId: userId || -1 },
{ members: { some: { userId: userId || -1 } } },
{ isPublic: true },
],
},
});
});
if (!collectionIsAccessible)
return res
.status(401)
.json({ response: "You don't have access to this collection." });
if (!collectionIsAccessible)
return res
.status(401)
.json({ response: "You don't have access to this collection." });
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
res.setHeader("Content-Type", contentType).status(status as number);
const { file, contentType, status } = await readFile(
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
);
return res.send(file);
res.setHeader("Content-Type", contentType).status(status as number);
return res.send(file);
}
// else if (req.method === "POST") {
// const user = await verifyUser({ req, res });
// if (!user) return;
// const collectionPermissions = await getPermission({
// userId: user.id,
// linkId,
// });
// const memberHasAccess = collectionPermissions?.members.some(
// (e: UsersAndCollections) => e.userId === user.id && e.canCreate
// );
// if (!(collectionPermissions?.ownerId === user.id || memberHasAccess))
// return { response: "Collection is not accessible.", status: 401 };
// // await uploadHandler(linkId, )
// const MAX_UPLOAD_SIZE = Number(process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE);
// const form = formidable({
// maxFields: 1,
// maxFiles: 1,
// maxFileSize: MAX_UPLOAD_SIZE || 30 * 1048576,
// });
// form.parse(req, async (err, fields, files) => {
// const allowedMIMETypes = [
// "application/pdf",
// "image/png",
// "image/jpg",
// "image/jpeg",
// ];
// if (
// err ||
// !files.file ||
// !files.file[0] ||
// !allowedMIMETypes.includes(files.file[0].mimetype || "")
// ) {
// // Handle parsing error
// return res.status(500).json({
// response: `Sorry, we couldn't process your file. Please ensure it's a PDF, PNG, or JPG format and doesn't exceed ${MAX_UPLOAD_SIZE}MB.`,
// });
// } else {
// const fileBuffer = fs.readFileSync(files.file[0].filepath);
// const linkStillExists = await prisma.link.findUnique({
// where: { id: linkId },
// });
// if (linkStillExists) {
// await createFile({
// filePath: `archives/${collectionPermissions?.id}/${
// linkId + suffix
// }`,
// data: fileBuffer,
// });
// await prisma.link.update({
// where: { id: linkId },
// data: {
// screenshotPath: `archives/${collectionPermissions?.id}/${
// linkId + suffix
// }`,
// lastPreserved: new Date().toISOString(),
// },
// });
// }
// fs.unlinkSync(files.file[0].filepath);
// }
// return res.status(200).json({
// response: files,
// });
// });
// }
}
+9 -2
View File
@@ -1,7 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
import archive from "@/lib/api/archive";
import urlHandler from "@/lib/api/urlHandler";
import { prisma } from "@/lib/api/db";
import verifyUser from "@/lib/api/verifyUser";
import isValidUrl from "@/lib/shared/isValidUrl";
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
@@ -41,7 +42,13 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
} minutes or create a new one.`,
});
archive(link.id, link.url, user.id);
if (link.url && isValidUrl(link.url)) {
urlHandler(link.id, link.url, user.id);
return res.status(200).json({
response: "Link is not a webpage to be archived.",
});
}
return res.status(200).json({
response: "Link is being archived.",
});
+7 -9
View File
@@ -41,28 +41,26 @@ export default function ChooseUsername() {
return (
<CenteredForm>
<form onSubmit={submitUsername}>
<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-3xl text-center text-black dark:text-white font-extralight">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Choose a Username
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username
</p>
<p className="text-sm w-fit font-semibold mb-1">Username</p>
<TextInput
autoFocus
placeholder="john"
value={inputedUsername}
className="bg-white"
className="bg-base-100"
onChange={(e) => setInputedUsername(e.target.value)}
/>
</div>
<div>
<p className="text-md text-gray-500 dark:text-gray-400 mt-1">
<p className="text-md text-neutral mt-1">
Feel free to reach out to us at{" "}
<a
className="font-semibold underline"
@@ -83,7 +81,7 @@ export default function ChooseUsername() {
<div
onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
>
Sign Out
</div>
+188 -172
View File
@@ -1,37 +1,32 @@
import Dropdown from "@/components/Dropdown";
import LinkCard from "@/components/LinkCard";
import useCollectionStore from "@/store/collections";
import useLinkStore from "@/store/links";
import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global";
import {
faEllipsis,
faFolder,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { faEllipsis, faFolder } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import ProfilePhoto from "@/components/ProfilePhoto";
import SortDropdown from "@/components/SortDropdown";
import useModalStore from "@/store/modals";
import useLinks from "@/hooks/useLinks";
import usePermissions from "@/hooks/usePermissions";
import NoLinksFound from "@/components/NoLinksFound";
import { useTheme } from "next-themes";
import useLocalSettingsStore from "@/store/localSettings";
import useAccountStore from "@/store/account";
import getPublicUserData from "@/lib/client/getPublicUserData";
import EditCollectionModal from "@/components/ModalContent/EditCollectionModal";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal";
export default function Index() {
const { setModal } = useModalStore();
const { settings } = useLocalSettingsStore();
const router = useRouter();
const { theme } = useTheme();
const { links } = useLinkStore();
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [activeCollection, setActiveCollection] =
@@ -47,185 +42,184 @@ export default function Index() {
);
}, [router, collections]);
const { account } = useAccountStore();
const [collectionOwner, setCollectionOwner] = useState({
id: null as unknown as number,
name: "",
username: "",
image: "",
});
useEffect(() => {
const fetchOwner = async () => {
if (activeCollection && activeCollection.ownerId !== account.id) {
const owner = await getPublicUserData(
activeCollection.ownerId as number
);
setCollectionOwner(owner);
} else if (activeCollection && activeCollection.ownerId === account.id) {
setCollectionOwner({
id: account.id as number,
name: account.name,
username: account.username as string,
image: account.image as string,
});
}
};
fetchOwner();
}, [activeCollection]);
const [editCollectionModal, setEditCollectionModal] = useState(false);
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
return (
<MainLayout>
<div className="p-5 flex flex-col gap-5 w-full h-full">
<div
style={{
backgroundImage: `linear-gradient(-45deg, ${
activeCollection?.color
}30 10%, ${theme === "dark" ? "#262626" : "#f3f4f6"} 50%, ${
theme === "dark" ? "#262626" : "#f9fafb"
} 100%)`,
}}
className="border border-solid border-sky-100 dark:border-neutral-700 rounded-2xl shadow min-h-[10rem] p-5 flex gap-5 flex-col justify-between"
>
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
{activeCollection && (
<div className="flex gap-3 items-center">
<div className="flex gap-2">
<FontAwesomeIcon
icon={faFolder}
style={{ color: activeCollection?.color }}
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 font-thin">
{activeCollection?.name}
</p>
</div>
<div
style={{
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 14rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
className="h-full p-5 flex gap-3 flex-col"
>
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
{activeCollection && (
<div className="flex gap-3 items-center">
<div className="flex gap-2">
<FontAwesomeIcon
icon={faFolder}
style={{ color: activeCollection?.color }}
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
/>
<p className="sm:text-4xl text-3xl capitalize w-full py-1 break-words hyphens-auto font-thin">
{activeCollection?.name}
</p>
</div>
)}
</div>
)}
</div>
{activeCollection ? (
{activeCollection ? (
<div className={`min-w-[15rem]`}>
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
<div
className={`min-w-[15rem] ${
activeCollection.members[1] && "mr-3"
}`}
className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
onClick={() => setEditCollectionSharingModal(true)}
>
<div
onClick={() =>
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: activeCollection,
defaultIndex: permissions === true ? 1 : 0,
})
}
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-center w-fit sm:mr-0 sm:ml-auto cursor-pointer"
>
{activeCollection?.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className={`${
activeCollection.members[1] && "-mr-3"
} border-[3px]`}
/>
);
})
.slice(0, 4)}
{activeCollection?.members.length &&
activeCollection.members.length - 4 > 0 ? (
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700 -mr-3">
+{activeCollection?.members?.length - 4}
{collectionOwner.id ? (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
{activeCollection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className="-ml-3"
name={e.user.name}
/>
);
})
.slice(0, 3)}
{activeCollection.members.length - 3 > 0 ? (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{activeCollection.members.length - 3}</span>
</div>
) : null}
</div>
</div>
) : null}
</div>
<div className="text-black dark:text-white flex justify-between items-end gap-5">
<p>{activeCollection?.description}</p>
<div className="flex items-center gap-2">
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-black hover:dark:bg-white hover:bg-opacity-10 hover:dark:bg-opacity-10 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)}
/>
</div>
) : null}
</div>
<div className="relative">
<p className="text-neutral text-sm font-semibold">
By {collectionOwner.name}
{activeCollection.members.length > 0
? ` and ${activeCollection.members.length} others`
: undefined}
.
</p>
</div>
</div>
) : undefined}
{activeCollection?.description ? (
<p>{activeCollection?.description}</p>
) : undefined}
<div className="divider my-0"></div>
<div className="flex justify-between items-end gap-5">
<p>Showing {activeCollection?._count?.links} results</p>
<div className="flex items-center gap-2">
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
<div className="relative">
<div className="dropdown dropdown-bottom dropdown-end">
<div
onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-black hover:dark:bg-white hover:bg-opacity-10 hover:dark:bg-opacity-10 duration-100 p-1"
tabIndex={0}
role="button"
className="btn btn-ghost btn-sm btn-square text-neutral"
>
<FontAwesomeIcon
icon={faEllipsis}
id="expand-dropdown"
title="More"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
className="w-5 h-5"
/>
</div>
{expandDropdown ? (
<Dropdown
items={[
permissions === true
? {
name: "Edit Collection Info",
onClick: () => {
activeCollection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: activeCollection,
});
setExpandDropdown(false);
},
}
: undefined,
{
name:
permissions === true
? "Share/Collaborate"
: "View Team",
onClick: () => {
activeCollection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: activeCollection,
defaultIndex: permissions === true ? 1 : 0,
});
setExpandDropdown(false);
},
},
{
name:
permissions === true
? "Delete Collection"
: "Leave Collection",
onClick: () => {
activeCollection &&
setModal({
modal: "COLLECTION",
state: true,
method: "UPDATE",
isOwner: permissions === true,
active: activeCollection,
defaultIndex: permissions === true ? 2 : 1,
});
setExpandDropdown(false);
},
},
]}
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "expand-dropdown")
setExpandDropdown(false);
}}
className="absolute top-8 right-0 z-10 w-44"
/>
) : null}
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
{permissions === true ? (
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionModal(true);
}}
>
Edit Collection Info
</div>
</li>
) : undefined}
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setEditCollectionSharingModal(true);
}}
>
{permissions === true
? "Share and Collaborate"
: "View Team"}
</div>
</li>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setDeleteCollectionModal(true);
}}
>
{permissions === true
? "Delete Collection"
: "Leave Collection"}
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
{links
@@ -238,6 +232,28 @@ export default function Index() {
<NoLinksFound />
)}
</div>
{activeCollection ? (
<>
{editCollectionModal ? (
<EditCollectionModal
onClose={() => setEditCollectionModal(false)}
activeCollection={activeCollection}
/>
) : undefined}
{editCollectionSharingModal ? (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={activeCollection}
/>
) : undefined}
{deleteCollectionModal ? (
<DeleteCollectionModal
onClose={() => setDeleteCollectionModal(false)}
activeCollection={activeCollection}
/>
) : undefined}
</>
) : undefined}
</MainLayout>
);
}
+17 -84
View File
@@ -3,32 +3,29 @@ import {
faEllipsis,
faFolder,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import CollectionCard from "@/components/CollectionCard";
import Dropdown from "@/components/Dropdown";
import { useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import { useSession } from "next-auth/react";
import useModalStore from "@/store/modals";
import SortDropdown from "@/components/SortDropdown";
import { Sort } from "@/types/global";
import useSort from "@/hooks/useSort";
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
export default function Collections() {
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortedCollections, setSortedCollections] = useState(collections);
const { data } = useSession();
const { setModal } = useModalStore();
useSort({ sortBy, setData: setSortedCollections, data: collections });
const [newCollectionModal, setNewCollectionModal] = useState(false);
return (
<MainLayout>
<div className="p-5">
@@ -37,77 +34,20 @@ export default function Collections() {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faFolder}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
<p className="text-3xl capitalize font-thin">
Your Collections
</p>
<p className="text-black dark:text-white">
Collections you own
</p>
<p>Collections you own</p>
</div>
</div>
<div className="relative mt-2">
<div
onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faEllipsis}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{expandDropdown ? (
<Dropdown
items={[
{
name: "New Collection",
onClick: () => {
setModal({
modal: "COLLECTION",
state: true,
method: "CREATE",
});
setExpandDropdown(false);
},
},
]}
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "expand-dropdown")
setExpandDropdown(false);
}}
className="absolute top-8 sm:left-0 right-0 sm:right-auto w-36"
/>
) : null}
</div>
</div>
<div className="relative mt-2">
<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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
@@ -119,21 +59,13 @@ export default function Collections() {
})}
<div
className="p-5 bg-gray-50 dark:bg-neutral-800 self-stretch border border-solid border-sky-100 dark:border-neutral-700 min-h-[12rem] rounded-2xl cursor-pointer shadow duration-100 hover:shadow-none flex flex-col gap-4 justify-center items-center group"
onClick={() => {
setModal({
modal: "COLLECTION",
state: true,
method: "CREATE",
});
}}
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content p-5 bg-base-200 self-stretch min-h-[12rem] rounded-2xl cursor-pointer flex flex-col gap-4 justify-center items-center group btn"
onClick={() => setNewCollectionModal(true)}
>
<p className="text-black dark:text-white group-hover:opacity-0 duration-100">
New Collection
</p>
<p className="group-hover:opacity-0 duration-100">New Collection</p>
<FontAwesomeIcon
icon={faPlus}
className="w-8 h-8 text-sky-500 dark:text-sky-500 group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100"
className="w-8 h-8 text-primary group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100"
/>
</div>
</div>
@@ -143,16 +75,14 @@ export default function Collections() {
<div className="flex items-center gap-3 my-5">
<FontAwesomeIcon
icon={faFolder}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
<p className="text-3xl capitalize font-thin">
Other Collections
</p>
<p className="text-black dark:text-white">
Shared collections you&apos;re a member of
</p>
<p>Shared collections you&apos;re a member of</p>
</div>
</div>
@@ -166,6 +96,9 @@ export default function Collections() {
</>
) : undefined}
</div>
{newCollectionModal ? (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined}
</MainLayout>
);
}
+2 -2
View File
@@ -5,12 +5,12 @@ import React from "react";
export default function EmailConfirmaion() {
return (
<CenteredForm>
<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">
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-neutral-content bg-base-200">
<p className="text-center text-2xl sm:text-3xl font-extralight mb-2 ">
Please check your Email
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700 my-3" />
<div className="divider my-3"></div>
<p>A sign in link has been sent to your email address.</p>
+79 -107
View File
@@ -23,8 +23,8 @@ import React from "react";
import useModalStore from "@/store/modals";
import { toast } from "react-hot-toast";
import { MigrationFormat, MigrationRequest } from "@/types/global";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import DashboardItem from "@/components/DashboardItem";
import NewLinkModal from "@/components/ModalContent/NewLinkModal";
export default function Dashboard() {
const { collections } = useCollectionStore();
@@ -63,8 +63,6 @@ export default function Dashboard() {
handleNumberOfLinksToShow();
}, [width]);
const [importDropdown, setImportDropdown] = useState(false);
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
@@ -92,8 +90,6 @@ export default function Dashboard() {
toast.success("Imported the Bookmarks! Reloading the page...");
setImportDropdown(false);
setTimeout(() => {
location.reload();
}, 2000);
@@ -104,35 +100,32 @@ export default function Dashboard() {
}
};
const [newLinkModal, setNewLinkModal] = useState(false);
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faChartSimple}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
Dashboard
</p>
<p className="text-3xl capitalize font-thin">Dashboard</p>
<p className="text-black dark:text-white">
A brief overview of your data
</p>
<p>A brief overview of your data</p>
</div>
</div>
<div>
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-sky-100 dark:border-neutral-700 bg-gray-100 dark:bg-neutral-800">
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
icon={faLink}
/>
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
<div className="h-24 border-1 border-l border-sky-100 dark:border-neutral-700 hidden md:block"></div>
<div className="divider md:divider-horizontal"></div>
<DashboardItem
name={collections.length === 1 ? "Collection" : "Collections"}
@@ -140,8 +133,7 @@ export default function Dashboard() {
icon={faFolder}
/>
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
<div className="h-24 border-1 border-r border-sky-100 dark:border-neutral-700 hidden md:block"></div>
<div className="divider md:divider-horizontal"></div>
<DashboardItem
name={tags.length === 1 ? "Tag" : "Tags"}
@@ -155,21 +147,16 @@ export default function Dashboard() {
<div className="flex gap-2 items-center">
<FontAwesomeIcon
icon={faClockRotateLeft}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
className="w-5 h-5 text-primary drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">
Recently Added Links
</p>
<p className="text-2xl">Recently Added Links</p>
</div>
<Link
href="/links"
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
className="flex items-center gap-2 cursor-pointer"
>
View All
<FontAwesomeIcon
icon={faChevronRight}
className={`w-4 h-4 text-black dark:text-white`}
/>
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
</Link>
</div>
@@ -180,7 +167,7 @@ export default function Dashboard() {
{links[0] ? (
<div className="w-full">
<div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links.slice(0, showLinks).map((e, i) => (
<LinkCard key={i} link={e} count={i} />
@@ -190,101 +177,87 @@ export default function Dashboard() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
View Your Recently Added Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
This section will view your latest added Links across every
Collections you have access to.
</p>
<div className="text-center text-black dark:text-white w-full mt-4 flex flex-wrap gap-4 justify-center">
<div className="text-center w-full mt-4 flex flex-wrap gap-4 justify-center">
<div
onClick={() => {
setModal({
modal: "LINK",
state: true,
method: "CREATE",
});
setNewLinkModal(true);
}}
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group"
className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent text-white group"
>
<FontAwesomeIcon
icon={faPlus}
className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
className="w-5 h-5 left-4 group-hover:ml-[4rem] absolute duration-100"
/>
<span className="group-hover:opacity-0 text-right w-full duration-100">
Create New Link
</span>
</div>
<div className="relative">
<div className="dropdown dropdown-bottom">
<div
onClick={() => setImportDropdown(!importDropdown)}
tabIndex={0}
role="button"
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
id="import-dropdown"
className="flex gap-2 select-none text-sm cursor-pointer p-2 px-3 rounded-md border dark:hover:border-sky-600 text-black border-black dark:text-white dark:border-white hover:border-sky-500 hover:dark:border-sky-500 hover:text-sky-500 hover:dark:text-sky-500 duration-100 group"
>
<FontAwesomeIcon
icon={faFileImport}
className="w-5 h-5 duration-100"
id="import-dropdown"
/>
<span
className="text-right w-full duration-100"
id="import-dropdown"
>
Import Your Bookmarks
</span>
<p>Import From</p>
</div>
{importDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown")
setImportDropdown(false);
}}
className={`absolute text-black dark:text-white top-10 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-linkwarden-file"
title="JSON File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Linkwarden File...
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
<label
htmlFor="import-html-file"
title="HTML File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</div>
</ClickAwayHandler>
) : null}
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-linkwarden-file"
title="JSON File"
>
From Linkwarden
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
</div>
@@ -295,19 +268,16 @@ export default function Dashboard() {
<div className="flex gap-2 items-center">
<FontAwesomeIcon
icon={faThumbTack}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
className="w-5 h-5 text-primary drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">Pinned Links</p>
<p className="text-2xl">Pinned Links</p>
</div>
<Link
href="/links/pinned"
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
className="flex items-center gap-2 cursor-pointer"
>
View All
<FontAwesomeIcon
icon={faChevronRight}
className={`w-4 h-4 text-black dark:text-white`}
/>
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
</Link>
</div>
@@ -318,10 +288,9 @@ export default function Dashboard() {
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
.slice(0, showLinks)}
@@ -330,12 +299,12 @@ export default function Dashboard() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
Pin Your Favorite Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
You can Pin your favorite Links by clicking on the three dots on
each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
@@ -344,6 +313,9 @@ export default function Dashboard() {
)}
</div>
</div>
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</MainLayout>
);
}
+8 -13
View File
@@ -43,34 +43,32 @@ export default function Forgot() {
return (
<CenteredForm>
<form onSubmit={sendConfirmation}>
<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-3xl text-center text-black dark:text-white font-extralight">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Password Recovery
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-black dark:text-white">
<p>
Enter your email so we can send you a link to recover your
account. Make sure to change your password in the profile settings
afterwards.
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-neutral">
You wont get logged in if you haven&apos;t created an account yet.
</p>
</div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email
</p>
<p className="text-sm w-fit font-semibold mb-1">Email</p>
<TextInput
autoFocus
type="email"
placeholder="johnny@example.com"
value={form.email}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
</div>
@@ -82,10 +80,7 @@ export default function Forgot() {
loading={submitLoader}
/>
<div className="flex items-baseline gap-1 justify-center">
<Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
<Link href={"/login"} className="block font-bold">
Go back
</Link>
</div>
+74 -73
View File
@@ -9,14 +9,18 @@ import {
} from "@/types/global";
import Image from "next/image";
import ColorThief, { RGBColor } from "colorthief";
import { useTheme } from "next-themes";
import unescapeString from "@/lib/client/unescapeString";
import isValidUrl from "@/lib/client/isValidUrl";
import isValidUrl from "@/lib/shared/isValidUrl";
import DOMPurify from "dompurify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
import {
faBoxesStacked,
faFolder,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import useModalStore from "@/store/modals";
import { useSession } from "next-auth/react";
import useLocalSettingsStore from "@/store/localSettings";
type LinkContent = {
title: string;
@@ -31,10 +35,11 @@ type LinkContent = {
};
export default function Index() {
const { theme } = useTheme();
const { links, getLink } = useLinkStore();
const { setModal } = useModalStore();
const { settings } = useLocalSettingsStore();
const session = useSession();
const userId = session.data?.user.id;
@@ -117,19 +122,19 @@ export default function Index() {
if (colorPalette && banner && bannerInner) {
if (colorPalette[0] && colorPalette[1]) {
banner.style.background = `linear-gradient(to right, ${rgbToHex(
banner.style.background = `linear-gradient(to bottom, ${rgbToHex(
colorPalette[0][0],
colorPalette[0][1],
colorPalette[0][2]
)}30, ${rgbToHex(
)}20, ${rgbToHex(
colorPalette[1][0],
colorPalette[1][1],
colorPalette[1][2]
)}30)`;
)}20)`;
}
if (colorPalette[2] && colorPalette[3]) {
bannerInner.style.background = `linear-gradient(to left, ${rgbToHex(
bannerInner.style.background = `linear-gradient(to bottom, ${rgbToHex(
colorPalette[2][0],
colorPalette[2][1],
colorPalette[2][2]
@@ -140,23 +145,19 @@ export default function Index() {
)})30`;
}
}
}, [colorPalette, theme]);
}, [colorPalette]);
return (
<LinkLayout>
<div
className={`flex flex-col max-w-screen-md h-full ${
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
}`}
>
<div className={`flex flex-col max-w-screen-md h-full`}>
<div
id="link-banner"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
className="link-banner relative bg-opacity-10 border-neutral-content"
>
<div id="link-banner-inner" className="link-banner-inner"></div>
{/* <div id="link-banner-inner" className="link-banner-inner"></div> */}
<div className={`relative flex flex-col gap-3 items-start`}>
<div className="flex gap-3 items-end">
<div className="flex gap-3 items-start">
{!imageError && link?.url && (
<Image
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
@@ -164,7 +165,7 @@ export default function Index() {
height={42}
alt=""
id={"favicon-" + link.id}
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
className="bg-white shadow rounded-md p-1 select-none mt-1"
draggable="false"
onLoad={(e) => {
try {
@@ -183,92 +184,92 @@ export default function Index() {
}}
/>
)}
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
<p className=" min-w-fit">
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: undefined}
<div className="flex flex-col">
<p className="text-xl">
{unescapeString(link?.name || link?.description || "")}
</p>
{link?.url ? (
<>
<p></p>
<Link
href={link?.url || ""}
title={link?.url}
target="_blank"
className="hover:opacity-60 duration-100 break-all"
>
{isValidUrl(link?.url || "")
? new URL(link?.url as string).host
: undefined}
</Link>
</>
<Link
href={link?.url || ""}
title={link?.url}
target="_blank"
className="hover:opacity-60 duration-100 break-all text-sm flex items-center gap-1 text-neutral w-fit"
>
<FontAwesomeIcon icon={faLink} className="w-4 h-4" />
{isValidUrl(link?.url || "")
? new URL(link?.url as string).host
: undefined}
</Link>
) : undefined}
</div>
</div>
<div className="flex flex-col gap-2">
<p className="capitalize text-2xl sm:text-3xl font-thin">
{unescapeString(link?.name || link?.description || "")}
</p>
<div className="flex gap-1 items-center flex-wrap">
<Link
href={`/collections/${link?.collection.id}`}
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
<div className="flex gap-1 items-center flex-wrap">
<Link
href={`/collections/${link?.collection.id}`}
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
>
<FontAwesomeIcon
icon={faFolder}
className="w-5 h-5 drop-shadow"
style={{ color: link?.collection.color }}
/>
<p
title={link?.collection.name}
className="text-lg truncate max-w-[12rem]"
>
<FontAwesomeIcon
icon={faFolder}
className="w-5 h-5 drop-shadow"
style={{ color: link?.collection.color }}
/>
{link?.collection.name}
</p>
</Link>
{link?.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={link?.collection.name}
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
title={e.name}
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
>
{link?.collection.name}
#{e.name}
</p>
</Link>
{link?.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={e.name}
className="px-2 bg-sky-200 text-black dark:text-white dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
>
{e.name}
</p>
</Link>
))}
</div>
))}
</div>
<p className="min-w-fit text-sm text-neutral">
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: undefined}
</p>
{link?.name ? <p>{link?.description}</p> : undefined}
</div>
</div>
<div className="divider"></div>
<div className="flex flex-col gap-5 h-full">
{link?.readabilityPath?.startsWith("archives") ? (
<div
className="line-break px-3 reader-view"
className="line-break px-1 reader-view"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
}}
></div>
) : (
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
<div className="border border-solid border-neutral-content w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-base-200">
{link?.readabilityPath === "pending" ? (
<p className="text-center">
Generating readable format, please wait...
</p>
) : (
<>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
There is no reader view for this webpage
</p>
<p className="text-center text-sm text-black dark:text-white">
<p className="text-center text-sm">
{link?.collection.ownerId === userId
? "You can update (refetch) the preserved formats by managing them below"
: "The collections owners can refetch the preserved formats"}
+5 -28
View File
@@ -5,14 +5,13 @@ import useLinks from "@/hooks/useLinks";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import { Sort } from "@/types/global";
import { faLink, faSort } from "@fortawesome/free-solid-svg-icons";
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
export default function Links() {
const { links } = useLinkStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ sort: sortBy });
@@ -24,39 +23,17 @@ export default function Links() {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faLink}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
All Links
</p>
<p className="text-3xl capitalize font-thin">All Links</p>
<p className="text-black dark:text-white">
Links from every Collections
</p>
<p>Links from every Collections</p>
</div>
</div>
<div className="relative mt-2">
<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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
{links[0] ? (
+8 -32
View File
@@ -1,18 +1,16 @@
import LinkCard from "@/components/LinkCard";
import NoLinksFound from "@/components/NoLinksFound";
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 { faSort, faThumbTack } from "@fortawesome/free-solid-svg-icons";
import { faThumbTack } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
export default function PinnedLinks() {
const { links } = useLinkStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ sort: sortBy, pinnedOnly: true });
@@ -24,39 +22,17 @@ export default function PinnedLinks() {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faThumbTack}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
Pinned Links
</p>
<p className="text-3xl capitalize font-thin">Pinned Links</p>
<p className="text-black dark:text-white">
Pinned Links from your Collections
</p>
<p>Pinned Links from your Collections</p>
</div>
</div>
<div className="relative mt-2">
<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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
@@ -68,12 +44,12 @@ export default function PinnedLinks() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
Pin Your Favorite Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
You can Pin your favorite Links by clicking on the three dots on
each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
+10 -21
View File
@@ -76,18 +76,17 @@ export default function Login() {
return (
<CenteredForm text="Sign in to your account">
<form onSubmit={loginUser}>
<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">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
{process.env.NEXT_PUBLIC_DISABLE_LOGIN !== "true" ? (
<div>
<p className="text-3xl text-black dark:text-white text-center font-extralight">
<p className="text-3xl text-center font-extralight">
Enter your credentials
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
<p className="text-sm w-fit font-semibold mb-1">
Username
{emailEnabled ? " or Email" : undefined}
</p>
@@ -96,29 +95,24 @@ export default function Login() {
autoFocus={true}
placeholder="johnny"
value={form.username}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, username: e.target.value })}
/>
</div>
<div className="w-full">
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Password
</p>
<p className="text-sm w-fit font-semibold mb-1">Password</p>
<TextInput
type="password"
placeholder="••••••••••••••"
value={form.password}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
{emailEnabled && (
<div className="w-fit ml-auto mt-1">
<Link
href={"/forgot"}
className="text-gray-500 dark:text-gray-400 font-semibold"
>
<Link href={"/forgot"} className="text-neutral font-semibold">
Forgot Password?
</Link>
</div>
@@ -154,13 +148,8 @@ export default function Login() {
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
"true" ? undefined : (
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500 dark:text-gray-400">
New here?
</p>
<Link
href={"/register"}
className="block text-black dark:text-white font-semibold"
>
<p className="w-fit text-neutral">New here?</p>
<Link href={"/register"} className="block font-semibold">
Sign Up
</Link>
</div>
+63 -104
View File
@@ -9,18 +9,16 @@ import Head from "next/head";
import useLinks from "@/hooks/useLinks";
import useLinkStore from "@/store/links";
import ProfilePhoto from "@/components/ProfilePhoto";
import useModalStore from "@/store/modals";
import ModalManagement from "@/components/ModalManagement";
import ToggleDarkMode from "@/components/ToggleDarkMode";
import { useTheme } from "next-themes";
import getPublicUserData from "@/lib/client/getPublicUserData";
import Image from "next/image";
import Link from "next/link";
import PublicSearchBar from "@/components/PublicPage/PublicSearchBar";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFilter, faSort } from "@fortawesome/free-solid-svg-icons";
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
import SortDropdown from "@/components/SortDropdown";
import useLocalSettingsStore from "@/store/localSettings";
import SearchBar from "@/components/SearchBar";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
const cardVariants: Variants = {
offscreen: {
@@ -38,15 +36,8 @@ const cardVariants: Variants = {
export default function PublicCollections() {
const { links } = useLinkStore();
const { modal, setModal } = useModalStore();
useEffect(() => {
modal
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "auto");
}, [modal]);
const { theme } = useTheme();
const { settings } = useLocalSettingsStore();
const router = useRouter();
@@ -65,8 +56,6 @@ export default function PublicCollections() {
tags: true,
});
const [filterDropdown, setFilterDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({
@@ -101,13 +90,16 @@ export default function PublicCollections() {
fetchOwner();
}, [collection]);
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
return collection ? (
<div
className="h-screen"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
theme === "dark" ? "#262626" : "#f3f4f6"
} 50%, ${theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
<ModalManagement />
@@ -128,63 +120,57 @@ export default function PublicCollections() {
{collection.name}
</p>
<div className="flex gap-2 items-center mt-8 min-w-fit">
<ToggleDarkMode className="w-8 h-8 flex" />
<ToggleDarkMode />
<Link href="https://linkwarden.app/" target="_blank">
<Image
src={`/icon.png`}
width={551}
height={551}
alt="Linkwarden"
title="Linkwarden"
className="h-8 w-fit mx-auto"
title="Created with Linkwarden"
className="h-8 w-fit mx-auto rounded"
/>
</Link>
</div>
</div>
<div>
<div className="mt-3">
<div className={`min-w-[15rem]`}>
<div
onClick={() =>
setModal({
modal: "COLLECTION",
state: true,
method: "VIEW_TEAM",
isOwner: false,
active: collection,
defaultIndex: 0,
})
}
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-start w-fit cursor-pointer"
>
{collectionOwner.id ? (
<ProfilePhoto
src={
collectionOwner.image ? collectionOwner.image : undefined
}
className={`w-8 h-8 border-2`}
/>
) : undefined}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className={`w-8 h-8 border-2`}
/>
);
})
.slice(0, 3)}
{collection?.members.length &&
collection.members.length - 3 > 0 ? (
<div className="w-8 h-8 min-w-[2rem] text-white text-sm flex items-center justify-center rounded-full border-2 bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700">
+{collection?.members?.length - 3}
</div>
) : null}
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
<div
className="flex items-center btn px-2 btn-ghost rounded-full"
onClick={() => setEditCollectionSharingModal(true)}
>
{collectionOwner.id ? (
<ProfilePhoto
src={collectionOwner.image || undefined}
name={collectionOwner.name}
/>
) : undefined}
{collection.members
.sort((a, b) => (a.userId as number) - (b.userId as number))
.map((e, i) => {
return (
<ProfilePhoto
key={i}
src={e.user.image ? e.user.image : undefined}
className="-ml-3"
name={e.user.name}
/>
);
})
.slice(0, 3)}
{collection.members.length - 3 > 0 ? (
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
<span>+{collection.members.length - 3}</span>
</div>
</div>
) : null}
</div>
<p className="ml-2 mt-1 text-gray-500 dark:text-gray-300">
<p className="text-neutral text-sm font-semibold">
By {collectionOwner.name}
{collection.members.length > 0
? ` and ${collection.members.length} others`
@@ -197,57 +183,24 @@ export default function PublicCollections() {
<p className="mt-5">{collection.description}</p>
<hr className="mt-5 border-1 border-neutral-500" />
<div className="divider mt-5 mb-0"></div>
<div className="flex mb-5 mt-10 flex-col gap-5">
<div className="flex justify-between">
<PublicSearchBar
placeHolder={`Search ${collection._count?.links} Links`}
<SearchBar
placeholder={`Search ${collection._count?.links} Links`}
/>
<div className="flex gap-3 items-center">
<div className="flex gap-2 items-center">
<div className="relative">
<div
onClick={() => setFilterDropdown(!filterDropdown)}
id="filter-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 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}
<FilterSearchDropdown
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
</div>
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
</div>
@@ -271,11 +224,17 @@ export default function PublicCollections() {
})}
</div>
{/* <p className="text-center text-gray-500">
{/* <p className="text-center text-neutral">
List created with <span className="text-black">Linkwarden.</span>
</p> */}
</div>
</div>
{editCollectionSharingModal ? (
<EditCollectionSharingModal
onClose={() => setEditCollectionSharingModal(false)}
activeCollection={collection}
/>
) : undefined}
</div>
) : (
<></>
+20 -19
View File
@@ -9,14 +9,14 @@ import {
} from "@/types/global";
import Image from "next/image";
import ColorThief, { RGBColor } from "colorthief";
import { useTheme } from "next-themes";
import unescapeString from "@/lib/client/unescapeString";
import isValidUrl from "@/lib/client/isValidUrl";
import isValidUrl from "@/lib/shared/isValidUrl";
import DOMPurify from "dompurify";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
import useModalStore from "@/store/modals";
import { useSession } from "next-auth/react";
import useLocalSettingsStore from "@/store/localSettings";
type LinkContent = {
title: string;
@@ -31,10 +31,11 @@ type LinkContent = {
};
export default function Index() {
const { theme } = useTheme();
const { links, getLink } = useLinkStore();
const { setModal } = useModalStore();
const { settings } = useLocalSettingsStore();
const session = useSession();
const userId = session.data?.user.id;
@@ -140,18 +141,18 @@ export default function Index() {
)})30`;
}
}
}, [colorPalette, theme]);
}, [colorPalette]);
return (
<LinkLayout>
<div
className={`flex flex-col max-w-screen-md h-full ${
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
settings.theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
}`}
>
<div
id="link-banner"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-neutral-content shadow-md"
>
<div id="link-banner-inner" className="link-banner-inner"></div>
@@ -164,7 +165,7 @@ export default function Index() {
height={42}
alt=""
id={"favicon-" + link.id}
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-base-100 bg-base-100 aspect-square"
draggable="false"
onLoad={(e) => {
try {
@@ -184,7 +185,7 @@ export default function Index() {
/>
)}
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
<div className="flex gap-2 text-sm text-neutral">
<p className=" min-w-fit">
{link?.createdAt
? new Date(link?.createdAt).toLocaleString("en-US", {
@@ -229,19 +230,19 @@ export default function Index() {
/>
<p
title={link?.collection?.name}
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
className="text-lg truncate max-w-[12rem]"
>
{link?.collection?.name}
</p>
</Link>
{link?.tags.map((e, i) => (
<Link key={i} href={`/tags/${e.id}`} className="z-10">
<p
title={e.name}
className="px-2 py-1 bg-sky-200 text-black dark:text-white dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
>
{e.name}
</p>
<Link
key={i}
href={"/public/collections/20?q=" + e.name}
title={e.name}
className="z-10 btn btn-xs btn-ghost truncate max-w-[19rem]"
>
#{e.name}
</Link>
))}
</div>
@@ -258,17 +259,17 @@ export default function Index() {
}}
></div>
) : (
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
<div className="border border-solid border-neutral-content w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-base-200">
{link?.readabilityPath === "pending" ? (
<p className="text-center">
Generating readable format, please wait...
</p>
) : (
<>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
There is no reader view for this webpage
</p>
<p className="text-center text-sm text-black dark:text-white">
<p className="text-center text-sm">
{link?.collection?.ownerId === userId
? "You can update (refetch) the preserved formats by managing them below"
: "The collections owners can refetch the preserved formats"}
+18 -31
View File
@@ -104,7 +104,7 @@ export default function Register() {
}
>
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
<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">
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p>
Registration is disabled for this instance, please contact the admin
in case of any issues.
@@ -112,37 +112,33 @@ export default function Register() {
</div>
) : (
<form onSubmit={registerUser}>
<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-3xl text-black dark:text-white text-center font-extralight">
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Enter your details
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Display Name
</p>
<p className="text-sm w-fit font-semibold mb-1">Display Name</p>
<TextInput
autoFocus={true}
placeholder="Johnny"
value={form.name}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
</div>
{emailEnabled ? undefined : (
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username
</p>
<p className="text-sm w-fit font-semibold mb-1">Username</p>
<TextInput
placeholder="john"
value={form.username}
className="bg-white"
className="bg-base-100"
onChange={(e) =>
setForm({ ...form, username: e.target.value })
}
@@ -152,36 +148,32 @@ export default function Register() {
{emailEnabled ? (
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email
</p>
<p className="text-sm w-fit font-semibold mb-1">Email</p>
<TextInput
type="email"
placeholder="johnny@example.com"
value={form.email}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
</div>
) : undefined}
<div className="w-full">
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Password
</p>
<p className="text-sm w-fit font-semibold mb-1">Password</p>
<TextInput
type="password"
placeholder="••••••••••••••"
value={form.password}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
</div>
<div className="w-full">
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
<p className="text-sm w-fit font-semibold mb-1">
Confirm Password
</p>
@@ -189,7 +181,7 @@ export default function Register() {
type="password"
placeholder="••••••••••••••"
value={form.passwordConfirmation}
className="bg-white"
className="bg-base-100"
onChange={(e) =>
setForm({ ...form, passwordConfirmation: e.target.value })
}
@@ -198,7 +190,7 @@ export default function Register() {
{process.env.NEXT_PUBLIC_STRIPE ? (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-neutral">
By signing up, you agree to our{" "}
<Link
href="https://linkwarden.app/tos"
@@ -215,7 +207,7 @@ export default function Register() {
</Link>
.
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-neutral">
Need help?{" "}
<Link
href="mailto:support@linkwarden.app"
@@ -235,13 +227,8 @@ export default function Register() {
<p className="text-center w-full font-bold">Sign Up</p>
</button>
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500 dark:text-gray-400">
Already have an account?
</p>
<Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
<p className="w-fit text-neutral">Already have an account?</p>
<Link href={"/login"} className="block font-bold">
Login
</Link>
</div>
+9 -43
View File
@@ -24,7 +24,6 @@ export default function Search() {
});
const [filterDropdown, setFilterDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({
@@ -45,57 +44,24 @@ export default function Search() {
<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"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary drop-shadow"
/>
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
<p className="sm:text-4xl text-3xl capitalize font-thin">
Search Results
</p>
</div>
</div>
<div className="flex gap-3 items-center">
<div className="flex gap-2 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}
<FilterSearchDropdown
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
</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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
</div>
@@ -106,7 +72,7 @@ export default function Search() {
})}
</div>
) : (
<p className="text-black dark:text-white">
<p>
Nothing found.{" "}
<span className="font-bold text-xl" title="Shruggie">
¯\_()_/¯
+78 -90
View File
@@ -153,37 +153,40 @@ export default function Account() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Account Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-5">
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="flex flex-col gap-3">
<div>
<p className="text-black dark:text-white mb-2">Display Name</p>
<p className="mb-2">Display Name</p>
<TextInput
value={user.name || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, name: e.target.value })}
/>
</div>
<div>
<p className="text-black dark:text-white mb-2">Username</p>
<p className="mb-2">Username</p>
<TextInput
value={user.username || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, username: e.target.value })}
/>
</div>
{emailEnabled ? (
<div>
<p className="text-black dark:text-white mb-2">Email</p>
<p className="mb-2">Email</p>
{user.email !== account.email &&
process.env.NEXT_PUBLIC_STRIPE === "true" ? (
<p className="text-gray-500 dark:text-gray-400 mb-2 text-sm">
<p className="text-neutral mb-2 text-sm">
Updating this field will change your billing email as well
</p>
) : undefined}
<TextInput
value={user.email || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
</div>
@@ -191,14 +194,12 @@ export default function Account() {
</div>
<div className="sm:row-span-2 sm:justify-self-center mx-auto my-3">
<p className="text-black dark:text-white mb-2 text-center">
Profile Photo
</p>
<p className="mb-2 text-center">Profile Photo</p>
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<ProfilePhoto
priority={true}
src={user.image ? user.image : undefined}
className="h-auto border-none w-28"
dimensionClass="w-28 h-28"
/>
{user.image && (
<div
@@ -208,13 +209,13 @@ export default function Account() {
image: "",
})
}
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"
className="absolute top-1 left-1 btn btn-xs btn-circle btn-neutral btn-outline bg-base-100"
>
<FontAwesomeIcon icon={faClose} className="w-3 h-3" />
</div>
)}
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
<label className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
<label className="btn btn-xs btn-neutral btn-outline bg-base-100">
Browse...
<input
type="file"
@@ -232,85 +233,74 @@ export default function Account() {
<div>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
<p className="truncate w-full pr-7 text-3xl font-thin">
Import & Export
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex gap-3 flex-col">
<div>
<p className="text-black dark:text-white mb-2">
Import your data from other platforms.
</p>
<div
onClick={() => setImportDropdown(true)}
className="w-fit relative"
id="import-dropdown"
>
<p className="mb-2">Import your data from other platforms.</p>
<div className="dropdown dropdown-bottom">
<div
tabIndex={0}
role="button"
className="flex gap-2 text-sm btn btn-outline btn-neutral btn-xs"
id="import-dropdown"
className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600"
>
Import From
</div>
{importDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown")
setImportDropdown(false);
}}
className={`absolute top-7 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-linkwarden-file"
title="JSON File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Linkwarden File...
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
<label
htmlFor="import-html-file"
title="HTML File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</div>
</ClickAwayHandler>
) : null}
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-linkwarden-file"
title="JSON File"
>
From Linkwarden
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
<div>
<p className="text-black dark:text-white mb-2">
Download your data instantly.
</p>
<p className="mb-2">Download your data instantly.</p>
<Link className="w-fit" href="/api/v1/migration">
<div className="border w-fit border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
<div className="btn btn-outline btn-neutral btn-xs">
Export Data
</div>
</Link>
@@ -320,12 +310,12 @@ export default function Account() {
<div>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
<p className="truncate w-full pr-7 text-3xl font-thin">
Profile Visibility
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<Checkbox
label="Make profile private"
@@ -333,21 +323,19 @@ export default function Account() {
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/>
<p className="text-gray-500 dark:text-gray-300 text-sm">
<p className="text-neutral text-sm">
This will limit who can find and add you to new Collections.
</p>
{user.isPrivate && (
<div className="pl-5">
<p className="text-black dark:text-white mt-2">
Whitelisted Users
</p>
<p className="text-gray-500 dark:text-gray-300 text-sm mb-3">
<p className="mt-2">Whitelisted Users</p>
<p className="text-neutral text-sm mb-3">
Please provide the Username of the users you wish to grant
visibility to your profile. Separated by comma.
</p>
<textarea
className="w-full resize-none border rounded-md duration-100 bg-gray-50 dark:bg-neutral-950 p-2 outline-none border-sky-100 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600"
className="w-full resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
placeholder="Your profile is hidden from everyone right now..."
value={whitelistedUsersTextbox}
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
@@ -370,7 +358,7 @@ export default function Account() {
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p>
This will permanently delete ALL the Links, Collections, Tags, and
@@ -381,14 +369,14 @@ export default function Account() {
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>
<Link
href="/settings/delete"
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 bg-red-500 hover:bg-red-400 cursor-pointer"
>
<p className="text-center w-full">Delete Your Account</p>
</Link>
</div>
</SettingsLayout>
);
+3 -3
View File
@@ -56,10 +56,10 @@ export default function Api() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">API Keys (Soon)</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<div className="badge bg-orange-500 rounded-md border border-black w-fit px-2 text-black">
<div className="badge badge-warning rounded-md w-fit p-4">
Status: Under Development
</div>
@@ -67,7 +67,7 @@ export default function Api() {
<p>
For now, you can <i>temporarily</i> use your{" "}
<code className="text-xs whitespace-nowrap bg-gray-500/40 rounded-md px-2 py-1">
<code className="text-xs whitespace-nowrap bg-black/40 rounded-md px-2 py-1">
next-auth.session-token
</code>{" "}
in your browser cookies as the API key for your integrations.
+16 -55
View File
@@ -1,23 +1,18 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useTheme } from "next-themes";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
import { useState, useEffect } from "react";
import { faClose } from "@fortawesome/free-solid-svg-icons";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
import { resizeImage } from "@/lib/client/resizeImage";
import ProfilePhoto from "@/components/ProfilePhoto";
import SubmitButton from "@/components/SubmitButton";
import React from "react";
import Checkbox from "@/components/Checkbox";
import LinkPreview from "@/components/LinkPreview";
import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() {
const { theme, setTheme } = useTheme();
const { updateSettings } = useLocalSettingsStore();
const submit = async () => {
setSubmitLoader(true);
@@ -70,80 +65,46 @@ export default function Appearance() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Appearance</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-5">
<div>
<p className="mb-3">Select Theme</p>
<div className="flex gap-3 w-full">
<div
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
theme === "dark"
? "dark:outline-sky-500 text-sky-500"
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
localStorage.getItem("theme") === "dark"
? "dark:outline-primary text-primary"
: "text-white"
}`}
onClick={() => setTheme("dark")}
onClick={() => updateSettings({ theme: "dark" })}
>
<FontAwesomeIcon icon={faMoon} className="w-1/2 h-1/2" />
<p className="text-2xl">Dark Theme</p>
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
<div
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
theme === "light"
? "outline-sky-500 text-sky-500"
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
localStorage.getItem("theme") === "light"
? "outline-primary text-primary"
: "text-black"
}`}
onClick={() => setTheme("light")}
onClick={() => updateSettings({ theme: "light" })}
>
<FontAwesomeIcon icon={faSun} className="w-1/2 h-1/2" />
<p className="text-2xl">Light Theme</p>
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
</div>
</div>
</div>
<div>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
Link Card
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<Checkbox
label="Display Icons"
state={user.displayLinkIcons}
onClick={() =>
setUser({ ...user, displayLinkIcons: !user.displayLinkIcons })
}
/>
{user.displayLinkIcons ? (
<Checkbox
label="Blurred"
className="pl-5 mt-1"
state={user.blurredFavicons}
onClick={() =>
setUser({ ...user, blurredFavicons: !user.blurredFavicons })
}
/>
) : undefined}
<p className="my-3">Preview:</p>
<LinkPreview
settings={{
blurredFavicons: user.blurredFavicons,
displayLinkIcons: user.displayLinkIcons,
}}
/>
</div>
<SubmitButton
{/* <SubmitButton
onClick={submit}
loading={submitLoader}
label="Save"
className="mt-2 mx-auto lg:mx-0"
/>
/> */}
</div>
</SettingsLayout>
);
+1 -1
View File
@@ -59,7 +59,7 @@ export default function Archive() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Archive Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p>Formats to Archive webpages:</p>
<div className="p-3">
+3 -3
View File
@@ -13,10 +13,10 @@ export default function Billing() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Billing Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="w-full mx-auto flex flex-col gap-3 justify-between">
<p className="text-md text-black dark:text-white">
<p className="text-md">
To manage/cancel your subscription, visit the{" "}
<a
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
@@ -27,7 +27,7 @@ export default function Billing() {
.
</p>
<p className="text-md text-black dark:text-white">
<p className="text-md">
If you still need help or encountered any issues, feel free to reach
out to us at:{" "}
<a
+15 -13
View File
@@ -7,7 +7,7 @@ import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
export default function Password() {
export default function Delete() {
const [password, setPassword] = useState("");
const [comment, setComment] = useState<string>();
const [feedback, setFeedback] = useState<string>();
@@ -54,12 +54,15 @@ export default function Password() {
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">
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<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"
className="absolute top-4 left-4 btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
<FontAwesomeIcon
icon={faChevronLeft}
className="w-5 h-5 text-neutral"
/>
</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">
@@ -67,7 +70,7 @@ export default function Password() {
</p>
</div>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<p>
This will permanently delete all the Links, Collections, Tags, and
@@ -80,22 +83,21 @@ export default function Password() {
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? (
<div>
<p className="mb-2 text-black dark:text-white">
Confirm Your Password
</p>
<p className="mb-2">Confirm Your Password</p>
<TextInput
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••••"
className="bg-base-100"
type="password"
/>
</div>
) : undefined}
{process.env.NEXT_PUBLIC_STRIPE ? (
<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">
<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>Optional</b>{" "}
<i className="min-[390px]:text-sm text-xs">
(but it really helps us improve!)
@@ -104,7 +106,7 @@ export default function Password() {
<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"
className="rounded-md p-1 outline-none"
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
>
@@ -120,7 +122,7 @@ export default function Password() {
</select>
</label>
<div>
<p className="text-sm mb-2 text-black dark:text-white">
<p className="text-sm mb-2">
More information (the more details, the more helpful it&apos;d
be)
</p>
@@ -129,7 +131,7 @@ export default function Password() {
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"
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-100 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
/>
</div>
</fieldset>
+5 -3
View File
@@ -47,26 +47,28 @@ export default function Password() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Change Password</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p className="mb-3">
To change your password, please fill out the following. Your password
should be at least 8 characters.
</p>
<div className="w-full flex flex-col gap-2 justify-between">
<p className="text-black dark:text-white">New Password</p>
<p>New Password</p>
<TextInput
value={newPassword}
className="bg-base-200"
onChange={(e) => setNewPassword1(e.target.value)}
placeholder="••••••••••••••"
type="password"
/>
<p className="text-black dark:text-white">Confirm New Password</p>
<p>Confirm New Password</p>
<TextInput
value={newPassword2}
className="bg-base-200"
onChange={(e) => setNewPassword2(e.target.value)}
placeholder="••••••••••••••"
type="password"
+9 -11
View File
@@ -30,12 +30,12 @@ export default function Subscribe() {
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
}-day free trial, cancel anytime!`}
>
<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">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="sm:text-3xl text-2xl text-center font-extralight">
Subscribe to Linkwarden!
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p>
@@ -47,10 +47,10 @@ export default function Subscribe() {
</p>
</div>
<div className="flex text-white dark:text-black gap-3 border border-solid border-sky-100 dark:border-neutral-700 w-4/5 mx-auto p-1 rounded-xl relative">
<div className="flex text-white dark:text-black gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
<button
onClick={() => setPlan(Plan.monthly)}
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
className={`w-full duration-100 text-sm rounded-lg p-1 ${
plan === Plan.monthly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
@@ -61,7 +61,7 @@ export default function Subscribe() {
<button
onClick={() => setPlan(Plan.yearly)}
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
className={`w-full duration-100 text-sm rounded-lg p-1 ${
plan === Plan.yearly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
@@ -77,15 +77,13 @@ export default function Subscribe() {
<div className="flex flex-col gap-2 justify-center items-center">
<p className="text-3xl">
${plan === Plan.monthly ? "4" : "3"}
<span className="text-base text-gray-500 dark:text-gray-400">
/mo
</span>
<span className="text-base text-neutral">/mo</span>
</p>
<p className="font-semibold">
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
</p>
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700">
<legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl">
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-neutral-content">
<legend className="w-fit font-extralight px-2 border border-neutral rounded-md text-xl">
Total
</legend>
@@ -108,7 +106,7 @@ export default function Subscribe() {
<div
onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
>
Sign Out
</div>
+46 -38
View File
@@ -4,7 +4,6 @@ import {
faCheck,
faEllipsis,
faHashtag,
faSort,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -24,7 +23,6 @@ export default function Index() {
const { links } = useLinkStore();
const { tags, updateTag, removeTag } = useTagStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [expandDropdown, setExpandDropdown] = useState(false);
@@ -107,7 +105,7 @@ export default function Index() {
<div className="flex gap-2 items-end font-thin">
<FontAwesomeIcon
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-primary"
/>
{renameTag ? (
<>
@@ -115,50 +113,78 @@ export default function Index() {
<input
type="text"
autoFocus
className="sm:text-4xl text-3xl capitalize text-black dark:text-white bg-transparent h-10 w-3/4 outline-none border-b border-b-sky-100 dark:border-b-neutral-700"
className="sm:text-4xl text-3xl capitalize bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
/>
<div
onClick={() => submit()}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
className="btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon
icon={faCheck}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
className="w-5 h-5 text-neutral"
/>
</div>
<div
onClick={() => cancelUpdateTag()}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
className="btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon
icon={faXmark}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
className="w-5 h-5 text-neutral"
/>
</div>
</form>
</>
) : (
<>
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
<p className="sm:text-4xl text-3xl capitalize">
{activeTag?.name}
</p>
<div className="relative">
<div
onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faEllipsis}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
<div className="dropdown dropdown-bottom font-normal">
<div
tabIndex={0}
role="button"
className="btn btn-ghost btn-sm btn-square text-neutral"
>
<FontAwesomeIcon
icon={faEllipsis}
title="More"
className="w-5 h-5"
/>
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-36 mt-1">
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setRenameTag(true);
}}
>
Rename Tag
</div>
</li>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
remove();
}}
>
Remove Tag
</div>
</li>
</ul>
</div>
{expandDropdown ? (
@@ -194,25 +220,7 @@ export default function Index() {
</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}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">