Merge branch 'feat/extra-login-providers' into main
This commit is contained in:
+10
-20
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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're a member of
|
||||
</p>
|
||||
<p>Shared collections you're a member of</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,6 +96,9 @@ export default function Collections() {
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
{newCollectionModal ? (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
) : undefined}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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'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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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'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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user