Merge branch 'feat/extra-login-providers' into main
This commit is contained in:
commit
8ba2cecf06
|
@ -14,6 +14,7 @@ AUTOSCROLL_TIMEOUT=
|
||||||
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
NEXT_PUBLIC_DISABLE_REGISTRATION=
|
||||||
NEXT_PUBLIC_DISABLE_LOGIN=
|
NEXT_PUBLIC_DISABLE_LOGIN=
|
||||||
RE_ARCHIVE_LIMIT=
|
RE_ARCHIVE_LIMIT=
|
||||||
|
NEXT_PUBLIC_MAX_UPLOAD_SIZE=
|
||||||
|
|
||||||
# AWS S3 Settings
|
# AWS S3 Settings
|
||||||
SPACES_KEY=
|
SPACES_KEY=
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -9,7 +9,7 @@ type Props = {
|
||||||
|
|
||||||
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
|
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed w-full z-20 dark:bg-neutral-900 bg-white">
|
<div className="fixed w-full z-20 bg-base-200">
|
||||||
<div className="w-full h-10 rainbow flex items-center justify-center">
|
<div className="w-full h-10 rainbow flex items-center justify-center">
|
||||||
<div className="w-fit font-semibold">
|
<div className="w-fit font-semibold">
|
||||||
🎉️{" "}
|
🎉️{" "}
|
||||||
|
|
|
@ -12,23 +12,17 @@ type Props = {
|
||||||
export default function Checkbox({ label, state, className, onClick }: Props) {
|
export default function Checkbox({ label, state, className, onClick }: Props) {
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`cursor-pointer flex items-center gap-2 ${className || ""}`}
|
className={`label cursor-pointer flex gap-2 justify-start ${
|
||||||
|
className || ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={state}
|
checked={state}
|
||||||
onChange={onClick}
|
onChange={onClick}
|
||||||
className="peer sr-only"
|
className="checkbox checkbox-primary"
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<span className="label-text">{label}</span>
|
||||||
icon={faSquareCheck}
|
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
|
|
||||||
/>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSquare}
|
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
|
|
||||||
/>
|
|
||||||
<span className="rounded select-none">{label}</span>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,30 +2,25 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faEllipsis, faGlobe, faLink } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsis, faGlobe, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||||
import Dropdown from "./Dropdown";
|
import { useEffect, useState } from "react";
|
||||||
import { useState } from "react";
|
|
||||||
import ProfilePhoto from "./ProfilePhoto";
|
import ProfilePhoto from "./ProfilePhoto";
|
||||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import { useTheme } from "next-themes";
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
|
import useAccountStore from "@/store/account";
|
||||||
|
import EditCollectionModal from "./ModalContent/EditCollectionModal";
|
||||||
|
import EditCollectionSharingModal from "./ModalContent/EditCollectionSharingModal";
|
||||||
|
import DeleteCollectionModal from "./ModalContent/DeleteCollectionModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collection: CollectionIncludingMembersAndLinkCount;
|
collection: CollectionIncludingMembersAndLinkCount;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DropdownTrigger =
|
|
||||||
| {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
| false;
|
|
||||||
|
|
||||||
export default function CollectionCard({ collection, className }: Props) {
|
export default function CollectionCard({ collection, className }: Props) {
|
||||||
const { setModal } = useModalStore();
|
const { settings } = useLocalSettingsStore();
|
||||||
|
const { account } = useAccountStore();
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
|
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
|
||||||
"en-US",
|
"en-US",
|
||||||
|
@ -36,144 +31,183 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
|
||||||
|
|
||||||
const permissions = usePermissions(collection.id as number);
|
const permissions = usePermissions(collection.id as number);
|
||||||
|
|
||||||
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
|
id: null as unknown as number,
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
image: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOwner = async () => {
|
||||||
|
if (collection && collection.ownerId !== account.id) {
|
||||||
|
const owner = await getPublicUserData(collection.ownerId as number);
|
||||||
|
setCollectionOwner(owner);
|
||||||
|
} else if (collection && collection.ownerId === account.id) {
|
||||||
|
setCollectionOwner({
|
||||||
|
id: account.id as number,
|
||||||
|
name: account.name,
|
||||||
|
username: account.username as string,
|
||||||
|
image: account.image as string,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOwner();
|
||||||
|
}, [collection]);
|
||||||
|
|
||||||
|
const [editCollectionModal, setEditCollectionModal] = useState(false);
|
||||||
|
const [editCollectionSharingModal, setEditCollectionSharingModal] =
|
||||||
|
useState(false);
|
||||||
|
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="relative">
|
||||||
|
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-20">
|
||||||
|
<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-[1] 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
|
||||||
|
className="flex items-center absolute bottom-3 left-3 z-10 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}
|
||||||
|
name={e.user.name}
|
||||||
|
className="-ml-3"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.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>
|
||||||
|
<Link
|
||||||
|
href={`/collections/${collection.id}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
||||||
theme === "dark" ? "#262626" : "#f3f4f6"
|
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||||
} 50%, ${theme === "dark" ? "#262626" : "#f9fafb"} 100%)`,
|
} 50%, ${
|
||||||
|
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||||
|
} 100%)`,
|
||||||
}}
|
}}
|
||||||
className={`border border-solid border-sky-100 dark:border-neutral-700 self-stretch min-h-[12rem] rounded-2xl shadow duration-100 hover:shadow-none hover:opacity-80 group relative ${
|
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
|
||||||
className || ""
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="card-body flex flex-col justify-between min-h-[12rem]">
|
||||||
onClick={(e) => setExpandDropdown({ x: e.clientX, y: e.clientY })}
|
<div className="flex justify-between">
|
||||||
id={"expand-dropdown" + collection.id}
|
<p className="card-title break-words line-clamp-2 w-full">
|
||||||
className="inline-flex absolute top-5 right-5 rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
{collection.name}
|
||||||
>
|
</p>
|
||||||
<FontAwesomeIcon
|
<div className="w-8 h-8 ml-10"></div>
|
||||||
icon={faEllipsis}
|
</div>
|
||||||
id={"expand-dropdown" + collection.id}
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
<div className="flex justify-end items-center">
|
||||||
/>
|
<div className="text-right">
|
||||||
</div>
|
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
||||||
<Link
|
|
||||||
href={`/collections/${collection.id}`}
|
|
||||||
className="flex flex-col gap-2 justify-between min-h-[12rem] h-full select-none p-5"
|
|
||||||
>
|
|
||||||
<p className="text-2xl capitalize text-black dark:text-white break-words line-clamp-3 w-4/5">
|
|
||||||
{collection.name}
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="flex items-center w-full">
|
|
||||||
{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="-mr-3 border-[3px]"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.slice(0, 4)}
|
|
||||||
{collection.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">
|
|
||||||
+{collection.members.length - 4}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="text-right w-40">
|
|
||||||
<div className="text-black dark:text-white font-bold text-sm flex justify-end gap-1 items-center">
|
|
||||||
{collection.isPublic ? (
|
{collection.isPublic ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faGlobe}
|
icon={faGlobe}
|
||||||
title="This collection is being shared publicly."
|
title="This collection is being shared publicly."
|
||||||
className="w-4 h-4 drop-shadow text-gray-500 dark:text-gray-300"
|
className="w-4 h-4 drop-shadow text-neutral"
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faLink}
|
icon={faLink}
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 text-neutral"
|
||||||
/>
|
/>
|
||||||
{collection._count && collection._count.links}
|
{collection._count && collection._count.links}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end gap-1 text-gray-500 dark:text-gray-300">
|
<div className="flex items-center justify-end gap-1 text-neutral">
|
||||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
<p className="font-bold text-xs flex gap-1 items-center">
|
||||||
<p className="font-bold text-xs">{formattedDate}</p>
|
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />{" "}
|
||||||
|
{formattedDate}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
{expandDropdown ? (
|
{editCollectionModal ? (
|
||||||
<Dropdown
|
<EditCollectionModal
|
||||||
points={{ x: expandDropdown.x, y: expandDropdown.y }}
|
onClose={() => setEditCollectionModal(false)}
|
||||||
items={[
|
activeCollection={collection}
|
||||||
permissions === true
|
|
||||||
? {
|
|
||||||
name: "Edit Collection Info",
|
|
||||||
onClick: () => {
|
|
||||||
collection &&
|
|
||||||
setModal({
|
|
||||||
modal: "COLLECTION",
|
|
||||||
state: true,
|
|
||||||
method: "UPDATE",
|
|
||||||
isOwner: permissions === true,
|
|
||||||
active: collection,
|
|
||||||
});
|
|
||||||
setExpandDropdown(false);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
{
|
|
||||||
name: permissions === true ? "Share/Collaborate" : "View Team",
|
|
||||||
onClick: () => {
|
|
||||||
collection &&
|
|
||||||
setModal({
|
|
||||||
modal: "COLLECTION",
|
|
||||||
state: true,
|
|
||||||
method: "UPDATE",
|
|
||||||
isOwner: permissions === true,
|
|
||||||
active: collection,
|
|
||||||
defaultIndex: permissions === true ? 1 : 0,
|
|
||||||
});
|
|
||||||
setExpandDropdown(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name:
|
|
||||||
permissions === true ? "Delete Collection" : "Leave Collection",
|
|
||||||
onClick: () => {
|
|
||||||
collection &&
|
|
||||||
setModal({
|
|
||||||
modal: "COLLECTION",
|
|
||||||
state: true,
|
|
||||||
method: "UPDATE",
|
|
||||||
isOwner: permissions === true,
|
|
||||||
active: collection,
|
|
||||||
defaultIndex: permissions === true ? 2 : 1,
|
|
||||||
});
|
|
||||||
setExpandDropdown(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClickOutside={(e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
if (target.id !== "expand-dropdown" + collection.id)
|
|
||||||
setExpandDropdown(false);
|
|
||||||
}}
|
|
||||||
className="w-fit"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : undefined}
|
||||||
</>
|
{editCollectionSharingModal ? (
|
||||||
|
<EditCollectionSharingModal
|
||||||
|
onClose={() => setEditCollectionSharingModal(false)}
|
||||||
|
activeCollection={collection}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{deleteCollectionModal ? (
|
||||||
|
<DeleteCollectionModal
|
||||||
|
onClose={() => setDeleteCollectionModal(false)}
|
||||||
|
activeCollection={collection}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,19 +10,12 @@ type Props = {
|
||||||
export default function dashboardItem({ name, value, icon }: Props) {
|
export default function dashboardItem({ name, value, icon }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 items-end">
|
<div className="flex gap-4 items-end">
|
||||||
<div className="p-4 bg-sky-500 bg-opacity-20 dark:bg-opacity-10 rounded-xl select-none">
|
<div className="p-4 bg-primary/20 rounded-xl select-none">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={icon} className="w-8 h-8 text-primary" />
|
||||||
icon={icon}
|
|
||||||
className="w-8 h-8 text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wider">
|
<p className="text-neutral text-sm tracking-wider">{name}</p>
|
||||||
{name}
|
<p className="font-thin text-6xl text-primary mt-2">{value}</p>
|
||||||
</p>
|
|
||||||
<p className="font-thin text-6xl text-sky-500 dark:text-sky-500 mt-2">
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,13 +78,13 @@ export default function Dropdown({
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={onClickOutside}
|
||||||
className={`${
|
className={`${
|
||||||
className || ""
|
className || ""
|
||||||
} 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`}
|
} py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`}
|
||||||
>
|
>
|
||||||
{items.map((e, i) => {
|
{items.map((e, i) => {
|
||||||
const inner = e && (
|
const inner = e && (
|
||||||
<div className="cursor-pointer rounded-md">
|
<div className="cursor-pointer rounded-md">
|
||||||
<div className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 dark:hover:bg-neutral-700 duration-100">
|
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
|
||||||
<p className="text-black dark:text-white select-none">{e.name}</p>
|
<p className="select-none">{e.name}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import React, { SetStateAction } from "react";
|
import React from "react";
|
||||||
import ClickAwayHandler from "./ClickAwayHandler";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Checkbox from "./Checkbox";
|
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setFilterDropdown: (value: SetStateAction<boolean>) => void;
|
|
||||||
setSearchFilter: Function;
|
setSearchFilter: Function;
|
||||||
searchFilter: {
|
searchFilter: {
|
||||||
name: boolean;
|
name: boolean;
|
||||||
|
@ -15,64 +14,123 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FilterSearchDropdown({
|
export default function FilterSearchDropdown({
|
||||||
setFilterDropdown,
|
|
||||||
setSearchFilter,
|
setSearchFilter,
|
||||||
searchFilter,
|
searchFilter,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<ClickAwayHandler
|
<div className="dropdown dropdown-bottom dropdown-end">
|
||||||
onClickOutside={(e: Event) => {
|
<div
|
||||||
const target = e.target as HTMLInputElement;
|
tabIndex={0}
|
||||||
if (target.id !== "filter-dropdown") setFilterDropdown(false);
|
role="button"
|
||||||
}}
|
className="btn btn-sm btn-square btn-ghost"
|
||||||
className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-40"
|
>
|
||||||
>
|
<FontAwesomeIcon
|
||||||
<p className="mb-2 text-black dark:text-white text-center font-semibold">
|
icon={faFilter}
|
||||||
Filter by
|
id="sort-dropdown"
|
||||||
</p>
|
className="w-5 h-5 text-neutral"
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Checkbox
|
|
||||||
label="Name"
|
|
||||||
state={searchFilter.name}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchFilter({ ...searchFilter, name: !searchFilter.name })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Link"
|
|
||||||
state={searchFilter.url}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchFilter({ ...searchFilter, url: !searchFilter.url })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Description"
|
|
||||||
state={searchFilter.description}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchFilter({
|
|
||||||
...searchFilter,
|
|
||||||
description: !searchFilter.description,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Full Content"
|
|
||||||
state={searchFilter.textContent}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchFilter({
|
|
||||||
...searchFilter,
|
|
||||||
textContent: !searchFilter.textContent,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Tags"
|
|
||||||
state={searchFilter.tags}
|
|
||||||
onClick={() =>
|
|
||||||
setSearchFilter({ ...searchFilter, tags: !searchFilter.tags })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayHandler>
|
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mt-1">
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary [--chkfg:white]"
|
||||||
|
checked={searchFilter.name}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({ ...searchFilter, name: !searchFilter.name });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Name</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary [--chkfg:white]"
|
||||||
|
checked={searchFilter.url}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({ ...searchFilter, url: !searchFilter.url });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Link</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary [--chkfg:white]"
|
||||||
|
checked={searchFilter.description}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
description: !searchFilter.description,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Description</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary [--chkfg:white]"
|
||||||
|
checked={searchFilter.textContent}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
textContent: !searchFilter.textContent,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Full Content</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="search-filter-checkbox"
|
||||||
|
className="checkbox checkbox-primary [--chkfg:white]"
|
||||||
|
checked={searchFilter.tags}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchFilter({
|
||||||
|
...searchFilter,
|
||||||
|
tags: !searchFilter.tags,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Tags</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Select from "react-select";
|
|
||||||
import { styles } from "./styles";
|
import { styles } from "./styles";
|
||||||
import { Options } from "./types";
|
import { Options } from "./types";
|
||||||
|
import CreatableSelect from "react-select/creatable";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChange: any;
|
onChange: any;
|
||||||
|
@ -43,8 +43,8 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||||
}, [collections]);
|
}, [collections]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<CreatableSelect
|
||||||
isClearable
|
isClearable={false}
|
||||||
className="react-select-container"
|
className="react-select-container"
|
||||||
classNamePrefix="react-select"
|
classNamePrefix="react-select"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreatableSelect
|
<CreatableSelect
|
||||||
isClearable
|
isClearable={false}
|
||||||
className="react-select-container"
|
className="react-select-container"
|
||||||
classNamePrefix="react-select"
|
classNamePrefix="react-select"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|
|
@ -8,20 +8,27 @@ export const styles: StylesConfig = {
|
||||||
...styles,
|
...styles,
|
||||||
fontFamily: font,
|
fontFamily: font,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
backgroundColor: state.isSelected ? "#0ea5e9" : "inherit",
|
backgroundColor: state.isSelected ? "oklch(var(--p))" : "inherit",
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: state.isSelected ? "#0ea5e9" : "#e2e8f0",
|
backgroundColor: state.isSelected
|
||||||
|
? "oklch(var(--p))"
|
||||||
|
: "oklch(var(--nc))",
|
||||||
},
|
},
|
||||||
transition: "all 50ms",
|
transition: "all 50ms",
|
||||||
}),
|
}),
|
||||||
control: (styles) => ({
|
control: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
fontFamily: font,
|
fontFamily: font,
|
||||||
border: "none",
|
borderRadius: "0.375rem",
|
||||||
|
border: state.isFocused
|
||||||
|
? "1px solid oklch(var(--p))"
|
||||||
|
: "1px solid oklch(var(--nc))",
|
||||||
|
boxShadow: "none",
|
||||||
|
minHeight: "2.6rem",
|
||||||
}),
|
}),
|
||||||
container: (styles) => ({
|
container: (styles, state) => ({
|
||||||
...styles,
|
...styles,
|
||||||
border: "1px solid #e0f2fe",
|
height: "full",
|
||||||
borderRadius: "0.375rem",
|
borderRadius: "0.375rem",
|
||||||
lineHeight: "1.25rem",
|
lineHeight: "1.25rem",
|
||||||
// "@media screen and (min-width: 1024px)": {
|
// "@media screen and (min-width: 1024px)": {
|
||||||
|
@ -58,4 +65,5 @@ export const styles: StylesConfig = {
|
||||||
backgroundColor: "#38bdf8",
|
backgroundColor: "#38bdf8",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,18 +10,24 @@ import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Dropdown from "./Dropdown";
|
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import useModalStore from "@/store/modals";
|
import {
|
||||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
faCalendarDays,
|
||||||
|
faFileImage,
|
||||||
|
faFilePdf,
|
||||||
|
} from "@fortawesome/free-regular-svg-icons";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import EditLinkModal from "./ModalContent/EditLinkModal";
|
||||||
|
import DeleteLinkModal from "./ModalContent/DeleteLinkModal";
|
||||||
|
import ExpandedLink from "./ModalContent/ExpandedLink";
|
||||||
|
import PreservedFormatsModal from "./ModalContent/PreservedFormatsModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
link: LinkIncludingShortenedCollectionAndTags;
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
@ -29,22 +35,11 @@ type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DropdownTrigger =
|
|
||||||
| {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
| false;
|
|
||||||
|
|
||||||
export default function LinkCard({ link, count, className }: Props) {
|
export default function LinkCard({ link, count, className }: Props) {
|
||||||
const { setModal } = useModalStore();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const permissions = usePermissions(link.collection.id as number);
|
const permissions = usePermissions(link.collection.id as number);
|
||||||
|
|
||||||
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
|
||||||
|
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
|
@ -54,7 +49,7 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
let shortendURL;
|
let shortendURL;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
shortendURL = new URL(link.url).host.toLowerCase();
|
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
@ -74,15 +69,13 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
);
|
);
|
||||||
}, [collections, links]);
|
}, [collections, links]);
|
||||||
|
|
||||||
const { removeLink, updateLink, getLink } = useLinkStore();
|
const { removeLink, updateLink } = useLinkStore();
|
||||||
|
|
||||||
const pinLink = async () => {
|
const pinLink = async () => {
|
||||||
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0];
|
||||||
|
|
||||||
const load = toast.loading("Applying...");
|
const load = toast.loading("Applying...");
|
||||||
|
|
||||||
setExpandDropdown(false);
|
|
||||||
|
|
||||||
const response = await updateLink({
|
const response = await updateLink({
|
||||||
...link,
|
...link,
|
||||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
||||||
|
@ -94,25 +87,6 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateArchive = async () => {
|
|
||||||
const load = toast.loading("Sending request...");
|
|
||||||
|
|
||||||
setExpandDropdown(false);
|
|
||||||
|
|
||||||
const response = await fetch(`/api/v1/links/${link.id}/archive`, {
|
|
||||||
method: "PUT",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
toast.dismiss(load);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(`Link is being archived...`);
|
|
||||||
getLink(link.id as number);
|
|
||||||
} else toast.error(data.response);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteLink = async () => {
|
const deleteLink = async () => {
|
||||||
const load = toast.loading("Deleting...");
|
const load = toast.loading("Deleting...");
|
||||||
|
|
||||||
|
@ -121,10 +95,10 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
toast.dismiss(load);
|
toast.dismiss(load);
|
||||||
|
|
||||||
response.ok && toast.success(`Link Deleted.`);
|
response.ok && toast.success(`Link Deleted.`);
|
||||||
setExpandDropdown(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = isValidUrl(link.url) ? new URL(link.url) : undefined;
|
const url =
|
||||||
|
isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined;
|
||||||
|
|
||||||
const formattedDate = new Date(link.createdAt as string).toLocaleString(
|
const formattedDate = new Date(link.createdAt as string).toLocaleString(
|
||||||
"en-US",
|
"en-US",
|
||||||
|
@ -135,167 +109,214 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||||
|
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||||
|
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||||
|
const [expandedLink, setExpandedLink] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className={`h-fit border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative ${
|
||||||
className={`h-fit border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-2xl relative group ${
|
className || ""
|
||||||
className || ""
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{permissions === true ||
|
||||||
{(permissions === true ||
|
permissions?.canUpdate ||
|
||||||
permissions?.canUpdate ||
|
permissions?.canDelete ? (
|
||||||
permissions?.canDelete) && (
|
<div className="dropdown dropdown-left absolute top-3 right-3 z-20">
|
||||||
<div
|
<div
|
||||||
onClick={(e) => {
|
tabIndex={0}
|
||||||
setExpandDropdown({ x: e.clientX, y: e.clientY });
|
role="button"
|
||||||
}}
|
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||||
id={"expand-dropdown" + link.id}
|
|
||||||
className="text-gray-500 dark:text-gray-300 inline-flex rounded-md cursor-pointer hover:bg-slate-200 dark:hover:bg-neutral-700 absolute right-4 top-4 z-10 duration-100 p-1"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faEllipsis}
|
icon={faEllipsis}
|
||||||
title="More"
|
title="More"
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
id={"expand-dropdown" + link.id}
|
id={"expand-dropdown" + collection.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ul className="dropdown-content z-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-44 mr-1">
|
||||||
|
{permissions === true ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
pinLink();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{link?.pinnedBy && link.pinnedBy[0]
|
||||||
|
? "Unpin"
|
||||||
|
: "Pin to Dashboard"}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
{permissions === true || permissions?.canUpdate ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setEditLinkModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
{permissions === true ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setPreservedFormatsModal(true);
|
||||||
|
// updateArchive();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Preserved Formats
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
{permissions === true || permissions?.canDelete ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={"/links/" + link.id}
|
||||||
|
// onClick={
|
||||||
|
// () => router.push("/links/" + link.id)
|
||||||
|
// // setExpandedLink(true)
|
||||||
|
// }
|
||||||
|
className="flex flex-col justify-between cursor-pointer h-full w-full gap-1 p-3"
|
||||||
|
>
|
||||||
|
{link.url && url ? (
|
||||||
|
<Image
|
||||||
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
alt=""
|
||||||
|
className={`absolute w-12 bg-white shadow rounded-md p-1 bottom-3 right-3 select-none z-10`}
|
||||||
|
draggable="false"
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
target.style.display = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : link.type === "pdf" ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFilePdf}
|
||||||
|
className="absolute h-12 w-12 bg-primary/20 text-primary shadow rounded-md p-2 bottom-3 right-3 select-none z-10"
|
||||||
|
/>
|
||||||
|
) : link.type === "image" ? (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFileImage}
|
||||||
|
className="absolute h-12 w-12 bg-primary/20 text-primary shadow rounded-md p-2 bottom-3 right-3 select-none z-10"
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div className="flex items-baseline gap-1">
|
||||||
|
<p className="text-sm text-neutral">{count + 1}</p>
|
||||||
|
<p className="text-lg truncate w-full pr-8">
|
||||||
|
{unescapeString(link.name || link.description) || shortendURL}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{link.url ? (
|
||||||
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(link.url || "", "_blank");
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1 max-w-full w-fit text-neutral hover:opacity-60 duration-100"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
||||||
|
<p className="truncate w-full">{shortendURL}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="badge badge-primary badge-sm my-1">{link.type}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => router.push("/links/" + link.id)}
|
onClick={(e) => {
|
||||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-4"
|
e.preventDefault();
|
||||||
>
|
router.push(`/collections/${link.collection.id}`);
|
||||||
{url && account.displayLinkIcons && (
|
|
||||||
<Image
|
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
alt=""
|
|
||||||
className={`${
|
|
||||||
account.blurredFavicons ? "blur-sm " : ""
|
|
||||||
}absolute w-16 group-hover:opacity-80 duration-100 rounded-2xl bottom-5 right-5 opacity-60 select-none z-10`}
|
|
||||||
draggable="false"
|
|
||||||
onError={(e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
target.style.display = "none";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-between gap-5 w-full h-full z-0">
|
|
||||||
<div className="flex flex-col justify-between w-full">
|
|
||||||
<div className="flex items-baseline gap-1">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
|
||||||
{count + 1}
|
|
||||||
</p>
|
|
||||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
|
||||||
{unescapeString(link.name || link.description)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Link
|
|
||||||
href={`/collections/${link.collection.id}`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faFolder}
|
|
||||||
className="w-4 h-4 mt-1 drop-shadow"
|
|
||||||
style={{ color: collection?.color }}
|
|
||||||
/>
|
|
||||||
<p className="text-black dark:text-white truncate capitalize w-full">
|
|
||||||
{collection?.name}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* {link.tags[0] ? (
|
|
||||||
<div className="flex gap-3 items-center flex-wrap my-2 truncate relative">
|
|
||||||
<div className="flex gap-1 items-center flex-nowrap">
|
|
||||||
{link.tags.map((e, i) => (
|
|
||||||
<Link
|
|
||||||
href={"/tags/" + e.id}
|
|
||||||
key={i}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
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}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="absolute w-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-slate-100 dark:to-neutral-800 to-35%"></div>
|
|
||||||
</div>
|
|
||||||
) : undefined} */}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={link.url}
|
|
||||||
target="_blank"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-1 max-w-full w-fit text-gray-500 dark:text-gray-300 hover:opacity-70 duration-100"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
|
||||||
<p className="truncate w-full">{shortendURL}</p>
|
|
||||||
</Link>
|
|
||||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
|
|
||||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
|
||||||
<p>{formattedDate}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{expandDropdown ? (
|
|
||||||
<Dropdown
|
|
||||||
points={{ x: expandDropdown.x, y: expandDropdown.y }}
|
|
||||||
items={[
|
|
||||||
permissions === true
|
|
||||||
? {
|
|
||||||
name:
|
|
||||||
link?.pinnedBy && link.pinnedBy[0]
|
|
||||||
? "Unpin"
|
|
||||||
: "Pin to Dashboard",
|
|
||||||
onClick: pinLink,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
permissions === true || permissions?.canUpdate
|
|
||||||
? {
|
|
||||||
name: "Edit",
|
|
||||||
onClick: () => {
|
|
||||||
setModal({
|
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
method: "UPDATE",
|
|
||||||
active: link,
|
|
||||||
});
|
|
||||||
setExpandDropdown(false);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
permissions === true
|
|
||||||
? {
|
|
||||||
name: "Refresh Link",
|
|
||||||
onClick: updateArchive,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
permissions === true || permissions?.canDelete
|
|
||||||
? {
|
|
||||||
name: "Delete",
|
|
||||||
onClick: deleteLink,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
]}
|
|
||||||
onClickOutside={(e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
if (target.id !== "expand-dropdown" + link.id)
|
|
||||||
setExpandDropdown(false);
|
|
||||||
}}
|
}}
|
||||||
className="w-40"
|
className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFolder}
|
||||||
|
className="w-4 h-4 mt-1 drop-shadow"
|
||||||
|
style={{ color: collection?.color }}
|
||||||
|
/>
|
||||||
|
<p className="truncate capitalize w-full">{collection?.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 text-neutral">
|
||||||
|
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||||
|
<p>{formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
{/* {link.tags[0] ? (
|
||||||
|
<div className="flex gap-3 items-center flex-wrap mt-2 truncate relative">
|
||||||
|
<div className="flex gap-1 items-center flex-nowrap">
|
||||||
|
{link.tags.map((e, i) => (
|
||||||
|
<Link
|
||||||
|
href={"/tags/" + e.id}
|
||||||
|
key={i}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
|
>
|
||||||
|
#{e.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="absolute w-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-base-200 to-35%"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs mt-2 p-1 font-semibold italic">No Tags</p>
|
||||||
|
)} */}
|
||||||
|
</Link>
|
||||||
|
{editLinkModal ? (
|
||||||
|
<EditLinkModal
|
||||||
|
onClose={() => setEditLinkModal(false)}
|
||||||
|
activeLink={link}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : undefined}
|
||||||
</>
|
{deleteLinkModal ? (
|
||||||
|
<DeleteLinkModal
|
||||||
|
onClose={() => setDeleteLinkModal(false)}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{preservedFormatsModal ? (
|
||||||
|
<PreservedFormatsModal
|
||||||
|
onClose={() => setPreservedFormatsModal(false)}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{/* {expandedLink ? (
|
||||||
|
<ExpandedLink onClose={() => setExpandedLink(false)} link={link} />
|
||||||
|
) : undefined} */}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { faFolder, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import A from "next/link";
|
import A from "next/link";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import { Link } from "@prisma/client";
|
import { Link } from "@prisma/client";
|
||||||
|
@ -49,7 +49,7 @@ export default function LinkPreview({ link, className, settings }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`h-fit border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-2xl relative group ${
|
className={`h-fit border border-solid border-neutral-content bg-base-200 shadow hover:shadow-none duration-100 rounded-2xl relative group ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -74,19 +74,17 @@ export default function LinkPreview({ link, className, settings }: Props) {
|
||||||
<div className="flex justify-between gap-5 w-full h-full z-0">
|
<div className="flex justify-between gap-5 w-full h-full z-0">
|
||||||
<div className="flex flex-col justify-between w-full">
|
<div className="flex flex-col justify-between w-full">
|
||||||
<div className="flex items-baseline gap-1">
|
<div className="flex items-baseline gap-1">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300">{1}</p>
|
<p className="text-sm text-neutral">{1}</p>
|
||||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
<p className="text-lg truncate capitalize w-full pr-8">
|
||||||
{unescapeString(link.name as string)}
|
{unescapeString(link.name as string)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100">
|
<div className="flex items-center gap-1 max-w-full w-fit my-1 hover:opacity-70 duration-100">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
className="w-4 h-4 mt-1 drop-shadow text-sky-400"
|
className="w-4 h-4 mt-1 drop-shadow text-primary"
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate capitalize w-full">
|
<p className="truncate capitalize w-full">Landing Pages ⚡️</p>
|
||||||
Landing Pages ⚡️
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<A
|
<A
|
||||||
href={link.url as string}
|
href={link.url as string}
|
||||||
|
@ -94,12 +92,12 @@ export default function LinkPreview({ link, className, settings }: Props) {
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-1 max-w-full w-fit text-gray-500 dark:text-gray-300 hover:opacity-70 duration-100"
|
className="flex items-center gap-1 max-w-full w-fit text-neutral hover:opacity-70 duration-100"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
<FontAwesomeIcon icon={faLink} className="mt-1 w-4 h-4" />
|
||||||
<p className="truncate w-full">{shortendURL}</p>
|
<p className="truncate w-full">{shortendURL}</p>
|
||||||
</A>
|
</A>
|
||||||
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
|
<div className="flex items-center gap-1 text-neutral">
|
||||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||||
<p>{formattedDate}</p>
|
<p>{formattedDate}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`dark:bg-neutral-900 bg-white h-full lg:w-10 w-62 overflow-y-auto lg:p-0 p-5 border-solid border-white border dark:border-neutral-900 dark:lg:border-r-neutral-900 lg:border-r-white border-r-sky-100 dark:border-r-neutral-700 z-20 flex flex-col gap-5 lg:justify-center justify-start ${
|
className={`bg-base-100 h-full w-64 overflow-y-auto border-solid border border-base-100 border-r-neutral-content p-5 z-20 flex flex-col gap-5 justify-between ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -71,14 +71,9 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||||
}}
|
}}
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faPen} className="w-6 h-6 text-neutral" />
|
||||||
icon={faPen}
|
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
<p className="truncate w-full lg:hidden">Edit</p>
|
||||||
Edit
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
|
@ -99,12 +94,10 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faBoxesStacked}
|
icon={faBoxesStacked}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-6 h-6 text-neutral"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
<p className="truncate w-full lg:hidden">Preserved Formats</p>
|
||||||
Preserved Formats
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{link?.collection.ownerId === userId ||
|
{link?.collection.ownerId === userId ||
|
||||||
|
@ -124,12 +117,10 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTrashCan}
|
icon={faTrashCan}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-6 h-6 text-neutral"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
<p className="truncate w-full lg:hidden">Delete</p>
|
||||||
Delete
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { MouseEventHandler, ReactNode, useEffect } from "react";
|
||||||
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
toggleModal: Function;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Modal({ toggleModal, className, children }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto pt-2 sm:py-2 fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
||||||
|
<ClickAwayHandler
|
||||||
|
onClickOutside={toggleModal}
|
||||||
|
className={`w-full mt-auto sm:m-auto sm:w-11/12 sm:max-w-2xl ${
|
||||||
|
className || ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="slide-up mt-auto sm:m-auto relative border-neutral-content rounded-t-2xl sm:rounded-2xl border-t sm:border shadow-2xl p-5 bg-base-100">
|
||||||
|
<div
|
||||||
|
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
||||||
|
className="absolute top-3 right-3 btn btn-sm outline-none btn-circle btn-ghost"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faClose} className="w-4 h-4 text-neutral" />
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</ClickAwayHandler>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -60,7 +60,7 @@ export default function CollectionInfo({
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-black dark:text-white mb-2">Name</p>
|
<p className="mb-2">Name</p>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<TextInput
|
<TextInput
|
||||||
value={collection.name}
|
value={collection.name}
|
||||||
|
@ -71,7 +71,7 @@ export default function CollectionInfo({
|
||||||
/>
|
/>
|
||||||
<div className="color-picker flex justify-between">
|
<div className="color-picker flex justify-between">
|
||||||
<div className="flex flex-col justify-between items-center w-32">
|
<div className="flex flex-col justify-between items-center w-32">
|
||||||
<p className="w-full text-black dark:text-white mb-2">Color</p>
|
<p className="w-full mb-2">Color</p>
|
||||||
<div style={{ color: collection.color }}>
|
<div style={{ color: collection.color }}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
|
@ -79,7 +79,7 @@ export default function CollectionInfo({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="py-1 px-2 rounded-md text-xs font-semibold cursor-pointer text-black dark:text-white hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
className="btn btn-ghost btn-xs"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCollection({ ...collection, color: "#0ea5e9" })
|
setCollection({ ...collection, color: "#0ea5e9" })
|
||||||
}
|
}
|
||||||
|
@ -96,9 +96,9 @@ export default function CollectionInfo({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<p className="text-black dark:text-white mb-2">Description</p>
|
<p className="mb-2">Description</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="w-full h-[11.4rem] 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 h-[11.4rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-sky-300 dark:focus:border-sky-600"
|
||||||
placeholder="The purpose of this Collection..."
|
placeholder="The purpose of this Collection..."
|
||||||
value={collection.description}
|
value={collection.description}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default function DeleteCollection({
|
||||||
<p className="text-red-500 font-bold text-center">Warning!</p>
|
<p className="text-red-500 font-bold text-center">Warning!</p>
|
||||||
|
|
||||||
<div className="max-h-[20rem] overflow-y-auto">
|
<div className="max-h-[20rem] overflow-y-auto">
|
||||||
<div className="text-black dark:text-white">
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Please note that deleting the collection will permanently remove
|
Please note that deleting the collection will permanently remove
|
||||||
all its contents, including the following:
|
all its contents, including the following:
|
||||||
|
@ -82,7 +82,7 @@ export default function DeleteCollection({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<p className="text-black dark:text-white text-center">
|
<p className="text-center">
|
||||||
To confirm, type "
|
To confirm, type "
|
||||||
<span className="font-bold">{collection.name}</span>
|
<span className="font-bold">{collection.name}</span>
|
||||||
" in the box below:
|
" in the box below:
|
||||||
|
@ -98,9 +98,7 @@ export default function DeleteCollection({
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-black dark:text-white">
|
<p>Click the button below to leave the current collection.</p>
|
||||||
Click the button below to leave the current collection.
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -102,7 +102,7 @@ export default function TeamManagement({
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
{permissions === true && (
|
{permissions === true && (
|
||||||
<>
|
<>
|
||||||
<p className="text-black dark:text-white">Make Public</p>
|
<p>Make Public</p>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Make this a public collection."
|
label="Make this a public collection."
|
||||||
|
@ -112,7 +112,7 @@ export default function TeamManagement({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-gray-500 dark:text-gray-300 text-sm">
|
<p className="text-neutral text-sm">
|
||||||
This will let <b>Anyone</b> to view this collection.
|
This will let <b>Anyone</b> to view this collection.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
|
@ -120,9 +120,7 @@ export default function TeamManagement({
|
||||||
|
|
||||||
{collection.isPublic ? (
|
{collection.isPublic ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">
|
<p className="mb-2">Public Link (Click to copy)</p>
|
||||||
Public Link (Click to copy)
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
try {
|
try {
|
||||||
|
@ -133,7 +131,7 @@ export default function TeamManagement({
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 dark:bg-neutral-950 border-sky-100 dark:border-neutral-700 border-solid border outline-none hover:border-sky-300 dark:hover:border-sky-600 duration-100 cursor-text"
|
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 dark:bg-neutral-950 border-neutral-content border-solid border outline-none hover:border-sky-300 dark:hover:border-sky-600 duration-100 cursor-text"
|
||||||
>
|
>
|
||||||
{publicCollectionURL}
|
{publicCollectionURL}
|
||||||
</div>
|
</div>
|
||||||
|
@ -141,12 +139,12 @@ export default function TeamManagement({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{permissions !== true && collection.isPublic && (
|
{permissions !== true && collection.isPublic && (
|
||||||
<hr className="mb-3 border border-sky-100 dark:border-neutral-700" />
|
<div className="divider mb-3 mt-0"></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{permissions === true && (
|
{permissions === true && (
|
||||||
<>
|
<>
|
||||||
<p className="text-black dark:text-white">Member Management</p>
|
<p>Member Management</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -183,7 +181,7 @@ export default function TeamManagement({
|
||||||
|
|
||||||
{collection?.members[0]?.user && (
|
{collection?.members[0]?.user && (
|
||||||
<>
|
<>
|
||||||
<p className="text-center text-gray-500 dark:text-gray-300 text-xs sm:text-sm">
|
<p className="text-center text-neutral text-xs sm:text-sm">
|
||||||
(All Members have <b>Read</b> access to this collection.)
|
(All Members have <b>Read</b> access to this collection.)
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col gap-3 rounded-md">
|
<div className="flex flex-col gap-3 rounded-md">
|
||||||
|
@ -193,12 +191,12 @@ export default function TeamManagement({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="relative border p-2 rounded-md border-sky-100 dark:border-neutral-700 flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
className="relative border p-2 rounded-md border-neutral flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
||||||
>
|
>
|
||||||
{permissions === true && (
|
{permissions === true && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faClose}
|
icon={faClose}
|
||||||
className="absolute right-2 top-2 text-gray-500 dark:text-gray-300 h-4 hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
|
className="absolute right-2 top-2 text-neutral h-4 hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
|
||||||
title="Remove Member"
|
title="Remove Member"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const updatedMembers = collection.members.filter(
|
const updatedMembers = collection.members.filter(
|
||||||
|
@ -219,25 +217,21 @@ export default function TeamManagement({
|
||||||
className="border-[3px]"
|
className="border-[3px]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-black dark:text-white">
|
<p className="text-sm font-bold">{e.user.name}</p>
|
||||||
{e.user.name}
|
<p className="text-neutral">@{e.user.username}</p>
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 dark:text-gray-300">
|
|
||||||
@{e.user.username}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex sm:block items-center justify-between gap-5 min-w-[10rem]">
|
<div className="flex sm:block items-center justify-between gap-5 min-w-[10rem]">
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className={`font-bold text-sm text-black dark:text-white ${
|
className={`font-bold text-sm ${
|
||||||
permissions === true ? "" : "mb-2"
|
permissions === true ? "" : "mb-2"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Permissions
|
Permissions
|
||||||
</p>
|
</p>
|
||||||
{permissions === true && (
|
{permissions === true && (
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-300 mb-2">
|
<p className="text-xs text-neutral mb-2">
|
||||||
(Click to toggle.)
|
(Click to toggle.)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -247,7 +241,7 @@ export default function TeamManagement({
|
||||||
!e.canCreate &&
|
!e.canCreate &&
|
||||||
!e.canUpdate &&
|
!e.canUpdate &&
|
||||||
!e.canDelete ? (
|
!e.canDelete ? (
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
<p className="text-sm text-neutral">
|
||||||
Has no permissions.
|
Has no permissions.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
@ -287,7 +281,7 @@ export default function TeamManagement({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
||||||
permissions === true
|
permissions === true
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||||
: ""
|
: ""
|
||||||
|
@ -332,7 +326,7 @@ export default function TeamManagement({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
||||||
permissions === true
|
permissions === true
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||||
: ""
|
: ""
|
||||||
|
@ -377,7 +371,7 @@ export default function TeamManagement({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`text-black dark:text-white peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
className={`peer-checked:bg-sky-200 dark:peer-checked:bg-sky-600 text-sm ${
|
||||||
permissions === true
|
permissions === true
|
||||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||||
: ""
|
: ""
|
||||||
|
@ -397,7 +391,7 @@ export default function TeamManagement({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative border px-2 rounded-md border-sky-100 dark:border-neutral-700 flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between"
|
className="relative border px-2 rounded-md border-neutral-content flex min-h-[7rem] sm:min-h-[5rem] gap-2 justify-between"
|
||||||
title={`'@${collectionOwner.username}' is the owner of this collection.`}
|
title={`'@${collectionOwner.username}' is the owner of this collection.`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -407,21 +401,17 @@ export default function TeamManagement({
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<p className="text-sm font-bold text-black dark:text-white">
|
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
||||||
{collectionOwner.name}
|
|
||||||
</p>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCrown}
|
icon={faCrown}
|
||||||
className="w-3 h-3 text-yellow-500"
|
className="w-3 h-3 text-yellow-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-300">
|
<p className="text-neutral">@{collectionOwner.username}</p>
|
||||||
@{collectionOwner.username}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col justify-center min-w-[10rem] text-black dark:text-white">
|
<div className="flex flex-col justify-center min-w-[10rem]">
|
||||||
<p className={`font-bold text-sm`}>Permissions</p>
|
<p className={`font-bold text-sm`}>Permissions</p>
|
||||||
<p>Full Access (Owner)</p>
|
<p>Full Access (Owner)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -33,8 +33,8 @@ export default function ViewTeam({ collection }: Props) {
|
||||||
<p>Here are all the members who are collaborating on this collection.</p>
|
<p>Here are all the members who are collaborating on this collection.</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative border px-2 rounded-md border-sky-100 dark:border-neutral-700 flex min-h-[4rem] gap-2 justify-between"
|
className="relative border px-2 rounded-md border-neutral flex min-h-[4rem] gap-2 justify-between"
|
||||||
title={`'@${collectionOwner.username}' is the owner of this collection.`}
|
title={`@${collectionOwner.username} is the owner of this collection.`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
|
@ -43,9 +43,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||||
/>
|
/>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex items-center gap-1 w-full justify-between">
|
<div className="flex items-center gap-1 w-full justify-between">
|
||||||
<p className="text-sm font-bold text-black dark:text-white">
|
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
||||||
{collectionOwner.name}
|
|
||||||
</p>
|
|
||||||
<div className="flex text-xs gap-1 items-center">
|
<div className="flex text-xs gap-1 items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCrown}
|
icon={faCrown}
|
||||||
|
@ -54,9 +52,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||||
Admin
|
Admin
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-300">
|
<p className="text-neutral">@{collectionOwner.username}</p>
|
||||||
@{collectionOwner.username}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +66,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="relative border p-2 rounded-md border-sky-100 dark:border-neutral-700 flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
className="relative border p-2 rounded-md border-neutral flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
|
@ -78,12 +74,8 @@ export default function ViewTeam({ collection }: Props) {
|
||||||
className="border-[3px]"
|
className="border-[3px]"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-black dark:text-white">
|
<p className="text-sm font-bold">{e.user.name}</p>
|
||||||
{e.user.name}
|
<p className="text-neutral">@{e.user.username}</p>
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 dark:text-gray-300">
|
|
||||||
@{e.user.username}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Tab } from "@headlessui/react";
|
||||||
import CollectionInfo from "./CollectionInfo";
|
import CollectionInfo from "./CollectionInfo";
|
||||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||||
import TeamManagement from "./TeamManagement";
|
import TeamManagement from "./TeamManagement";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import DeleteCollection from "./DeleteCollection";
|
import DeleteCollection from "./DeleteCollection";
|
||||||
import ViewTeam from "./ViewTeam";
|
import ViewTeam from "./ViewTeam";
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export default function CollectionModal({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{method !== "VIEW_TEAM" && (
|
{method !== "VIEW_TEAM" && (
|
||||||
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5 text-black dark:text-white">
|
<Tab.List className="flex justify-center flex-col max-w-[15rem] sm:max-w-[30rem] mx-auto sm:flex-row gap-2 sm:gap-3 mb-5">
|
||||||
{method === "UPDATE" && (
|
{method === "UPDATE" && (
|
||||||
<>
|
<>
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
|
|
|
@ -44,6 +44,7 @@ export default function AddOrEditLink({
|
||||||
activeLink || {
|
activeLink || {
|
||||||
name: "",
|
name: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
type: "",
|
||||||
description: "",
|
description: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
screenshotPath: "",
|
screenshotPath: "",
|
||||||
|
@ -138,11 +139,11 @@ export default function AddOrEditLink({
|
||||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||||
{method === "UPDATE" ? (
|
{method === "UPDATE" ? (
|
||||||
<div
|
<div
|
||||||
className="text-gray-500 dark:text-gray-300 break-all w-full flex gap-2"
|
className="text-neutral break-all w-full flex gap-2"
|
||||||
title={link.url}
|
title={link.url || ""}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faLink} className="w-6 h-6" />
|
<FontAwesomeIcon icon={faLink} className="w-6 h-6" />
|
||||||
<Link href={link.url} target="_blank" className="w-full">
|
<Link href={link.url || ""} target="_blank" className="w-full">
|
||||||
{link.url}
|
{link.url}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -151,15 +152,16 @@ export default function AddOrEditLink({
|
||||||
{method === "CREATE" ? (
|
{method === "CREATE" ? (
|
||||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||||
<div className="sm:col-span-3 col-span-5">
|
<div className="sm:col-span-3 col-span-5">
|
||||||
<p className="text-black dark:text-white mb-2">Address (URL)</p>
|
<p className="mb-2">Address (URL)</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={link.url}
|
value={link.url || ""}
|
||||||
onChange={(e) => setLink({ ...link, url: e.target.value })}
|
onChange={(e) => setLink({ ...link, url: e.target.value })}
|
||||||
placeholder="e.g. http://example.com/"
|
placeholder="e.g. http://example.com/"
|
||||||
|
className="bg-base-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 col-span-5">
|
<div className="sm:col-span-2 col-span-5">
|
||||||
<p className="text-black dark:text-white mb-2">Collection</p>
|
<p className="mb-2">Collection</p>
|
||||||
{link.collection.name ? (
|
{link.collection.name ? (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
|
@ -186,20 +188,21 @@ export default function AddOrEditLink({
|
||||||
|
|
||||||
{optionsExpanded ? (
|
{optionsExpanded ? (
|
||||||
<div>
|
<div>
|
||||||
{/* <hr className="mb-3 border border-sky-100 dark:border-neutral-700" /> */}
|
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||||
<div className="grid sm:grid-cols-2 gap-3">
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
<div className={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
||||||
<p className="text-black dark:text-white mb-2">Name</p>
|
<p className="mb-2">Name</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={link.name}
|
value={link.name}
|
||||||
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||||
placeholder="e.g. Example Link"
|
placeholder="e.g. Example Link"
|
||||||
|
className="bg-base-200"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{method === "UPDATE" ? (
|
{method === "UPDATE" ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">Collection</p>
|
<p className="mb-2">Collection</p>
|
||||||
{link.collection.name ? (
|
{link.collection.name ? (
|
||||||
<CollectionSelection
|
<CollectionSelection
|
||||||
onChange={setCollection}
|
onChange={setCollection}
|
||||||
|
@ -220,7 +223,7 @@ export default function AddOrEditLink({
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">Tags</p>
|
<p className="mb-2">Tags</p>
|
||||||
<TagSelection
|
<TagSelection
|
||||||
onChange={setTags}
|
onChange={setTags}
|
||||||
defaultValue={link.tags.map((e) => {
|
defaultValue={link.tags.map((e) => {
|
||||||
|
@ -230,7 +233,7 @@ export default function AddOrEditLink({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<p className="text-black dark:text-white mb-2">Description</p>
|
<p className="mb-2">Description</p>
|
||||||
<textarea
|
<textarea
|
||||||
value={unescapeString(link.description) as string}
|
value={unescapeString(link.description) as string}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
|
@ -241,7 +244,7 @@ export default function AddOrEditLink({
|
||||||
? "Will be auto generated if nothing is provided."
|
? "Will be auto generated if nothing is provided."
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
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-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -253,7 +256,7 @@ export default function AddOrEditLink({
|
||||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||||
className={`${
|
className={`${
|
||||||
method === "UPDATE" ? "hidden" : ""
|
method === "UPDATE" ? "hidden" : ""
|
||||||
} rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 flex items-center px-2 w-fit text-sm`}
|
} rounded-md cursor-pointer btn btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||||
>
|
>
|
||||||
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -76,8 +76,7 @@ export default function PreservedFormats() {
|
||||||
// Create a temporary link and click it to trigger the download
|
// Create a temporary link and click it to trigger the download
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = path;
|
link.href = path;
|
||||||
link.download =
|
link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
|
||||||
format === ArchivedFormat.screenshot ? "Screenshot" : "PDF";
|
|
||||||
link.click();
|
link.click();
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to download file");
|
console.error("Failed to download file");
|
||||||
|
@ -91,34 +90,38 @@ export default function PreservedFormats() {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-3 sm:w-[35rem] w-80 pt-3`}>
|
<div className={`flex flex-col gap-3 sm:w-[35rem] w-80 pt-3`}>
|
||||||
{link?.screenshotPath && link?.screenshotPath !== "pending" ? (
|
{link?.screenshotPath && link?.screenshotPath !== "pending" ? (
|
||||||
<div className="flex justify-between items-center pr-1 border border-sky-100 dark:border-neutral-700 rounded-md">
|
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-l-md">
|
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||||
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">Screenshot</p>
|
<p>Screenshot</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex text-black dark:text-white gap-1">
|
<div className="flex gap-1">
|
||||||
<div
|
<div
|
||||||
onClick={() => handleDownload(ArchivedFormat.screenshot)}
|
onClick={() => handleDownload(ArchivedFormat.png)}
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCloudArrowDown}
|
icon={faCloudArrowDown}
|
||||||
className="w-5 h-5 cursor-pointer text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 cursor-pointer text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/api/v1/archives/${link?.id}?format=${ArchivedFormat.screenshot}`}
|
href={`/api/v1/archives/${link?.id}?format=${
|
||||||
|
link.screenshotPath.endsWith("png")
|
||||||
|
? ArchivedFormat.png
|
||||||
|
: ArchivedFormat.jpeg
|
||||||
|
}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 text-neutral"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,23 +129,23 @@ export default function PreservedFormats() {
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{link?.pdfPath && link.pdfPath !== "pending" ? (
|
{link?.pdfPath && link.pdfPath !== "pending" ? (
|
||||||
<div className="flex justify-between items-center pr-1 border border-sky-100 dark:border-neutral-700 rounded-md">
|
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-l-md">
|
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||||
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">PDF</p>
|
<p>PDF</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex text-black dark:text-white gap-1">
|
<div className="flex gap-1">
|
||||||
<div
|
<div
|
||||||
onClick={() => handleDownload(ArchivedFormat.pdf)}
|
onClick={() => handleDownload(ArchivedFormat.pdf)}
|
||||||
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCloudArrowDown}
|
icon={faCloudArrowDown}
|
||||||
className="w-5 h-5 cursor-pointer text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 cursor-pointer text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -153,7 +156,7 @@ export default function PreservedFormats() {
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faArrowUpRightFromSquare}
|
icon={faArrowUpRightFromSquare}
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 text-neutral"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -163,7 +166,7 @@ export default function PreservedFormats() {
|
||||||
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
<div className="flex flex-col-reverse sm:flex-row gap-5 items-center justify-center">
|
||||||
{link?.collection.ownerId === session.data?.user.id ? (
|
{link?.collection.ownerId === session.data?.user.id ? (
|
||||||
<div
|
<div
|
||||||
className={`w-full text-center bg-sky-700 p-1 rounded-md cursor-pointer select-none hover:bg-sky-600 duration-100 ${
|
className={`btn btn-accent text-white ${
|
||||||
link?.pdfPath &&
|
link?.pdfPath &&
|
||||||
link?.screenshotPath &&
|
link?.screenshotPath &&
|
||||||
link?.pdfPath !== "pending" &&
|
link?.pdfPath !== "pending" &&
|
||||||
|
@ -173,17 +176,19 @@ export default function PreservedFormats() {
|
||||||
}`}
|
}`}
|
||||||
onClick={() => updateArchive()}
|
onClick={() => updateArchive()}
|
||||||
>
|
>
|
||||||
<p>Update Preserved Formats</p>
|
<div>
|
||||||
<p className="text-xs">(Refresh Link)</p>
|
<p>Update Preserved Formats</p>
|
||||||
|
<p className="text-xs">(Refresh Link)</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Link
|
<Link
|
||||||
href={`https://web.archive.org/web/${link?.url.replace(
|
href={`https://web.archive.org/web/${link?.url?.replace(
|
||||||
/(^\w+:|^)\/\//,
|
/(^\w+:|^)\/\//,
|
||||||
""
|
""
|
||||||
)}`}
|
)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className={`text-gray-500 dark:text-gray-300 duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
|
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
|
||||||
link?.pdfPath &&
|
link?.pdfPath &&
|
||||||
link?.screenshotPath &&
|
link?.screenshotPath &&
|
||||||
link?.pdfPath !== "pending" &&
|
link?.pdfPath !== "pending" &&
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { MouseEventHandler, ReactNode } from "react";
|
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
toggleModal: Function;
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Modal({ toggleModal, className, children }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="overflow-y-auto py-2 fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex justify-center items-center fade-in z-30">
|
|
||||||
<ClickAwayHandler
|
|
||||||
onClickOutside={toggleModal}
|
|
||||||
className={`m-auto ${className || ""}`}
|
|
||||||
>
|
|
||||||
<div className="slide-up relative border-sky-100 dark:border-neutral-700 rounded-2xl border-solid border shadow-lg p-5 bg-white dark:bg-neutral-900">
|
|
||||||
<div
|
|
||||||
onClick={toggleModal as MouseEventHandler<HTMLDivElement>}
|
|
||||||
className="absolute top-5 left-5 inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 z-20 p-2"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faChevronLeft}
|
|
||||||
className="w-4 h-4 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</ClickAwayHandler>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
faRightFromBracket,
|
||||||
|
faTrashCan,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DeleteCollectionModal({
|
||||||
|
onClose,
|
||||||
|
activeCollection,
|
||||||
|
}: Props) {
|
||||||
|
const [collection, setCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollection(activeCollection);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
const { removeCollection } = useCollectionStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const [inputField, setInputField] = useState("");
|
||||||
|
|
||||||
|
const permissions = usePermissions(collection.id as number);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (permissions === true) if (collection.name !== inputField) return null;
|
||||||
|
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Deleting...");
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await removeCollection(collection.id as any);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Deleted.`);
|
||||||
|
onClose();
|
||||||
|
router.push("/collections");
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin text-red-500">
|
||||||
|
{permissions === true ? "Delete" : "Leave"} Collection
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{permissions === true ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>
|
||||||
|
To confirm, type "
|
||||||
|
<span className="font-bold">{collection.name}</span>
|
||||||
|
" in the box below:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={inputField}
|
||||||
|
onChange={(e) => setInputField(e.target.value)}
|
||||||
|
placeholder={`Type "${collection.name}" Here.`}
|
||||||
|
className="w-3/4 mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="alert" className="alert alert-warning">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
<b>
|
||||||
|
Warning: Deleting this collection will permanently erase all
|
||||||
|
its contents
|
||||||
|
</b>
|
||||||
|
, and it will become inaccessible to everyone, including members
|
||||||
|
with previous access.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>Click the button below to leave the current collection.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={permissions === true && inputField !== collection.name}
|
||||||
|
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 ${
|
||||||
|
permissions === true
|
||||||
|
? inputField === collection.name
|
||||||
|
? "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
|
||||||
|
: "cursor-not-allowed bg-red-300 dark:bg-red-900"
|
||||||
|
: "bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer"
|
||||||
|
}`}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={permissions === true ? faTrashCan : faRightFromBracket}
|
||||||
|
className="h-5"
|
||||||
|
/>
|
||||||
|
{permissions === true ? "Delete" : "Leave"} Collection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||||
|
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faLink, faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DeleteLinkModal({ onClose, activeLink }: Props) {
|
||||||
|
const [link, setLink] =
|
||||||
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
|
||||||
|
const { removeLink } = useLinkStore();
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLink(activeLink);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteLink = async () => {
|
||||||
|
const load = toast.loading("Deleting...");
|
||||||
|
|
||||||
|
const response = await removeLink(link.id as number);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
response.ok && toast.success(`Link Deleted.`);
|
||||||
|
|
||||||
|
if (router.pathname.startsWith("/links/[id]")) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin text-red-500">Delete Link</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p>Are you sure you want to delete this Link?</p>
|
||||||
|
|
||||||
|
<div role="alert" className="alert alert-warning">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="stroke-current shrink-0 h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Warning: This action is irreversible!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Hold the <kbd className="kbd kbd-sm">Shift</kbd> key while clicking
|
||||||
|
'Delete' to bypass this confirmation in the future.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`ml-auto btn w-fit text-white flex items-center gap-2 duration-100 bg-red-500 hover:bg-red-400 hover:dark:bg-red-600 cursor-pointer`}
|
||||||
|
onClick={deleteLink}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} className="h-5" />
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { HexColorPicker } from "react-colorful";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditCollectionModal({
|
||||||
|
onClose,
|
||||||
|
activeCollection,
|
||||||
|
}: Props) {
|
||||||
|
const [collection, setCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||||
|
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
const { updateCollection } = useCollectionStore();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Updating...");
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await updateCollection(collection as any);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Updated!`);
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Edit Collection Info</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Name</p>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<TextInput
|
||||||
|
className="bg-base-200"
|
||||||
|
value={collection.name}
|
||||||
|
placeholder="e.g. Example Collection"
|
||||||
|
onChange={(e) =>
|
||||||
|
setCollection({ ...collection, name: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="w-full mb-2">Color</p>
|
||||||
|
<div className="color-picker flex justify-between">
|
||||||
|
<div className="flex flex-col gap-2 items-center w-32">
|
||||||
|
<div style={{ color: collection.color }}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFolder}
|
||||||
|
className="w-12 h-12 drop-shadow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="btn btn-ghost btn-xs"
|
||||||
|
onClick={() =>
|
||||||
|
setCollection({ ...collection, color: "#0ea5e9" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HexColorPicker
|
||||||
|
color={collection.color}
|
||||||
|
onChange={(e) => setCollection({ ...collection, color: e })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Description</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-[13rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||||
|
placeholder="The purpose of this Collection..."
|
||||||
|
value={collection.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-accent text-white w-fit ml-auto"
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,450 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import {
|
||||||
|
faClose,
|
||||||
|
faCrown,
|
||||||
|
faUserPlus,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
||||||
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
|
import useAccountStore from "@/store/account";
|
||||||
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
|
import ProfilePhoto from "../ProfilePhoto";
|
||||||
|
import addMemberToCollection from "@/lib/client/addMemberToCollection";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeCollection: CollectionIncludingMembersAndLinkCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditCollectionSharingModal({
|
||||||
|
onClose,
|
||||||
|
activeCollection,
|
||||||
|
}: Props) {
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOwner = async () => {
|
||||||
|
const owner = await getPublicUserData(collection.ownerId as number);
|
||||||
|
setCollectionOwner(owner);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOwner();
|
||||||
|
|
||||||
|
setCollection(activeCollection);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [collection, setCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>(activeCollection);
|
||||||
|
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
const { updateCollection } = useCollectionStore();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Updating...");
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await updateCollection(collection as any);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Updated!`);
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { account } = useAccountStore();
|
||||||
|
const permissions = usePermissions(collection.id as number);
|
||||||
|
|
||||||
|
const currentURL = new URL(document.URL);
|
||||||
|
|
||||||
|
const publicCollectionURL = `${currentURL.origin}/public/collections/${collection.id}`;
|
||||||
|
|
||||||
|
const [memberUsername, setMemberUsername] = useState("");
|
||||||
|
|
||||||
|
const [collectionOwner, setCollectionOwner] = useState({
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
username: "",
|
||||||
|
image: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const setMemberState = (newMember: Member) => {
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
members: [...collection.members, newMember],
|
||||||
|
});
|
||||||
|
|
||||||
|
setMemberUsername("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">
|
||||||
|
{permissions === true ? "Share and Collaborate" : "Team"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{permissions === true && (
|
||||||
|
<div>
|
||||||
|
<p>Make Public</p>
|
||||||
|
|
||||||
|
<label className="label cursor-pointer justify-start gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={collection.isPublic}
|
||||||
|
onChange={() =>
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
isPublic: !collection.isPublic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="checkbox checkbox-primary"
|
||||||
|
/>
|
||||||
|
<span className="label-text">Make this a public collection</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p className="text-neutral text-sm">
|
||||||
|
This will let <b>Anyone</b> to view this collection and it's
|
||||||
|
users.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{collection.isPublic ? (
|
||||||
|
<div className={permissions === true ? "pl-5" : ""}>
|
||||||
|
<p className="mb-2">Sharable Link (Click to copy)</p>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
try {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(publicCollectionURL)
|
||||||
|
.then(() => toast.success("Copied!"));
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border outline-none hover:border-primary dark:hover:border-primary duration-100 cursor-text"
|
||||||
|
>
|
||||||
|
{publicCollectionURL}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{permissions === true && <div className="divider my-3"></div>}
|
||||||
|
|
||||||
|
{permissions === true && (
|
||||||
|
<>
|
||||||
|
<p>Member Management</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TextInput
|
||||||
|
value={memberUsername || ""}
|
||||||
|
className="bg-base-200"
|
||||||
|
placeholder="Username (without the '@')"
|
||||||
|
onChange={(e) => setMemberUsername(e.target.value)}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
e.key === "Enter" &&
|
||||||
|
addMemberToCollection(
|
||||||
|
account.username as string,
|
||||||
|
memberUsername || "",
|
||||||
|
collection,
|
||||||
|
setMemberState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
addMemberToCollection(
|
||||||
|
account.username as string,
|
||||||
|
memberUsername || "",
|
||||||
|
collection,
|
||||||
|
setMemberState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="btn btn-accent text-white btn-square btn-sm h-10 w-10"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faUserPlus} className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{collection?.members[0]?.user && (
|
||||||
|
<>
|
||||||
|
{permissions === true ? (
|
||||||
|
<p className="text-center text-neutral text-xs sm:text-sm">
|
||||||
|
(All Members have <b>Read</b> access to this collection.)
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
Here are all the members who are collaborating on this
|
||||||
|
collection.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 rounded-md">
|
||||||
|
<div
|
||||||
|
className="relative border px-2 rounded-xl border-neutral-content bg-base-200 flex min-h-[6rem] sm:min-h-[4.1rem] gap-2 justify-between"
|
||||||
|
title={`@${collectionOwner.username} is the owner of this collection.`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<ProfilePhoto
|
||||||
|
src={
|
||||||
|
collectionOwner.image ? collectionOwner.image : undefined
|
||||||
|
}
|
||||||
|
name={collectionOwner.name}
|
||||||
|
/>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center gap-1 w-full justify-between">
|
||||||
|
<p className="text-sm font-bold">
|
||||||
|
{collectionOwner.name}
|
||||||
|
</p>
|
||||||
|
<div className="flex text-xs gap-1 items-center">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCrown}
|
||||||
|
className="w-3 h-3 text-yellow-500"
|
||||||
|
/>
|
||||||
|
Admin
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-neutral">@{collectionOwner.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{collection.members
|
||||||
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
|
.map((e, i) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="relative border p-2 rounded-xl border-neutral-content bg-base-200 flex flex-col sm:flex-row sm:items-center gap-2 justify-between"
|
||||||
|
>
|
||||||
|
{permissions === true && (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faClose}
|
||||||
|
className="absolute right-2 top-2 text-neutral h-4 hover:text-red-500 dark:hover:text-red-500 duration-100 cursor-pointer"
|
||||||
|
title="Remove Member"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedMembers = collection.members.filter(
|
||||||
|
(member) => {
|
||||||
|
return member.user.username !== e.user.username;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
members: updatedMembers,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ProfilePhoto
|
||||||
|
src={e.user.image ? e.user.image : undefined}
|
||||||
|
name={e.user.name}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold">{e.user.name}</p>
|
||||||
|
<p className="text-neutral">@{e.user.username}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex sm:block items-center justify-between gap-5 min-w-[10rem]">
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className={`font-bold text-sm ${
|
||||||
|
permissions === true ? "" : "mb-2"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Permissions
|
||||||
|
</p>
|
||||||
|
{permissions === true && (
|
||||||
|
<p className="text-xs text-neutral mb-2">
|
||||||
|
(Click to toggle.)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{permissions !== true &&
|
||||||
|
!e.canCreate &&
|
||||||
|
!e.canUpdate &&
|
||||||
|
!e.canDelete ? (
|
||||||
|
<p className="text-sm text-neutral">
|
||||||
|
Has no permissions.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
className={
|
||||||
|
permissions === true
|
||||||
|
? "cursor-pointer mr-1"
|
||||||
|
: "mr-1"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="canCreate"
|
||||||
|
className="peer sr-only"
|
||||||
|
checked={e.canCreate}
|
||||||
|
onChange={() => {
|
||||||
|
if (permissions === true) {
|
||||||
|
const updatedMembers =
|
||||||
|
collection.members.map((member) => {
|
||||||
|
if (
|
||||||
|
member.user.username ===
|
||||||
|
e.user.username
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
canCreate: !e.canCreate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
members: updatedMembers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||||
|
permissions === true
|
||||||
|
? "hover:bg-neutral-content duration-100"
|
||||||
|
: ""
|
||||||
|
} rounded p-1 select-none`}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className={
|
||||||
|
permissions === true
|
||||||
|
? "cursor-pointer mr-1"
|
||||||
|
: "mr-1"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="canUpdate"
|
||||||
|
className="peer sr-only"
|
||||||
|
checked={e.canUpdate}
|
||||||
|
onChange={() => {
|
||||||
|
if (permissions === true) {
|
||||||
|
const updatedMembers =
|
||||||
|
collection.members.map((member) => {
|
||||||
|
if (
|
||||||
|
member.user.username ===
|
||||||
|
e.user.username
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
canUpdate: !e.canUpdate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
members: updatedMembers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||||
|
permissions === true
|
||||||
|
? "hover:bg-neutral-content duration-100"
|
||||||
|
: ""
|
||||||
|
} rounded p-1 select-none`}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className={
|
||||||
|
permissions === true
|
||||||
|
? "cursor-pointer mr-1"
|
||||||
|
: "mr-1"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="canDelete"
|
||||||
|
className="peer sr-only"
|
||||||
|
checked={e.canDelete}
|
||||||
|
onChange={() => {
|
||||||
|
if (permissions === true) {
|
||||||
|
const updatedMembers =
|
||||||
|
collection.members.map((member) => {
|
||||||
|
if (
|
||||||
|
member.user.username ===
|
||||||
|
e.user.username
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
canDelete: !e.canDelete,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
members: updatedMembers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`peer-checked:bg-primary peer-checked:text-primary-content text-sm ${
|
||||||
|
permissions === true
|
||||||
|
? "hover:bg-neutral-content duration-100"
|
||||||
|
: ""
|
||||||
|
} rounded p-1 select-none`}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions === true && (
|
||||||
|
<button
|
||||||
|
className="btn btn-accent text-white w-fit ml-auto mt-3"
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||||
|
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faLink } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EditLinkModal({ onClose, activeLink }: Props) {
|
||||||
|
const [link, setLink] =
|
||||||
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
|
||||||
|
let shortendURL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
shortendURL = new URL(link.url || "").host.toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { updateLink } = useLinkStore();
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
const setCollection = (e: any) => {
|
||||||
|
if (e?.__isNew__) e.value = null;
|
||||||
|
|
||||||
|
setLink({
|
||||||
|
...link,
|
||||||
|
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTags = (e: any) => {
|
||||||
|
const tagNames = e.map((e: any) => {
|
||||||
|
return { name: e.label };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLink({ ...link, tags: tagNames });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLink(activeLink);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
const load = toast.loading("Updating...");
|
||||||
|
|
||||||
|
response = await updateLink(link);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Updated!`);
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Edit Link</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
{link.url ? (
|
||||||
|
<Link
|
||||||
|
href={link.url}
|
||||||
|
className="truncate text-neutral flex gap-2 mb-5 w-fit max-w-full"
|
||||||
|
title={link.url}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faLink}
|
||||||
|
className="mt-1 w-5 h-5 min-w-[1.25rem]"
|
||||||
|
/>
|
||||||
|
<p>{shortendURL}</p>
|
||||||
|
</Link>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Name</p>
|
||||||
|
<TextInput
|
||||||
|
value={link.name}
|
||||||
|
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||||
|
placeholder="e.g. Example Link"
|
||||||
|
className="bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||||
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Collection</p>
|
||||||
|
{link.collection.name ? (
|
||||||
|
<CollectionSelection
|
||||||
|
onChange={setCollection}
|
||||||
|
// defaultValue={{
|
||||||
|
// label: link.collection.name,
|
||||||
|
// value: link.collection.id,
|
||||||
|
// }}
|
||||||
|
defaultValue={
|
||||||
|
link.collection.id
|
||||||
|
? {
|
||||||
|
value: link.collection.id,
|
||||||
|
label: link.collection.name,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
value: null as unknown as number,
|
||||||
|
label: "Unorganized",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Tags</p>
|
||||||
|
<TagSelection
|
||||||
|
onChange={setTags}
|
||||||
|
defaultValue={link.tags.map((e) => {
|
||||||
|
return { label: e.name, value: e.id };
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<p className="mb-2">Description</p>
|
||||||
|
<textarea
|
||||||
|
value={unescapeString(link.description) as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLink({ ...link, description: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Will be auto generated if nothing is provided."
|
||||||
|
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end items-center mt-5">
|
||||||
|
<button className="btn btn-accent text-white" onClick={submit}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
import {
|
||||||
|
CollectionIncludingMembersAndLinkCount,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import Image from "next/image";
|
||||||
|
import ColorThief, { RGBColor } from "colorthief";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faBoxArchive,
|
||||||
|
faCloudArrowDown,
|
||||||
|
faFolder,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import {
|
||||||
|
faCalendarDays,
|
||||||
|
faFileImage,
|
||||||
|
faFilePdf,
|
||||||
|
} from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
link: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
onClose: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LinkDetails({ link, onClose }: Props) {
|
||||||
|
const {
|
||||||
|
settings: { theme },
|
||||||
|
} = useLocalSettingsStore();
|
||||||
|
|
||||||
|
const [imageError, setImageError] = useState<boolean>(false);
|
||||||
|
const formattedDate = new Date(link.createdAt as string).toLocaleString(
|
||||||
|
"en-US",
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
const [collection, setCollection] =
|
||||||
|
useState<CollectionIncludingMembersAndLinkCount>(
|
||||||
|
collections.find(
|
||||||
|
(e) => e.id === link.collection.id
|
||||||
|
) as CollectionIncludingMembersAndLinkCount
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollection(
|
||||||
|
collections.find(
|
||||||
|
(e) => e.id === link.collection.id
|
||||||
|
) as CollectionIncludingMembersAndLinkCount
|
||||||
|
);
|
||||||
|
}, [collections]);
|
||||||
|
|
||||||
|
const [colorPalette, setColorPalette] = useState<RGBColor[]>();
|
||||||
|
|
||||||
|
const colorThief = new ColorThief();
|
||||||
|
|
||||||
|
const url = link.url && isValidUrl(link.url) ? new URL(link.url) : undefined;
|
||||||
|
|
||||||
|
const handleDownload = (format: "png" | "pdf") => {
|
||||||
|
const path = `/api/v1/archives/${link.collection.id}/${link.id}.${format}`;
|
||||||
|
fetch(path)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
// Create a temporary link and click it to trigger the download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = path;
|
||||||
|
link.download = format === "pdf" ? "PDF" : "Screenshot";
|
||||||
|
link.click();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to download file");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<div className={`relative flex gap-5 items-start mr-10`}>
|
||||||
|
{!imageError && url && (
|
||||||
|
<Image
|
||||||
|
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${url.origin}&size=32`}
|
||||||
|
width={42}
|
||||||
|
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"
|
||||||
|
draggable="false"
|
||||||
|
onLoad={(e) => {
|
||||||
|
try {
|
||||||
|
const color = colorThief.getPalette(
|
||||||
|
e.target as HTMLImageElement,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
|
setColorPalette(color);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
setImageError(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex w-full flex-col min-h-[3rem] justify-center drop-shadow">
|
||||||
|
<p className="text-2xl capitalize break-words hyphens-auto">
|
||||||
|
{unescapeString(link.name)}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href={link.url || ""}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className={`${
|
||||||
|
link.name ? "text-sm" : "text-xl"
|
||||||
|
} text-gray-500 dark:text-gray-300 break-all hover:underline cursor-pointer w-fit`}
|
||||||
|
>
|
||||||
|
{url ? url.host : link.url}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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: collection?.color }}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
title={collection?.name}
|
||||||
|
className="text-lg truncate max-w-[12rem]"
|
||||||
|
>
|
||||||
|
{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 dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
|
||||||
|
>
|
||||||
|
{e.name}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{link.description && (
|
||||||
|
<>
|
||||||
|
<div className="max-h-[20rem] my-3 rounded-md overflow-y-auto hyphens-auto">
|
||||||
|
{unescapeString(link.description)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-1 text-gray-500 dark:text-gray-300">
|
||||||
|
<FontAwesomeIcon icon={faBoxArchive} className="w-4 h-4" />
|
||||||
|
<p>Archived Formats:</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-1 text-gray-500 dark:text-gray-300"
|
||||||
|
title={"Created at: " + formattedDate}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||||
|
<p>{formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
|
||||||
|
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Screenshot</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link.collectionId}/${link.id}.png`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("png")}
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center p-2 border border-sky-100 dark:border-neutral-700 rounded-md">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-md">
|
||||||
|
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>PDF</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link.collectionId}/${link.id}.pdf`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload("pdf")}
|
||||||
|
className="cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-sky-500 dark:text-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import toast, { Toaster } from "react-hot-toast";
|
||||||
|
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { HexColorPicker } from "react-colorful";
|
||||||
|
import { Collection } from "@prisma/client";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NewCollectionModal({ onClose }: Props) {
|
||||||
|
const initial = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
color: "#0ea5e9",
|
||||||
|
};
|
||||||
|
|
||||||
|
const [collection, setCollection] = useState<Partial<Collection>>(initial);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollection(initial);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
const { addCollection } = useCollectionStore();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
if (!collection) return null;
|
||||||
|
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
const load = toast.loading("Creating...");
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
response = await addCollection(collection as any);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success("Created!");
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Create a New Collection</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3">
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Name</p>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<TextInput
|
||||||
|
className="bg-base-200"
|
||||||
|
value={collection.name}
|
||||||
|
placeholder="e.g. Example Collection"
|
||||||
|
onChange={(e) =>
|
||||||
|
setCollection({ ...collection, name: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="w-full mb-2">Color</p>
|
||||||
|
<div className="color-picker flex justify-between">
|
||||||
|
<div className="flex flex-col gap-2 items-center w-32">
|
||||||
|
<div style={{ color: collection.color }}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFolder}
|
||||||
|
className="w-12 h-12 drop-shadow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="btn btn-ghost btn-xs"
|
||||||
|
onClick={() =>
|
||||||
|
setCollection({ ...collection, color: "#0ea5e9" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HexColorPicker
|
||||||
|
color={collection.color}
|
||||||
|
onChange={(e) => setCollection({ ...collection, color: e })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<p className="mb-2">Description</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-[13rem] resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||||
|
placeholder="The purpose of this Collection..."
|
||||||
|
value={collection.description}
|
||||||
|
onChange={(e) =>
|
||||||
|
setCollection({
|
||||||
|
...collection,
|
||||||
|
description: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-accent text-white w-fit ml-auto"
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
Create Collection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||||
|
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NewLinkModal({ onClose }: Props) {
|
||||||
|
const { data } = useSession();
|
||||||
|
|
||||||
|
const initial = {
|
||||||
|
name: "",
|
||||||
|
url: "",
|
||||||
|
description: "",
|
||||||
|
type: "url",
|
||||||
|
tags: [],
|
||||||
|
screenshotPath: "",
|
||||||
|
pdfPath: "",
|
||||||
|
readabilityPath: "",
|
||||||
|
textContent: "",
|
||||||
|
collection: {
|
||||||
|
name: "",
|
||||||
|
ownerId: data?.user.id as number,
|
||||||
|
},
|
||||||
|
} as LinkIncludingShortenedCollectionAndTags;
|
||||||
|
|
||||||
|
const [link, setLink] =
|
||||||
|
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||||
|
|
||||||
|
const { addLink } = useLinkStore();
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
const setCollection = (e: any) => {
|
||||||
|
if (e?.__isNew__) e.value = null;
|
||||||
|
|
||||||
|
setLink({
|
||||||
|
...link,
|
||||||
|
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTags = (e: any) => {
|
||||||
|
const tagNames = e.map((e: any) => {
|
||||||
|
return { name: e.label };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLink({ ...link, tags: tagNames });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (router.query.id) {
|
||||||
|
const currentCollection = collections.find(
|
||||||
|
(e) => e.id == Number(router.query.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentCollection &&
|
||||||
|
currentCollection.ownerId &&
|
||||||
|
router.asPath.startsWith("/collections/")
|
||||||
|
)
|
||||||
|
setLink({
|
||||||
|
...initial,
|
||||||
|
collection: {
|
||||||
|
id: currentCollection.id,
|
||||||
|
name: currentCollection.name,
|
||||||
|
ownerId: currentCollection.ownerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
setLink({
|
||||||
|
...initial,
|
||||||
|
collection: {
|
||||||
|
name: "Unorganized",
|
||||||
|
ownerId: data?.user.id as number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
const load = toast.loading("Creating...");
|
||||||
|
|
||||||
|
response = await addLink(link);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Created!`);
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Create a New Link</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||||
|
<div className="sm:col-span-3 col-span-5">
|
||||||
|
<p className="mb-2">Link</p>
|
||||||
|
<TextInput
|
||||||
|
value={link.url || ""}
|
||||||
|
onChange={(e) => setLink({ ...link, url: e.target.value })}
|
||||||
|
placeholder="e.g. http://example.com/"
|
||||||
|
className="bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2 col-span-5">
|
||||||
|
<p className="mb-2">Collection</p>
|
||||||
|
{link.collection.name ? (
|
||||||
|
<CollectionSelection
|
||||||
|
onChange={setCollection}
|
||||||
|
defaultValue={{
|
||||||
|
label: link.collection.name,
|
||||||
|
value: link.collection.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{optionsExpanded ? (
|
||||||
|
<div className="mt-5">
|
||||||
|
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||||
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Name</p>
|
||||||
|
<TextInput
|
||||||
|
value={link.name}
|
||||||
|
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||||
|
placeholder="e.g. Example Link"
|
||||||
|
className="bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Tags</p>
|
||||||
|
<TagSelection
|
||||||
|
onChange={setTags}
|
||||||
|
defaultValue={link.tags.map((e) => {
|
||||||
|
return { label: e.name, value: e.id };
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<p className="mb-2">Description</p>
|
||||||
|
<textarea
|
||||||
|
value={unescapeString(link.description) as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLink({ ...link, description: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Will be auto generated if nothing is provided."
|
||||||
|
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-5">
|
||||||
|
<div
|
||||||
|
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||||
|
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||||
|
>
|
||||||
|
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn btn-accent text-white" onClick={submit}>
|
||||||
|
Create Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||||
|
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faCloudArrowDown,
|
||||||
|
faLink,
|
||||||
|
faTrashCan,
|
||||||
|
faUpRightFromSquare,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PreservedFormatsModal({ onClose, activeLink }: Props) {
|
||||||
|
const session = useSession();
|
||||||
|
const { links, getLink } = useLinkStore();
|
||||||
|
|
||||||
|
const [link, setLink] =
|
||||||
|
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isPublicRoute = router.pathname.startsWith("/public")
|
||||||
|
? true
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const data = await getLink(link.id as number, isPublicRoute);
|
||||||
|
setLink(
|
||||||
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
let interval: NodeJS.Timer | undefined;
|
||||||
|
if (link?.screenshotPath === "pending" || link?.pdfPath === "pending") {
|
||||||
|
interval = setInterval(async () => {
|
||||||
|
const data = await getLink(link.id as number, isPublicRoute);
|
||||||
|
setLink(
|
||||||
|
(data as any).response as LinkIncludingShortenedCollectionAndTags
|
||||||
|
);
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [link?.screenshotPath, link?.pdfPath, link?.readabilityPath]);
|
||||||
|
|
||||||
|
const updateArchive = async () => {
|
||||||
|
const load = toast.loading("Sending request...");
|
||||||
|
|
||||||
|
const response = await fetch(`/api/v1/links/${link?.id}/archive`, {
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success(`Link is being archived...`);
|
||||||
|
getLink(link?.id as number);
|
||||||
|
} else toast.error(data.response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = (format: ArchivedFormat) => {
|
||||||
|
const path = `/api/v1/archives/${link?.id}?format=${format}`;
|
||||||
|
fetch(path)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
// Create a temporary link and click it to trigger the download
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = path;
|
||||||
|
link.download = format === ArchivedFormat.png ? "Screenshot" : "PDF";
|
||||||
|
link.click();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to download file");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<p className="text-xl font-thin">Preserved Formats</p>
|
||||||
|
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
|
<div className={`flex flex-col gap-3`}>
|
||||||
|
{link?.screenshotPath && link?.screenshotPath !== "pending" ? (
|
||||||
|
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||||
|
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Screenshot</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload(ArchivedFormat.png)}
|
||||||
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-neutral"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link?.id}?format=${
|
||||||
|
link.screenshotPath.endsWith("png")
|
||||||
|
? ArchivedFormat.png
|
||||||
|
: ArchivedFormat.jpeg
|
||||||
|
}`}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-neutral"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{link?.pdfPath && link.pdfPath !== "pending" ? (
|
||||||
|
<div className="flex justify-between items-center pr-1 border border-neutral-content rounded-md">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="bg-primary text-primary-content p-2 rounded-l-md">
|
||||||
|
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>PDF</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div
|
||||||
|
onClick={() => handleDownload(ArchivedFormat.pdf)}
|
||||||
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faCloudArrowDown}
|
||||||
|
className="w-5 h-5 cursor-pointer text-neutral"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/api/v1/archives/${link?.id}?format=${ArchivedFormat.pdf}`}
|
||||||
|
target="_blank"
|
||||||
|
className="cursor-pointer hover:opacity-60 duration-100 p-2 rounded-md"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-5 h-5 text-neutral"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<div className="flex flex-col-reverse sm:flex-row sm:gap-3 items-center justify-center">
|
||||||
|
{link?.collection.ownerId === session.data?.user.id ? (
|
||||||
|
<div
|
||||||
|
className={`btn btn-accent text-white ${
|
||||||
|
link?.pdfPath &&
|
||||||
|
link?.screenshotPath &&
|
||||||
|
link?.pdfPath !== "pending" &&
|
||||||
|
link?.screenshotPath !== "pending"
|
||||||
|
? "mt-3"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() => updateArchive()}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>Update Preserved Formats</p>
|
||||||
|
<p className="text-xs">(Refresh Link)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
<Link
|
||||||
|
href={`https://web.archive.org/web/${link?.url?.replace(
|
||||||
|
/(^\w+:|^)\/\//,
|
||||||
|
""
|
||||||
|
)}`}
|
||||||
|
target="_blank"
|
||||||
|
className={`text-neutral duration-100 hover:opacity-60 flex gap-2 w-fit items-center text-sm ${
|
||||||
|
link?.pdfPath &&
|
||||||
|
link?.screenshotPath &&
|
||||||
|
link?.pdfPath !== "pending" &&
|
||||||
|
link?.screenshotPath !== "pending"
|
||||||
|
? "sm:mt-3"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="whitespace-nowrap">
|
||||||
|
View Latest Snapshot on archive.org
|
||||||
|
</p>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import CollectionSelection from "@/components/InputSelect/CollectionSelection";
|
||||||
|
import TagSelection from "@/components/InputSelect/TagSelection";
|
||||||
|
import TextInput from "@/components/TextInput";
|
||||||
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import useLinkStore from "@/store/links";
|
||||||
|
import {
|
||||||
|
ArchivedFormat,
|
||||||
|
LinkIncludingShortenedCollectionAndTags,
|
||||||
|
} from "@/types/global";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UploadFileModal({ onClose }: Props) {
|
||||||
|
const { data } = useSession();
|
||||||
|
|
||||||
|
const initial = {
|
||||||
|
name: "",
|
||||||
|
url: "",
|
||||||
|
description: "",
|
||||||
|
type: "url",
|
||||||
|
tags: [],
|
||||||
|
screenshotPath: "",
|
||||||
|
pdfPath: "",
|
||||||
|
readabilityPath: "",
|
||||||
|
textContent: "",
|
||||||
|
collection: {
|
||||||
|
name: "",
|
||||||
|
ownerId: data?.user.id as number,
|
||||||
|
},
|
||||||
|
} as LinkIncludingShortenedCollectionAndTags;
|
||||||
|
|
||||||
|
const [link, setLink] =
|
||||||
|
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||||
|
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
|
const { addLink } = useLinkStore();
|
||||||
|
const [submitLoader, setSubmitLoader] = useState(false);
|
||||||
|
|
||||||
|
const [optionsExpanded, setOptionsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
const setCollection = (e: any) => {
|
||||||
|
if (e?.__isNew__) e.value = null;
|
||||||
|
|
||||||
|
setLink({
|
||||||
|
...link,
|
||||||
|
collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTags = (e: any) => {
|
||||||
|
const tagNames = e.map((e: any) => {
|
||||||
|
return { name: e.label };
|
||||||
|
});
|
||||||
|
|
||||||
|
setLink({ ...link, tags: tagNames });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOptionsExpanded(false);
|
||||||
|
if (router.query.id) {
|
||||||
|
const currentCollection = collections.find(
|
||||||
|
(e) => e.id == Number(router.query.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentCollection &&
|
||||||
|
currentCollection.ownerId &&
|
||||||
|
router.asPath.startsWith("/collections/")
|
||||||
|
)
|
||||||
|
setLink({
|
||||||
|
...initial,
|
||||||
|
collection: {
|
||||||
|
id: currentCollection.id,
|
||||||
|
name: currentCollection.name,
|
||||||
|
ownerId: currentCollection.ownerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
setLink({
|
||||||
|
...initial,
|
||||||
|
collection: {
|
||||||
|
name: "Unorganized",
|
||||||
|
ownerId: data?.user.id as number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!submitLoader && file) {
|
||||||
|
let fileType: ArchivedFormat | null = null;
|
||||||
|
let linkType: "url" | "image" | "pdf" | null = null;
|
||||||
|
|
||||||
|
if (file?.type === "image/jpg" || file.type === "image/jpeg") {
|
||||||
|
fileType = ArchivedFormat.jpeg;
|
||||||
|
linkType = "image";
|
||||||
|
} else if (file.type === "image/png") {
|
||||||
|
fileType = ArchivedFormat.png;
|
||||||
|
linkType = "image";
|
||||||
|
} else if (file.type === "application/pdf") {
|
||||||
|
fileType = ArchivedFormat.pdf;
|
||||||
|
linkType = "pdf";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType !== null && linkType !== null) {
|
||||||
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
const load = toast.loading("Creating...");
|
||||||
|
|
||||||
|
response = await addLink({
|
||||||
|
...link,
|
||||||
|
type: linkType,
|
||||||
|
name: link.name ? link.name : file.name.replace(/\.[^/.]+$/, ""),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const formBody = new FormData();
|
||||||
|
file && formBody.append("file", file);
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
`/api/v1/archives/${
|
||||||
|
(response.data as LinkIncludingShortenedCollectionAndTags).id
|
||||||
|
}?format=${fileType}`,
|
||||||
|
{
|
||||||
|
body: formBody,
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
toast.success(`Created!`);
|
||||||
|
onClose();
|
||||||
|
} else toast.error(response.data as string);
|
||||||
|
|
||||||
|
setSubmitLoader(false);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal toggleModal={onClose}>
|
||||||
|
<div className="flex gap-2 items-start">
|
||||||
|
<p className="text-xl font-thin">Upload File</p>
|
||||||
|
</div>
|
||||||
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||||
|
<div className="sm:col-span-3 col-span-5">
|
||||||
|
<p className="mb-2">File</p>
|
||||||
|
<label className="btn h-10 btn-sm w-full border border-neutral-content hover:border-neutral-content flex justify-between">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.png,.jpg,.jpeg"
|
||||||
|
className="cursor-pointer custom-file-input"
|
||||||
|
onChange={(e) => e.target.files && setFile(e.target.files[0])}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs font-semibold mt-2">
|
||||||
|
PDF, PNG, JPG (Up to {process.env.NEXT_PUBLIC_MAX_UPLOAD_SIZE || 30}
|
||||||
|
MB)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2 col-span-5">
|
||||||
|
<p className="mb-2">Collection</p>
|
||||||
|
{link.collection.name ? (
|
||||||
|
<CollectionSelection
|
||||||
|
onChange={setCollection}
|
||||||
|
defaultValue={{
|
||||||
|
label: link.collection.name,
|
||||||
|
value: link.collection.id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{optionsExpanded ? (
|
||||||
|
<div className="mt-5">
|
||||||
|
{/* <hr className="mb-3 border border-neutral-content" /> */}
|
||||||
|
<div className="grid sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Name</p>
|
||||||
|
<TextInput
|
||||||
|
value={link.name}
|
||||||
|
onChange={(e) => setLink({ ...link, name: e.target.value })}
|
||||||
|
placeholder="e.g. Example Link"
|
||||||
|
className="bg-base-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="mb-2">Tags</p>
|
||||||
|
<TagSelection
|
||||||
|
onChange={setTags}
|
||||||
|
defaultValue={link.tags.map((e) => {
|
||||||
|
return { label: e.name, value: e.id };
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<p className="mb-2">Description</p>
|
||||||
|
<textarea
|
||||||
|
value={unescapeString(link.description) as string}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLink({ ...link, description: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Will be auto generated if nothing is provided."
|
||||||
|
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-200 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : undefined}
|
||||||
|
<div className="flex justify-between items-center mt-5">
|
||||||
|
<div
|
||||||
|
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||||
|
className={`rounded-md cursor-pointer btn btn-sm btn-ghost duration-100 flex items-center px-2 w-fit text-sm`}
|
||||||
|
>
|
||||||
|
<p>{optionsExpanded ? "Hide" : "More"} Options</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="btn btn-accent text-white" onClick={submit}>
|
||||||
|
Create Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,42 +1,40 @@
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { faPlus, faBars } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faBars, faCaretDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Dropdown from "@/components/Dropdown";
|
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||||
import Sidebar from "@/components/Sidebar";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import SearchBar from "@/components/SearchBar";
|
import SearchBar from "@/components/SearchBar";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||||
import ToggleDarkMode from "./ToggleDarkMode";
|
import ToggleDarkMode from "./ToggleDarkMode";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||||
|
import NewCollectionModal from "./ModalContent/NewCollectionModal";
|
||||||
|
import Link from "next/link";
|
||||||
|
import UploadFileModal from "./ModalContent/UploadFileModal";
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const { setModal } = useModalStore();
|
const { settings, updateSettings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const { account } = useAccountStore();
|
const { account } = useAccountStore();
|
||||||
|
|
||||||
const [profileDropdown, setProfileDropdown] = useState(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { theme, setTheme } = useTheme();
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
|
||||||
if (theme === "dark") {
|
|
||||||
setTheme("light");
|
|
||||||
} else {
|
|
||||||
setTheme("dark");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [sidebar, setSidebar] = useState(false);
|
const [sidebar, setSidebar] = useState(false);
|
||||||
|
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (settings.theme === "dark") {
|
||||||
|
updateSettings({ theme: "light" });
|
||||||
|
} else {
|
||||||
|
updateSettings({ theme: "dark" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSidebar(false);
|
setSidebar(false);
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
@ -49,99 +47,139 @@ export default function Navbar() {
|
||||||
setSidebar(!sidebar);
|
setSidebar(!sidebar);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||||
|
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||||
|
const [uploadFileModal, setUploadFileModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between gap-2 items-center px-5 py-2 border-solid border-b-sky-100 dark:border-b-neutral-700 border-b h-16">
|
<div className="flex justify-between gap-2 items-center px-4 py-2 border-solid border-b-neutral-content border-b">
|
||||||
<div
|
<div
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-[0.687rem] text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
className="text-neutral btn btn-square btn-sm btn-ghost lg:hidden"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<ToggleDarkMode className="sm:inline-grid hidden" />
|
||||||
onClick={() => {
|
|
||||||
setModal({
|
<div className="dropdown dropdown-end">
|
||||||
modal: "LINK",
|
<div className="tooltip tooltip-bottom" data-tip="Create New...">
|
||||||
state: true,
|
<div
|
||||||
method: "CREATE",
|
tabIndex={0}
|
||||||
});
|
role="button"
|
||||||
}}
|
className="flex items-center group btn btn-accent text-white btn-sm px-2"
|
||||||
className="inline-flex gap-1 relative sm:w-[7.2rem] items-center font-semibold select-none cursor-pointer p-[0.687rem] sm:p-2 sm:px-3 rounded-md sm:rounded-full hover:bg-sky-100 dark:hover:bg-sky-800 sm:dark:hover:bg-sky-600 text-sky-500 sm:text-white sm:bg-sky-700 sm:hover:bg-sky-600 duration-100 group"
|
>
|
||||||
>
|
<FontAwesomeIcon icon={faPlus} className="w-5 h-5" />
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faPlus}
|
icon={faCaretDown}
|
||||||
className="w-5 h-5 sm:group-hover:ml-9 sm:absolute duration-100"
|
className="w-2 h-2 sm:w-3 sm:h-3"
|
||||||
/>
|
/>
|
||||||
<span className="hidden sm:block group-hover:opacity-0 text-right w-full duration-100">
|
</div>
|
||||||
New Link
|
</div>
|
||||||
</span>
|
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setNewLinkModal(true);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
New Link
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/* <li>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setUploadFileModal(true);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Upload File
|
||||||
|
</div>
|
||||||
|
</li> */}
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setNewCollectionModal(true);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
New Collection
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToggleDarkMode className="sm:flex hidden" />
|
<div className="dropdown dropdown-end">
|
||||||
|
<div tabIndex={0} role="button" className="btn btn-circle btn-ghost">
|
||||||
<div className="relative">
|
|
||||||
<div
|
|
||||||
className="flex gap-1 group sm:hover:bg-slate-200 sm:hover:dark:bg-neutral-700 sm:hover:p-1 sm:hover:pr-2 duration-100 h-10 rounded-full items-center w-fit cursor-pointer"
|
|
||||||
onClick={() => setProfileDropdown(!profileDropdown)}
|
|
||||||
id="profile-dropdown"
|
|
||||||
>
|
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
src={account.image ? account.image : undefined}
|
src={account.image ? account.image : undefined}
|
||||||
priority={true}
|
priority={true}
|
||||||
className="sm:group-hover:h-8 sm:group-hover:w-8 duration-100 border-[3px]"
|
|
||||||
/>
|
/>
|
||||||
<p
|
|
||||||
id="profile-dropdown"
|
|
||||||
className="text-black dark:text-white leading-3 hidden sm:block select-none truncate max-w-[8rem] py-1"
|
|
||||||
>
|
|
||||||
{account.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{profileDropdown ? (
|
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||||
<Dropdown
|
<li>
|
||||||
items={[
|
<Link
|
||||||
{
|
href="/settings/account"
|
||||||
name: "Settings",
|
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
||||||
href: "/settings/account",
|
tabIndex={0}
|
||||||
},
|
role="button"
|
||||||
{
|
|
||||||
name: `Switch to ${theme === "light" ? "Dark" : "Light"}`,
|
|
||||||
onClick: () => {
|
|
||||||
handleToggle();
|
|
||||||
setProfileDropdown(!profileDropdown);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Logout",
|
|
||||||
onClick: () => {
|
|
||||||
signOut();
|
|
||||||
setProfileDropdown(!profileDropdown);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClickOutside={(e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
if (target.id !== "profile-dropdown") setProfileDropdown(false);
|
|
||||||
}}
|
|
||||||
className="absolute top-11 right-0 z-20 w-36"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{sidebar ? (
|
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
|
||||||
<ClickAwayHandler
|
|
||||||
className="h-full"
|
|
||||||
onClickOutside={toggleSidebar}
|
|
||||||
>
|
>
|
||||||
<div className="slide-right h-full shadow-lg">
|
Settings
|
||||||
<Sidebar />
|
</Link>
|
||||||
</div>
|
</li>
|
||||||
</ClickAwayHandler>
|
<li>
|
||||||
</div>
|
<div
|
||||||
) : null}
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
handleToggle();
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Switch to {settings.theme === "light" ? "Dark" : "Light"}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
signOut();
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{sidebar ? (
|
||||||
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
||||||
|
<ClickAwayHandler className="h-full" onClickOutside={toggleSidebar}>
|
||||||
|
<div className="slide-right h-full shadow-lg">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</ClickAwayHandler>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{newLinkModal ? (
|
||||||
|
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||||
|
) : undefined}
|
||||||
|
{newCollectionModal ? (
|
||||||
|
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||||
|
) : undefined}
|
||||||
|
{uploadFileModal ? (
|
||||||
|
<UploadFileModal onClose={() => setUploadFileModal(false)} />
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import useModalStore from "@/store/modals";
|
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
text?: string;
|
text?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NoLinksFound({ text }: Props) {
|
export default function NoLinksFound({ text }: Props) {
|
||||||
const { setModal } = useModalStore();
|
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
||||||
<p className="text-center text-2xl text-black dark:text-white">
|
<p className="text-center text-2xl">
|
||||||
{text || "You haven't created any Links Here"}
|
{text || "You haven't created any Links Here"}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-center text-black dark:text-white w-full mt-4">
|
<div className="text-center w-full mt-4">
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModal({
|
setNewLinkModal(true);
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
method: "CREATE",
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-full 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
|
<FontAwesomeIcon
|
||||||
icon={faPlus}
|
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">
|
<span className="group-hover:opacity-0 text-right w-full duration-100">
|
||||||
Create New Link
|
Create New Link
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{newLinkModal ? (
|
||||||
|
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,18 @@ import Image from "next/image";
|
||||||
type Props = {
|
type Props = {
|
||||||
src?: string;
|
src?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
emptyImage?: boolean;
|
|
||||||
priority?: boolean;
|
priority?: boolean;
|
||||||
|
name?: string;
|
||||||
|
dimensionClass?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ProfilePhoto({ src, className, priority }: Props) {
|
export default function ProfilePhoto({
|
||||||
|
src,
|
||||||
|
className,
|
||||||
|
priority,
|
||||||
|
name,
|
||||||
|
dimensionClass,
|
||||||
|
}: Props) {
|
||||||
const [image, setImage] = useState("");
|
const [image, setImage] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -24,24 +31,39 @@ export default function ProfilePhoto({ src, className, priority }: Props) {
|
||||||
|
|
||||||
return !image ? (
|
return !image ? (
|
||||||
<div
|
<div
|
||||||
className={`bg-sky-600 dark:bg-sky-600 text-white h-10 w-10 aspect-square shadow rounded-full border border-slate-200 dark:border-neutral-700 flex items-center justify-center ${
|
className={`avatar drop-shadow-md placeholder ${className || ""} ${
|
||||||
className || ""
|
dimensionClass || "w-8 h-8 "
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUser} className="w-1/2 h-1/2 aspect-square" />
|
<div className="bg-base-100 text-neutral rounded-full w-full h-full ring-2 ring-neutral-content">
|
||||||
|
{name ? (
|
||||||
|
<span className="text-2xl capitalize">{name.slice(0, 1)}</span>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUser}
|
||||||
|
className="w-1/2 h-1/2 aspect-square"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<div
|
||||||
alt=""
|
className={`avatar drop-shadow-md ${className || ""} ${
|
||||||
src={image}
|
dimensionClass || "w-8 h-8 "
|
||||||
height={112}
|
|
||||||
width={112}
|
|
||||||
priority={priority}
|
|
||||||
draggable={false}
|
|
||||||
onError={() => setImage("")}
|
|
||||||
className={`h-10 w-10 bg-sky-600 dark:bg-sky-600 shadow rounded-full aspect-square border border-slate-200 dark:border-neutral-700 ${
|
|
||||||
className || ""
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
>
|
||||||
|
<div className="rounded-full w-full h-full ring-2 ring-neutral-content">
|
||||||
|
<Image
|
||||||
|
alt=""
|
||||||
|
src={image}
|
||||||
|
height={112}
|
||||||
|
width={112}
|
||||||
|
priority={priority}
|
||||||
|
draggable={false}
|
||||||
|
onError={() => setImage("")}
|
||||||
|
className="aspect-square rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Link as LinkType, Tag } from "@prisma/client";
|
import { Link as LinkType, Tag } from "@prisma/client";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import { TagIncludingLinkCount } from "@/types/global";
|
import { TagIncludingLinkCount } from "@/types/global";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
@ -17,7 +17,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LinkCard({ link, count }: Props) {
|
export default function LinkCard({ link, count }: Props) {
|
||||||
const url = isValidUrl(link.url) ? new URL(link.url) : undefined;
|
const url = link.url && isValidUrl(link.url) ? new URL(link.url) : undefined;
|
||||||
|
|
||||||
const formattedDate = new Date(
|
const formattedDate = new Date(
|
||||||
link.createdAt as unknown as string
|
link.createdAt as unknown as string
|
||||||
|
@ -28,7 +28,7 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-solid border-sky-100 dark:border-neutral-700 bg-gradient-to-tr from-slate-200 dark:from-neutral-800 from-10% to-gray-50 dark:to-[#303030] via-20% shadow hover:shadow-none duration-100 rounded-lg p-3 flex items-start relative gap-3 group/item">
|
<div className="border border-solid border-neutral-content bg-base-200 shadow hover:shadow-none duration-100 rounded-lg p-3 flex items-start relative gap-3 group/item">
|
||||||
<div className="flex justify-between items-end gap-5 w-full h-full z-0">
|
<div className="flex justify-between items-end gap-5 w-full h-full z-0">
|
||||||
<div className="flex flex-col justify-between w-full">
|
<div className="flex flex-col justify-between w-full">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
@ -57,21 +57,21 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
<Link
|
<Link
|
||||||
href={"/public/collections/20?q=" + e.name}
|
href={"/public/collections/20?q=" + e.name}
|
||||||
key={i}
|
key={i}
|
||||||
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]"
|
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
>
|
>
|
||||||
{e.name}
|
#{e.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 items-center flex-wrap text-sm text-gray-500 dark:text-gray-300">
|
<div className="flex gap-1 items-center flex-wrap text-sm text-neutral">
|
||||||
<p>{formattedDate}</p>
|
<p>{formattedDate}</p>
|
||||||
<p>·</p>
|
<p>·</p>
|
||||||
<Link
|
<Link
|
||||||
href={url ? url.href : link.url}
|
href={url ? url.href : link.url || ""}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="hover:opacity-50 duration-100 truncate w-52 sm:w-fit"
|
className="hover:opacity-50 duration-100 truncate w-52 sm:w-fit"
|
||||||
title={url ? url.href : link.url}
|
title={url ? url.href : link.url || ""}
|
||||||
>
|
>
|
||||||
{url ? url.host : link.url}
|
{url ? url.host : link.url}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -80,7 +80,7 @@ export default function LinkCard({ link, count }: Props) {
|
||||||
{unescapeString(link.description)}{" "}
|
{unescapeString(link.description)}{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/public/links/${link.id}`}
|
href={`/public/links/${link.id}`}
|
||||||
className="flex gap-1 items-center flex-wrap text-sm text-gray-500 dark:text-gray-300 hover:opacity-50 duration-100 min-w-fit float-right mt-1 ml-2"
|
className="flex gap-1 items-center flex-wrap text-sm text-neutral hover:opacity-50 duration-100 min-w-fit float-right mt-1 ml-2"
|
||||||
>
|
>
|
||||||
<p>Read</p>
|
<p>Read</p>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { toast } from "react-hot-toast";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
placeHolder?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function PublicSearchBar({ placeHolder }: Props) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.query.q
|
|
||||||
? setSearchQuery(decodeURIComponent(router.query.q as string))
|
|
||||||
: setSearchQuery("");
|
|
||||||
}, [router.query.q]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center relative group">
|
|
||||||
<label
|
|
||||||
htmlFor="search-box"
|
|
||||||
className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md text-sky-500 dark:text-sky-500"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="w-4 h-4" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="search-box"
|
|
||||||
type="text"
|
|
||||||
placeholder={placeHolder}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => {
|
|
||||||
e.target.value.includes("%") &&
|
|
||||||
toast.error("The search query should not contain '%'.");
|
|
||||||
setSearchQuery(e.target.value.replace("%", ""));
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
if (!searchQuery) {
|
|
||||||
return router.push("/public/collections/" + router.query.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return router.push(
|
|
||||||
"/public/collections/" +
|
|
||||||
router.query.id +
|
|
||||||
"?q=" +
|
|
||||||
encodeURIComponent(searchQuery || "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="border text-sm border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-8 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -20,15 +20,13 @@ export default function RadioButton({ label, state, onClick }: Props) {
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircleCheck}
|
icon={faCircleCheck}
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:block hidden"
|
className="w-5 h-5 text-primary peer-checked:block hidden"
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 peer-checked:hidden block"
|
className="w-5 h-5 text-primary peer-checked:hidden block"
|
||||||
/>
|
/>
|
||||||
<span className="text-black dark:text-white rounded select-none">
|
<span className="rounded select-none">{label}</span>
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
export default function RequiredBadge() {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
title="Required Field"
|
|
||||||
className="text-black dark:text-white cursor-help"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +1,29 @@
|
||||||
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
export default function SearchBar() {
|
type Props = {
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SearchBar({ placeholder }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const routeQuery = router.query.q;
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState(
|
useEffect(() => {
|
||||||
routeQuery ? decodeURIComponent(routeQuery as string) : ""
|
router.query.q
|
||||||
);
|
? setSearchQuery(decodeURIComponent(router.query.q as string))
|
||||||
|
: setSearchQuery("");
|
||||||
|
}, [router.query.q]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center relative group">
|
<div className="flex items-center relative group">
|
||||||
<label
|
<label
|
||||||
htmlFor="search-box"
|
htmlFor="search-box"
|
||||||
className="inline-flex w-fit absolute left-2 pointer-events-none rounded-md p-1 text-sky-500 dark:text-sky-500"
|
className="inline-flex w-fit absolute left-1 pointer-events-none rounded-md p-1 text-primary"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faMagnifyingGlass} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faMagnifyingGlass} className="w-5 h-5" />
|
||||||
</label>
|
</label>
|
||||||
|
@ -25,18 +31,34 @@ export default function SearchBar() {
|
||||||
<input
|
<input
|
||||||
id="search-box"
|
id="search-box"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for Links"
|
placeholder={placeholder || "Search for Links"}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.target.value.includes("%") &&
|
e.target.value.includes("%") &&
|
||||||
toast.error("The search query should not contain '%'.");
|
toast.error("The search query should not contain '%'.");
|
||||||
setSearchQuery(e.target.value.replace("%", ""));
|
setSearchQuery(e.target.value.replace("%", ""));
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) => {
|
||||||
e.key === "Enter" &&
|
if (e.key === "Enter") {
|
||||||
router.push("/search?q=" + encodeURIComponent(searchQuery))
|
if (router.pathname.startsWith("/public")) {
|
||||||
}
|
if (!searchQuery) {
|
||||||
className="border border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 rounded-md pl-10 py-2 pr-2 w-44 sm:w-60 dark:hover:border-neutral-600 md:focus:w-80 hover:border-sky-300 duration-100 outline-none dark:bg-neutral-800"
|
return router.push("/public/collections/" + router.query.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(
|
||||||
|
"/public/collections/" +
|
||||||
|
router.query.id +
|
||||||
|
"?q=" +
|
||||||
|
encodeURIComponent(searchQuery || "")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return router.push(
|
||||||
|
"/search?q=" + encodeURIComponent(searchQuery)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-44 sm:w-60 md:focus:w-80 duration-100 outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from "@fortawesome/free-brands-svg-icons";
|
} from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
export default function SettingsSidebar({ className }: { className?: string }) {
|
export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
const LINKWARDEN_VERSION = "v2.3.0";
|
const LINKWARDEN_VERSION = "v2.4.0";
|
||||||
|
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`dark:bg-neutral-900 bg-white h-full w-64 overflow-y-auto border-solid border-white border dark:border-neutral-900 border-r-sky-100 dark:border-r-neutral-700 p-5 z-20 flex flex-col gap-5 justify-between ${
|
className={`bg-base-100 h-full w-64 overflow-y-auto border-solid border border-base-100 border-r-neutral-content p-5 z-20 flex flex-col gap-5 justify-between ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -44,18 +44,13 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/account`
|
active === `/settings/account`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faUser} className="w-7 h-7 text-primary" />
|
||||||
icon={faUser}
|
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Account</p>
|
||||||
Account
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -63,18 +58,16 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/appearance`
|
active === `/settings/appearance`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faPalette}
|
icon={faPalette}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Appearance</p>
|
||||||
Appearance
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -82,35 +75,30 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/archive`
|
active === `/settings/archive`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faBoxArchive}
|
icon={faBoxArchive}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Archive</p>
|
||||||
Archive
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="/settings/api">
|
<Link href="/settings/api">
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/api` ? "bg-sky-500" : "hover:bg-slate-500"
|
active === `/settings/api`
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
? "bg-primary/20"
|
||||||
|
: "hover:bg-neutral/20"
|
||||||
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faKey} className="w-7 h-7 text-primary" />
|
||||||
icon={faKey}
|
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">API Keys</p>
|
||||||
API Keys
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -118,18 +106,13 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/password`
|
active === `/settings/password`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faLock} className="w-7 h-7 text-primary" />
|
||||||
icon={faLock}
|
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Password</p>
|
||||||
Password
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
@ -138,18 +121,16 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/settings/billing`
|
active === `/settings/billing`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCreditCard}
|
icon={faCreditCard}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Billing</p>
|
||||||
Billing
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -159,67 +140,59 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||||
<Link
|
<Link
|
||||||
href={`https://github.com/linkwarden/linkwarden/releases`}
|
href={`https://github.com/linkwarden/linkwarden/releases`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="dark:text-gray-300 text-gray-500 text-sm ml-2 hover:opacity-50 duration-100"
|
className="text-neutral text-sm ml-2 hover:opacity-50 duration-100"
|
||||||
>
|
>
|
||||||
Linkwarden {LINKWARDEN_VERSION}
|
Linkwarden {LINKWARDEN_VERSION}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="https://docs.linkwarden.app" target="_blank">
|
<Link href="https://docs.linkwarden.app" target="_blank">
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-slate-500 duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircleQuestion as any}
|
icon={faCircleQuestion as any}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-6 h-6 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Help</p>
|
||||||
Help
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="https://github.com/linkwarden/linkwarden" target="_blank">
|
<Link href="https://github.com/linkwarden/linkwarden" target="_blank">
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-slate-500 duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faGithub as any}
|
icon={faGithub as any}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-6 h-6 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">GitHub</p>
|
||||||
GitHub
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="https://twitter.com/LinkwardenHQ" target="_blank">
|
<Link href="https://twitter.com/LinkwardenHQ" target="_blank">
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-slate-500 duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faXTwitter as any}
|
icon={faXTwitter as any}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-6 h-6 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Twitter</p>
|
||||||
Twitter
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="https://fosstodon.org/@linkwarden" target="_blank">
|
<Link href="https://fosstodon.org/@linkwarden" target="_blank">
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-slate-500 duration-100 py-2 px-2 hover:bg-opacity-20 bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
className={`hover:bg-neutral/20 duration-100 py-2 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faMastodon as any}
|
icon={faMastodon as any}
|
||||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
className="w-6 h-6 text-primary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">Mastodon</p>
|
||||||
Mastodon
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-gray-100 dark:bg-neutral-800 h-full w-64 xl:w-80 overflow-y-auto border-solid border dark:border-neutral-800 border-r-sky-100 dark:border-r-neutral-700 px-2 z-20 ${
|
className={`bg-base-200 h-full w-64 xl:w-80 overflow-y-auto border-solid border border-base-200 border-r-neutral-content px-2 z-20 ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -60,64 +60,60 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<Link href={`/dashboard`}>
|
<Link href={`/dashboard`}>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/dashboard` ? "bg-sky-500" : "hover:bg-slate-500"
|
active === `/dashboard` ? "bg-primary/20" : "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faChartSimple}
|
icon={faChartSimple}
|
||||||
className="w-7 h-7 drop-shadow text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 drop-shadow text-primary"
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate w-full">
|
<p className="truncate w-full">Dashboard</p>
|
||||||
Dashboard
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href={`/links`}>
|
<Link href={`/links`}>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/links` ? "bg-sky-500" : "hover:bg-slate-500"
|
active === `/links` ? "bg-primary/20" : "hover:bg-neutral/20"
|
||||||
} duration-100 py-5 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faLink}
|
icon={faLink}
|
||||||
className="w-7 h-7 drop-shadow text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 drop-shadow text-primary"
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate w-full">
|
<p className="truncate w-full">All Links</p>
|
||||||
All Links
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href={`/collections`}>
|
<Link href={`/collections`}>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/collections` ? "bg-sky-500" : "hover:bg-slate-500"
|
active === `/collections`
|
||||||
} duration-100 py-5 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
? "bg-primary/20"
|
||||||
|
: "hover:bg-neutral/20"
|
||||||
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
className="w-7 h-7 drop-shadow text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 drop-shadow text-primary"
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate w-full">
|
<p className="truncate w-full">All Collections</p>
|
||||||
All Collections
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href={`/links/pinned`}>
|
<Link href={`/links/pinned`}>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/links/pinned` ? "bg-sky-500" : "hover:bg-slate-500"
|
active === `/links/pinned`
|
||||||
} duration-100 py-5 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
? "bg-primary/20"
|
||||||
|
: "hover:bg-neutral/20"
|
||||||
|
} duration-100 py-5 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faThumbTack}
|
icon={faThumbTack}
|
||||||
className="w-7 h-7 drop-shadow text-sky-500 dark:text-sky-500"
|
className="w-7 h-7 drop-shadow text-primary"
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate w-full">
|
<p className="truncate w-full">Pinned Links</p>
|
||||||
Pinned Links
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,7 +123,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCollectionDisclosure(!collectionDisclosure);
|
setCollectionDisclosure(!collectionDisclosure);
|
||||||
}}
|
}}
|
||||||
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 dark:text-gray-300 mt-5"
|
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-neutral mt-5"
|
||||||
>
|
>
|
||||||
<p>Collections</p>
|
<p>Collections</p>
|
||||||
|
|
||||||
|
@ -156,27 +152,25 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/collections/${e.id}`
|
active === `/collections/${e.id}`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-1 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
className="w-6 h-6 drop-shadow"
|
className="w-6 h-6 drop-shadow"
|
||||||
style={{ color: e.color }}
|
style={{ color: e.color }}
|
||||||
/>
|
/>
|
||||||
<p className="text-black dark:text-white truncate w-full">
|
<p className="truncate w-full">{e.name}</p>
|
||||||
{e.name}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{e.isPublic ? (
|
{e.isPublic ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faGlobe}
|
icon={faGlobe}
|
||||||
title="This collection is being shared publicly."
|
title="This collection is being shared publicly."
|
||||||
className="w-4 h-4 drop-shadow text-gray-500 dark:text-gray-300"
|
className="w-4 h-4 drop-shadow text-neutral"
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<div className="drop-shadow text-gray-500 dark:text-gray-300 text-xs">
|
<div className="drop-shadow text-neutral text-xs">
|
||||||
{e._count?.links}
|
{e._count?.links}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,7 +181,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<p className="text-gray-500 dark:text-gray-300 text-xs font-semibold truncate w-full pr-7">
|
<p className="text-neutral text-xs font-semibold truncate w-full pr-7">
|
||||||
You Have No Collections...
|
You Have No Collections...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -200,7 +194,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTagDisclosure(!tagDisclosure);
|
setTagDisclosure(!tagDisclosure);
|
||||||
}}
|
}}
|
||||||
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-gray-500 dark:text-gray-300 mt-5"
|
className="flex items-center justify-between text-sm w-full text-left mb-2 pl-2 font-bold text-neutral mt-5"
|
||||||
>
|
>
|
||||||
<p>Tags</p>
|
<p>Tags</p>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -226,19 +220,17 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
active === `/tags/${e.id}`
|
active === `/tags/${e.id}`
|
||||||
? "bg-sky-500"
|
? "bg-primary/20"
|
||||||
: "hover:bg-slate-500"
|
: "hover:bg-neutral/20"
|
||||||
} duration-100 py-1 px-2 bg-opacity-20 hover:bg-opacity-20 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHashtag}
|
icon={faHashtag}
|
||||||
className="w-4 h-4 text-sky-500 dark:text-sky-500 mt-1"
|
className="w-4 h-4 text-primary mt-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
<p className="truncate w-full pr-7">{e.name}</p>
|
||||||
{e.name}
|
<div className="drop-shadow text-neutral text-xs">
|
||||||
</p>
|
|
||||||
<div className="drop-shadow text-gray-500 dark:text-gray-300 text-xs">
|
|
||||||
{e._count?.links}
|
{e._count?.links}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -249,7 +241,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||||
<div
|
<div
|
||||||
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
className={`duration-100 py-1 px-2 flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||||
>
|
>
|
||||||
<p className="text-gray-500 dark:text-gray-300 text-xs font-semibold truncate w-full pr-7">
|
<p className="text-neutral text-xs font-semibold truncate w-full pr-7">
|
||||||
You Have No Tags...
|
You Have No Tags...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,68 +1,133 @@
|
||||||
import React, { Dispatch, SetStateAction } from "react";
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
import ClickAwayHandler from "./ClickAwayHandler";
|
|
||||||
import RadioButton from "./RadioButton";
|
|
||||||
import { Sort } from "@/types/global";
|
import { Sort } from "@/types/global";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faSort } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
sortBy: Sort;
|
sortBy: Sort;
|
||||||
setSort: Dispatch<SetStateAction<Sort>>;
|
setSort: Dispatch<SetStateAction<Sort>>;
|
||||||
|
|
||||||
toggleSortDropdown: Function;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SortDropdown({
|
export default function SortDropdown({ sortBy, setSort }: Props) {
|
||||||
sortBy,
|
|
||||||
toggleSortDropdown,
|
|
||||||
setSort,
|
|
||||||
}: Props) {
|
|
||||||
return (
|
return (
|
||||||
<ClickAwayHandler
|
<div className="dropdown dropdown-bottom dropdown-end">
|
||||||
onClickOutside={(e: Event) => {
|
<div
|
||||||
const target = e.target as HTMLInputElement;
|
tabIndex={0}
|
||||||
if (target.id !== "sort-dropdown") toggleSortDropdown();
|
role="button"
|
||||||
}}
|
className="btn btn-sm btn-square btn-ghost"
|
||||||
className="absolute top-8 right-0 border border-sky-100 dark:border-neutral-700 shadow-md bg-gray-50 dark:bg-neutral-800 rounded-md p-2 z-20 w-52"
|
>
|
||||||
>
|
<FontAwesomeIcon
|
||||||
<p className="mb-2 text-black dark:text-white text-center font-semibold">
|
icon={faSort}
|
||||||
Sort by
|
id="sort-dropdown"
|
||||||
</p>
|
className="w-5 h-5 text-neutral"
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<RadioButton
|
|
||||||
label="Date (Newest First)"
|
|
||||||
state={sortBy === Sort.DateNewestFirst}
|
|
||||||
onClick={() => setSort(Sort.DateNewestFirst)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
label="Date (Oldest First)"
|
|
||||||
state={sortBy === Sort.DateOldestFirst}
|
|
||||||
onClick={() => setSort(Sort.DateOldestFirst)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
label="Name (A-Z)"
|
|
||||||
state={sortBy === Sort.NameAZ}
|
|
||||||
onClick={() => setSort(Sort.NameAZ)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
label="Name (Z-A)"
|
|
||||||
state={sortBy === Sort.NameZA}
|
|
||||||
onClick={() => setSort(Sort.NameZA)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
label="Description (A-Z)"
|
|
||||||
state={sortBy === Sort.DescriptionAZ}
|
|
||||||
onClick={() => setSort(Sort.DescriptionAZ)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
label="Description (Z-A)"
|
|
||||||
state={sortBy === Sort.DescriptionZA}
|
|
||||||
onClick={() => setSort(Sort.DescriptionZA)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayHandler>
|
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-xl w-52 mt-1">
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Date (Newest First)"
|
||||||
|
checked={sortBy === Sort.DateNewestFirst}
|
||||||
|
onChange={() => {
|
||||||
|
setSort(Sort.DateNewestFirst);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Date (Newest First)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Date (Oldest First)"
|
||||||
|
checked={sortBy === Sort.DateOldestFirst}
|
||||||
|
onChange={() => setSort(Sort.DateOldestFirst)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Date (Oldest First)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Name (A-Z)"
|
||||||
|
checked={sortBy === Sort.NameAZ}
|
||||||
|
onChange={() => setSort(Sort.NameAZ)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Name (A-Z)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Name (Z-A)"
|
||||||
|
checked={sortBy === Sort.NameZA}
|
||||||
|
onChange={() => setSort(Sort.NameZA)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Name (Z-A)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Description (A-Z)"
|
||||||
|
checked={sortBy === Sort.DescriptionAZ}
|
||||||
|
onChange={() => setSort(Sort.DescriptionAZ)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Description (A-Z)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className="label cursor-pointer flex justify-start"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="sort-radio"
|
||||||
|
className="radio checked:bg-primary"
|
||||||
|
value="Description (Z-A)"
|
||||||
|
checked={sortBy === Sort.DescriptionZA}
|
||||||
|
onChange={() => setSort(Sort.DescriptionZA)}
|
||||||
|
/>
|
||||||
|
<span className="label-text">Description (Z-A)</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,17 +21,15 @@ export default function SubmitButton({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type ? type : undefined}
|
type={type ? type : undefined}
|
||||||
className={`text-white flex items-center gap-2 py-2 px-5 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit ${
|
className={`btn btn-accent text-white tracking-wider w-fit flex items-center gap-2 ${
|
||||||
loading
|
className || ""
|
||||||
? "bg-sky-600 cursor-auto"
|
}`}
|
||||||
: "bg-sky-700 hover:bg-sky-600 cursor-pointer"
|
|
||||||
} ${className || ""}`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!loading && onClick) onClick();
|
if (!loading && onClick) onClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
|
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
|
||||||
<p className="text-center w-full">{label}</p>
|
<p>{label}</p>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function TextInput({
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className={`w-full rounded-md p-2 border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-950 border-solid border outline-none focus:border-sky-300 focus:dark:border-sky-600 duration-100 ${
|
className={`w-full rounded-md p-2 border-neutral-content border-solid border outline-none focus:border-primary duration-100 ${
|
||||||
className || ""
|
className || ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,31 +1,63 @@
|
||||||
import { useTheme } from "next-themes";
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { useEffect, useState } from "react";
|
||||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ToggleDarkMode({ className }: Props) {
|
export default function ToggleDarkMode({ className }: Props) {
|
||||||
const { theme, setTheme } = useTheme();
|
const { settings, updateSettings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const handleToggle = () => {
|
const [theme, setTheme] = useState(localStorage.getItem("theme"));
|
||||||
if (theme === "dark") {
|
|
||||||
setTheme("light");
|
const handleToggle = (e: any) => {
|
||||||
} else {
|
if (e.target.checked) {
|
||||||
setTheme("dark");
|
setTheme("dark");
|
||||||
|
} else {
|
||||||
|
setTheme("light");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSettings({ theme: theme as string });
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`cursor-pointer flex select-none border border-sky-600 items-center justify-center dark:bg-neutral-900 bg-white hover:border-sky-500 group duration-100 rounded-full text-white w-10 h-10 ${className}`}
|
className="tooltip tooltip-bottom"
|
||||||
onClick={handleToggle}
|
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<label
|
||||||
icon={theme === "dark" ? faSun : faMoon}
|
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}
|
||||||
className="w-1/2 h-1/2 text-sky-600 group-hover:text-sky-500"
|
>
|
||||||
/>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
onChange={handleToggle}
|
||||||
|
className="theme-controller"
|
||||||
|
checked={localStorage.getItem("theme") === "light" ? false : true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* sun icon */}
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className="swap-on fill-current w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* moon icon */}
|
||||||
|
<svg
|
||||||
|
className="swap-off fill-current w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278" />
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import useTagStore from "@/store/tags";
|
import useTagStore from "@/store/tags";
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
|
||||||
export default function useInitialData() {
|
export default function useInitialData() {
|
||||||
const { status, data } = useSession();
|
const { status, data } = useSession();
|
||||||
|
@ -10,10 +11,12 @@ export default function useInitialData() {
|
||||||
const { setTags } = useTagStore();
|
const { setTags } = useTagStore();
|
||||||
// const { setLinks } = useLinkStore();
|
// const { setLinks } = useLinkStore();
|
||||||
const { account, setAccount } = useAccountStore();
|
const { account, setAccount } = useAccountStore();
|
||||||
|
const { setSettings } = useLocalSettingsStore();
|
||||||
|
|
||||||
// Get account info
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setSettings();
|
||||||
if (status === "authenticated") {
|
if (status === "authenticated") {
|
||||||
|
// Get account info
|
||||||
setAccount(data?.user.id as number);
|
setAccount(data?.user.id as number);
|
||||||
}
|
}
|
||||||
}, [status, data]);
|
}, [status, data]);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useTheme } from "next-themes";
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode, useEffect } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: string;
|
text?: string;
|
||||||
|
@ -9,44 +9,29 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CenteredForm({ text, children }: Props) {
|
export default function CenteredForm({ text, children }: Props) {
|
||||||
const { theme } = useTheme();
|
const { settings } = useLocalSettingsStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
|
<div className="absolute top-0 bottom-0 left-0 right-0 flex justify-center items-center p-5">
|
||||||
<div className="m-auto flex flex-col gap-2 w-full">
|
<div className="m-auto flex flex-col gap-2 w-full">
|
||||||
{theme ? (
|
{settings.theme ? (
|
||||||
<Image
|
<Image
|
||||||
src={`/linkwarden_${theme === "dark" ? "dark" : "light"}.png`}
|
src={`/linkwarden_${
|
||||||
|
settings.theme === "dark" ? "dark" : "light"
|
||||||
|
}.png`}
|
||||||
width={640}
|
width={640}
|
||||||
height={136}
|
height={136}
|
||||||
alt="Linkwarden"
|
alt="Linkwarden"
|
||||||
className="h-12 w-fit mx-auto"
|
className="h-12 w-fit mx-auto"
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{/* {theme === "dark" ? (
|
|
||||||
<Image
|
|
||||||
src="/linkwarden_dark.png"
|
|
||||||
width={640}
|
|
||||||
height={136}
|
|
||||||
alt="Linkwarden"
|
|
||||||
className="h-12 w-fit mx-auto"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
src="/linkwarden_light.png"
|
|
||||||
width={640}
|
|
||||||
height={136}
|
|
||||||
alt="Linkwarden"
|
|
||||||
className="h-12 w-fit mx-auto"
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
{text ? (
|
{text ? (
|
||||||
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold text-black dark:text-white px-2 text-center">
|
<p className="text-lg max-w-[30rem] min-w-80 w-full mx-auto font-semibold px-2 text-center">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
{children}
|
{children}
|
||||||
<p className="text-center text-xs text-gray-500 mb-5 dark:text-gray-400">
|
<p className="text-center text-xs text-neutral mb-5">
|
||||||
© {new Date().getFullYear()}{" "}
|
© {new Date().getFullYear()}{" "}
|
||||||
<Link href="https://linkwarden.app" className="font-semibold">
|
<Link href="https://linkwarden.app" className="font-semibold">
|
||||||
Linkwarden
|
Linkwarden
|
||||||
|
|
|
@ -19,6 +19,11 @@ import {
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
|
import EditLinkModal from "@/components/ModalContent/EditLinkModal";
|
||||||
|
import Link from "next/link";
|
||||||
|
import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -73,93 +78,81 @@ export default function LinkLayout({ children }: Props) {
|
||||||
setLinkCollection(collections.find((e) => e.id === link?.collection?.id));
|
setLinkCollection(collections.find((e) => e.id === link?.collection?.id));
|
||||||
}, [link, collections]);
|
}, [link, collections]);
|
||||||
|
|
||||||
|
const deleteLink = async () => {
|
||||||
|
const load = toast.loading("Deleting...");
|
||||||
|
|
||||||
|
const response = await removeLink(link?.id as number);
|
||||||
|
|
||||||
|
toast.dismiss(load);
|
||||||
|
|
||||||
|
response.ok && toast.success(`Link Deleted.`);
|
||||||
|
|
||||||
|
router.push("/dashboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||||
|
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||||
|
const [preservedFormatsModal, setPreservedFormatsModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalManagement />
|
|
||||||
|
|
||||||
<div className="flex mx-auto">
|
<div className="flex mx-auto">
|
||||||
{/* <div className="hidden lg:block fixed left-5 h-screen">
|
{/* <div className="hidden lg:block fixed left-5 h-screen">
|
||||||
<LinkSidebar />
|
<LinkSidebar />
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
|
<div className="w-full flex flex-col min-h-screen max-w-screen-md mx-auto p-5">
|
||||||
<div className="flex gap-3 mb-5 duration-100 items-center justify-between">
|
<div className="flex gap-3 mb-3 duration-100 items-center justify-between">
|
||||||
{/* <div
|
{/* <div
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="inline-flex lg:hidden 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="inline-flex lg:hidden gap-1 items-center select-none cursor-pointer p-2 text-neutral rounded-md duration-100 hover:bg-neutral-content"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<div
|
<Link
|
||||||
onClick={() => {
|
href={
|
||||||
if (router.pathname.startsWith("/public")) {
|
router.pathname.startsWith("/public")
|
||||||
router.push(
|
? `/public/collections/${
|
||||||
`/public/collections/${
|
|
||||||
linkCollection?.id || link?.collection.id
|
linkCollection?.id || link?.collection.id
|
||||||
}`
|
}`
|
||||||
);
|
: `/dashboard`
|
||||||
} else {
|
}
|
||||||
router.push(`/dashboard`);
|
className="inline-flex gap-1 btn btn-ghost btn-sm text-neutral px-2"
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="inline-flex gap-1 hover:opacity-60 items-center select-none cursor-pointer p-2 lg:p-0 lg:px-1 lg:my-2 text-gray-500 dark:text-gray-300 rounded-md duration-100"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||||
Back{" "}
|
<span className="capitalize">
|
||||||
<span className="hidden sm:inline-block">
|
{router.pathname.startsWith("/public")
|
||||||
to{" "}
|
? linkCollection?.name || link?.collection?.name
|
||||||
<span className="capitalize">
|
: "Dashboard"}
|
||||||
{router.pathname.startsWith("/public")
|
|
||||||
? linkCollection?.name || link?.collection?.name
|
|
||||||
: "Dashboard"}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
<div className="flex gap-5">
|
<div className="flex gap-3">
|
||||||
{link?.collection?.ownerId === userId ||
|
{link?.collection?.ownerId === userId ||
|
||||||
linkCollection?.members.some(
|
linkCollection?.members.some(
|
||||||
(e) => e.userId === userId && e.canUpdate
|
(e) => e.userId === userId && e.canUpdate
|
||||||
) ? (
|
) ? (
|
||||||
<div
|
<div
|
||||||
title="Edit"
|
title="Edit"
|
||||||
onClick={() => {
|
onClick={() => setEditLinkModal(true)}
|
||||||
link
|
className={`btn btn-ghost btn-square btn-sm`}
|
||||||
? setModal({
|
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
active: link,
|
|
||||||
method: "UPDATE",
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
}}
|
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faPen}
|
icon={faPen}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-4 h-4 text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => setPreservedFormatsModal(true)}
|
||||||
link
|
|
||||||
? setModal({
|
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
active: link,
|
|
||||||
method: "FORMATS",
|
|
||||||
})
|
|
||||||
: undefined;
|
|
||||||
}}
|
|
||||||
title="Preserved Formats"
|
title="Preserved Formats"
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
className={`btn btn-ghost btn-square btn-sm`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faBoxesStacked}
|
icon={faBoxesStacked}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-4 h-4 text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -168,18 +161,16 @@ export default function LinkLayout({ children }: Props) {
|
||||||
(e) => e.userId === userId && e.canDelete
|
(e) => e.userId === userId && e.canDelete
|
||||||
) ? (
|
) ? (
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
if (link?.id) {
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
removeLink(link.id);
|
e.shiftKey ? deleteLink() : setDeleteLinkModal(true);
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
className={`hover:opacity-60 duration-100 py-2 px-2 cursor-pointer flex items-center gap-4 w-full rounded-md h-8`}
|
className={`btn btn-ghost btn-square btn-sm`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faTrashCan}
|
icon={faTrashCan}
|
||||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
className="w-4 h-4 text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
@ -189,7 +180,7 @@ export default function LinkLayout({ children }: Props) {
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{sidebar ? (
|
{sidebar ? (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
className="h-full"
|
className="h-full"
|
||||||
onClickOutside={toggleSidebar}
|
onClickOutside={toggleSidebar}
|
||||||
|
@ -201,6 +192,24 @@ export default function LinkLayout({ children }: Props) {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{link && editLinkModal ? (
|
||||||
|
<EditLinkModal
|
||||||
|
onClose={() => setEditLinkModal(false)}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{link && deleteLinkModal ? (
|
||||||
|
<DeleteLinkModal
|
||||||
|
onClose={() => setDeleteLinkModal(false)}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{link && preservedFormatsModal ? (
|
||||||
|
<PreservedFormatsModal
|
||||||
|
onClose={() => setPreservedFormatsModal(false)}
|
||||||
|
activeLink={link}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -53,14 +53,14 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
<div className="gap-2 inline-flex mr-3">
|
<div className="gap-2 inline-flex mr-3">
|
||||||
<div
|
<div
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className="inline-flex lg:hidden 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="text-neutral btn btn-square btn-sm btn-ghost lg:hidden"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faBars} className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard"
|
href="/dashboard"
|
||||||
className="inline-flex w-fit 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="text-neutral btn btn-square btn-sm btn-ghost"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
|
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -69,7 +69,7 @@ export default function SettingsLayout({ children }: Props) {
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{sidebar ? (
|
{sidebar ? (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-black bg-opacity-10 backdrop-blur-sm flex items-center fade-in z-30">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
className="h-full"
|
className="h-full"
|
||||||
onClickOutside={toggleSidebar}
|
onClickOutside={toggleSidebar}
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
|
||||||
import getTitle from "@/lib/api/getTitle";
|
import getTitle from "@/lib/shared/getTitle";
|
||||||
import archive from "@/lib/api/archive";
|
import urlHandler from "@/lib/api/urlHandler";
|
||||||
import { UsersAndCollections } from "@prisma/client";
|
import { UsersAndCollections } from "@prisma/client";
|
||||||
import getPermission from "@/lib/api/getPermission";
|
import getPermission from "@/lib/api/getPermission";
|
||||||
import createFolder from "@/lib/api/storage/createFolder";
|
import createFolder from "@/lib/api/storage/createFolder";
|
||||||
|
import pdfHandler from "../../pdfHandler";
|
||||||
|
import validateUrlSize from "../../validateUrlSize";
|
||||||
|
import imageHandler from "../../imageHandler";
|
||||||
|
|
||||||
export default async function postLink(
|
export default async function postLink(
|
||||||
link: LinkIncludingShortenedCollectionAndTags,
|
link: LinkIncludingShortenedCollectionAndTags,
|
||||||
userId: number
|
userId: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
new URL(link.url);
|
if (link.url) new URL(link.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
response:
|
response:
|
||||||
|
@ -45,14 +48,33 @@ export default async function postLink(
|
||||||
const description =
|
const description =
|
||||||
link.description && link.description !== ""
|
link.description && link.description !== ""
|
||||||
? link.description
|
? link.description
|
||||||
: await getTitle(link.url);
|
: link.url
|
||||||
|
? await getTitle(link.url)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const validatedUrl = link.url ? await validateUrlSize(link.url) : undefined;
|
||||||
|
|
||||||
|
if (validatedUrl === null)
|
||||||
|
return { response: "File is too large to be stored.", status: 400 };
|
||||||
|
|
||||||
|
const contentType = validatedUrl?.get("content-type");
|
||||||
|
let linkType = "url";
|
||||||
|
let imageExtension = "png";
|
||||||
|
|
||||||
|
if (!link.url) linkType = link.type;
|
||||||
|
else if (contentType === "application/pdf") linkType = "pdf";
|
||||||
|
else if (contentType?.startsWith("image")) {
|
||||||
|
linkType = "image";
|
||||||
|
if (contentType === "image/jpeg") imageExtension = "jpeg";
|
||||||
|
else if (contentType === "image/png") imageExtension = "png";
|
||||||
|
}
|
||||||
|
|
||||||
const newLink = await prisma.link.create({
|
const newLink = await prisma.link.create({
|
||||||
data: {
|
data: {
|
||||||
url: link.url,
|
url: link.url,
|
||||||
name: link.name,
|
name: link.name,
|
||||||
description,
|
description,
|
||||||
readabilityPath: "pending",
|
type: linkType,
|
||||||
collection: {
|
collection: {
|
||||||
connectOrCreate: {
|
connectOrCreate: {
|
||||||
where: {
|
where: {
|
||||||
|
@ -91,7 +113,37 @@ export default async function postLink(
|
||||||
|
|
||||||
createFolder({ filePath: `archives/${newLink.collectionId}` });
|
createFolder({ filePath: `archives/${newLink.collectionId}` });
|
||||||
|
|
||||||
archive(newLink.id, newLink.url, userId);
|
newLink.url && linkType === "url"
|
||||||
|
? urlHandler(newLink.id, newLink.url, userId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newLink.url && linkType === "pdf"
|
||||||
|
? pdfHandler(newLink.id, newLink.url)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newLink.url && linkType === "image"
|
||||||
|
? imageHandler(newLink.id, newLink.url, imageExtension)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
!newLink.url && linkType === "pdf"
|
||||||
|
? await prisma.link.update({
|
||||||
|
where: { id: newLink.id },
|
||||||
|
data: {
|
||||||
|
pdfPath: "pending",
|
||||||
|
lastPreserved: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
!newLink.url && linkType === "image"
|
||||||
|
? await prisma.link.update({
|
||||||
|
where: { id: newLink.id },
|
||||||
|
data: {
|
||||||
|
screenshotPath: "pending",
|
||||||
|
lastPreserved: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return { response: newLink, status: 200 };
|
return { response: newLink, status: 200 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { prisma } from "@/lib/api/db";
|
||||||
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default async function imageHandler(
|
||||||
|
linkId: number,
|
||||||
|
url: string | null,
|
||||||
|
extension: string,
|
||||||
|
file?: string
|
||||||
|
) {
|
||||||
|
const pdf = await fetch(url as string).then((res) => res.blob());
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await pdf.arrayBuffer());
|
||||||
|
|
||||||
|
const linkExists = await prisma.link.findUnique({
|
||||||
|
where: { id: linkId },
|
||||||
|
});
|
||||||
|
|
||||||
|
linkExists
|
||||||
|
? await createFile({
|
||||||
|
data: buffer,
|
||||||
|
filePath: `archives/${linkExists.collectionId}/${linkId}.${extension}`,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await prisma.link.update({
|
||||||
|
where: { id: linkId },
|
||||||
|
data: {
|
||||||
|
screenshotPath: linkExists
|
||||||
|
? `archives/${linkExists.collectionId}/${linkId}.${extension}`
|
||||||
|
: null,
|
||||||
|
pdfPath: null,
|
||||||
|
readabilityPath: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { prisma } from "@/lib/api/db";
|
||||||
|
import createFile from "@/lib/api/storage/createFile";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default async function pdfHandler(
|
||||||
|
linkId: number,
|
||||||
|
url: string | null,
|
||||||
|
file?: string
|
||||||
|
) {
|
||||||
|
const targetLink = await prisma.link.update({
|
||||||
|
where: { id: linkId },
|
||||||
|
data: {
|
||||||
|
pdfPath: "pending",
|
||||||
|
lastPreserved: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdf = await fetch(url as string).then((res) => res.blob());
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await pdf.arrayBuffer());
|
||||||
|
|
||||||
|
const linkExists = await prisma.link.findUnique({
|
||||||
|
where: { id: linkId },
|
||||||
|
});
|
||||||
|
|
||||||
|
linkExists
|
||||||
|
? await createFile({
|
||||||
|
data: buffer,
|
||||||
|
filePath: `archives/${linkExists.collectionId}/${linkId}.pdf`,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await prisma.link.update({
|
||||||
|
where: { id: linkId },
|
||||||
|
data: {
|
||||||
|
pdfPath: linkExists
|
||||||
|
? `archives/${linkExists.collectionId}/${linkId}.pdf`
|
||||||
|
: null,
|
||||||
|
readabilityPath: null,
|
||||||
|
screenshotPath: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -97,7 +97,7 @@ export default async function readFile(filePath: string) {
|
||||||
return {
|
return {
|
||||||
file: "File not found.",
|
file: "File not found.",
|
||||||
contentType: "text/plain",
|
contentType: "text/plain",
|
||||||
status: 400,
|
status: 404,
|
||||||
};
|
};
|
||||||
else {
|
else {
|
||||||
const file = fs.readFileSync(creationPath);
|
const file = fs.readFileSync(creationPath);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { Readability } from "@mozilla/readability";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
export default async function archive(
|
export default async function urlHandler(
|
||||||
linkId: number,
|
linkId: number,
|
||||||
url: string,
|
url: string,
|
||||||
userId: number
|
userId: number
|
||||||
|
@ -37,6 +37,23 @@ export default async function archive(
|
||||||
|
|
||||||
const content = await page.content();
|
const content = await page.content();
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// const session = await page.context().newCDPSession(page);
|
||||||
|
|
||||||
|
// const doc = await session.send("Page.captureSnapshot", {
|
||||||
|
// format: "mhtml",
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const saveDocLocally = (doc: any) => {
|
||||||
|
// console.log(doc);
|
||||||
|
// return createFile({
|
||||||
|
// data: doc,
|
||||||
|
// filePath: `archives/${targetLink.collectionId}/${linkId}.mhtml`,
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// saveDocLocally(doc.data);
|
||||||
|
|
||||||
// Readability
|
// Readability
|
||||||
|
|
||||||
const window = new JSDOM("").window;
|
const window = new JSDOM("").window;
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default async function validateUrlSize(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: "HEAD" });
|
||||||
|
|
||||||
|
const totalSizeMB =
|
||||||
|
Number(response.headers.get("content-length")) / Math.pow(1024, 2);
|
||||||
|
if (totalSizeMB > 50) return null;
|
||||||
|
else return response.headers;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
export default function htmlDecode(input: string) {
|
export default function unescapeString(input: string) {
|
||||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||||
return doc.documentElement.textContent;
|
return doc.documentElement.textContent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"@prisma/client": "^4.16.2",
|
"@prisma/client": "^4.16.2",
|
||||||
"@stripe/stripe-js": "^1.54.1",
|
"@stripe/stripe-js": "^1.54.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/formidable": "^3.4.5",
|
||||||
"@types/node": "20.4.4",
|
"@types/node": "20.4.4",
|
||||||
"@types/nodemailer": "^6.4.8",
|
"@types/nodemailer": "^6.4.8",
|
||||||
"@types/react": "18.2.14",
|
"@types/react": "18.2.14",
|
||||||
|
@ -37,13 +38,13 @@
|
||||||
"dompurify": "^3.0.6",
|
"dompurify": "^3.0.6",
|
||||||
"eslint": "8.46.0",
|
"eslint": "8.46.0",
|
||||||
"eslint-config-next": "13.4.9",
|
"eslint-config-next": "13.4.9",
|
||||||
|
"formidable": "^3.5.1",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.4",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"lottie-web": "^5.12.2",
|
"lottie-web": "^5.12.2",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"next": "13.4.12",
|
"next": "13.4.12",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-themes": "^0.2.1",
|
|
||||||
"nodemailer": "^6.9.3",
|
"nodemailer": "^6.9.3",
|
||||||
"playwright": "^1.35.1",
|
"playwright": "^1.35.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
import { SessionProvider } from "next-auth/react";
|
import { SessionProvider } from "next-auth/react";
|
||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
|
@ -6,7 +6,6 @@ import Head from "next/head";
|
||||||
import AuthRedirect from "@/layouts/AuthRedirect";
|
import AuthRedirect from "@/layouts/AuthRedirect";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { ThemeProvider } from "next-themes";
|
|
||||||
|
|
||||||
export default function App({
|
export default function App({
|
||||||
Component,
|
Component,
|
||||||
|
@ -14,13 +13,6 @@ export default function App({
|
||||||
}: AppProps<{
|
}: AppProps<{
|
||||||
session: Session;
|
session: Session;
|
||||||
}>) {
|
}>) {
|
||||||
const defaultTheme: "light" | "dark" = "dark";
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!localStorage.getItem("theme"))
|
|
||||||
localStorage.setItem("theme", defaultTheme);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider
|
<SessionProvider
|
||||||
session={pageProps.session}
|
session={pageProps.session}
|
||||||
|
@ -50,17 +42,15 @@ export default function App({
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
</Head>
|
</Head>
|
||||||
<AuthRedirect>
|
<AuthRedirect>
|
||||||
<ThemeProvider attribute="class">
|
<Toaster
|
||||||
<Toaster
|
position="top-center"
|
||||||
position="top-center"
|
reverseOrder={false}
|
||||||
reverseOrder={false}
|
toastOptions={{
|
||||||
toastOptions={{
|
className:
|
||||||
className:
|
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-white",
|
||||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Component {...pageProps} />
|
||||||
<Component {...pageProps} />
|
|
||||||
</ThemeProvider>
|
|
||||||
</AuthRedirect>
|
</AuthRedirect>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,47 +3,142 @@ import readFile from "@/lib/api/storage/readFile";
|
||||||
import { getToken } from "next-auth/jwt";
|
import { getToken } from "next-auth/jwt";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { ArchivedFormat } from "@/types/global";
|
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) {
|
export default async function Index(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const linkId = Number(req.query.linkId);
|
const linkId = Number(req.query.linkId);
|
||||||
const format = Number(req.query.format);
|
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.pdf) suffix = ".pdf";
|
||||||
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
|
else if (format === ArchivedFormat.readability) suffix = "_readability.json";
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
if (!linkId || !suffix)
|
if (!linkId || !suffix)
|
||||||
return res.status(401).json({ response: "Invalid parameters." });
|
return res.status(401).json({ response: "Invalid parameters." });
|
||||||
|
|
||||||
const token = await getToken({ req });
|
if (req.method === "GET") {
|
||||||
const userId = token?.id;
|
const token = await getToken({ req });
|
||||||
|
const userId = token?.id;
|
||||||
|
|
||||||
const collectionIsAccessible = await prisma.collection.findFirst({
|
const collectionIsAccessible = await prisma.collection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
links: {
|
links: {
|
||||||
some: {
|
some: {
|
||||||
id: linkId,
|
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)
|
if (!collectionIsAccessible)
|
||||||
return res
|
return res
|
||||||
.status(401)
|
.status(401)
|
||||||
.json({ response: "You don't have access to this collection." });
|
.json({ response: "You don't have access to this collection." });
|
||||||
|
|
||||||
const { file, contentType, status } = await readFile(
|
const { file, contentType, status } = await readFile(
|
||||||
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
|
`archives/${collectionIsAccessible.id}/${linkId + suffix}`
|
||||||
);
|
);
|
||||||
res.setHeader("Content-Type", contentType).status(status as number);
|
|
||||||
|
|
||||||
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 type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import archive from "@/lib/api/archive";
|
import urlHandler from "@/lib/api/urlHandler";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import verifyUser from "@/lib/api/verifyUser";
|
import verifyUser from "@/lib/api/verifyUser";
|
||||||
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
|
|
||||||
const RE_ARCHIVE_LIMIT = Number(process.env.RE_ARCHIVE_LIMIT) || 5;
|
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.`,
|
} 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({
|
return res.status(200).json({
|
||||||
response: "Link is being archived.",
|
response: "Link is being archived.",
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,28 +41,26 @@ export default function ChooseUsername() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<CenteredForm>
|
||||||
<form onSubmit={submitUsername}>
|
<form onSubmit={submitUsername}>
|
||||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between 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">
|
<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 text-black dark:text-white font-extralight">
|
<p className="text-3xl text-center font-extralight">
|
||||||
Choose a Username
|
Choose a Username
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<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</p>
|
||||||
Username
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
placeholder="john"
|
placeholder="john"
|
||||||
value={inputedUsername}
|
value={inputedUsername}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setInputedUsername(e.target.value)}
|
onChange={(e) => setInputedUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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{" "}
|
Feel free to reach out to us at{" "}
|
||||||
<a
|
<a
|
||||||
className="font-semibold underline"
|
className="font-semibold underline"
|
||||||
|
@ -83,7 +81,7 @@ export default function ChooseUsername() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => signOut()}
|
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
|
Sign Out
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,37 +1,32 @@
|
||||||
import Dropdown from "@/components/Dropdown";
|
|
||||||
import LinkCard from "@/components/LinkCard";
|
import LinkCard from "@/components/LinkCard";
|
||||||
import useCollectionStore from "@/store/collections";
|
import useCollectionStore from "@/store/collections";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global";
|
import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global";
|
||||||
import {
|
import { faEllipsis, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
faEllipsis,
|
|
||||||
faFolder,
|
|
||||||
faSort,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import SortDropdown from "@/components/SortDropdown";
|
import SortDropdown from "@/components/SortDropdown";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import useLinks from "@/hooks/useLinks";
|
import useLinks from "@/hooks/useLinks";
|
||||||
import usePermissions from "@/hooks/usePermissions";
|
import usePermissions from "@/hooks/usePermissions";
|
||||||
import NoLinksFound from "@/components/NoLinksFound";
|
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() {
|
export default function Index() {
|
||||||
const { setModal } = useModalStore();
|
const { settings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
const [activeCollection, setActiveCollection] =
|
const [activeCollection, setActiveCollection] =
|
||||||
|
@ -47,185 +42,184 @@ export default function Index() {
|
||||||
);
|
);
|
||||||
}, [router, collections]);
|
}, [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 (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
|
||||||
backgroundImage: `linear-gradient(-45deg, ${
|
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||||
activeCollection?.color
|
} 14rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||||
}30 10%, ${theme === "dark" ? "#262626" : "#f3f4f6"} 50%, ${
|
}}
|
||||||
theme === "dark" ? "#262626" : "#f9fafb"
|
className="h-full p-5 flex gap-3 flex-col"
|
||||||
} 100%)`,
|
>
|
||||||
}}
|
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
|
||||||
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"
|
{activeCollection && (
|
||||||
>
|
<div className="flex gap-3 items-center">
|
||||||
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
|
<div className="flex gap-2">
|
||||||
{activeCollection && (
|
<FontAwesomeIcon
|
||||||
<div className="flex gap-3 items-center">
|
icon={faFolder}
|
||||||
<div className="flex gap-2">
|
style={{ color: activeCollection?.color }}
|
||||||
<FontAwesomeIcon
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
||||||
icon={faFolder}
|
/>
|
||||||
style={{ color: activeCollection?.color }}
|
<p className="sm:text-4xl text-3xl capitalize w-full py-1 break-words hyphens-auto font-thin">
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
{activeCollection?.name}
|
||||||
/>
|
</p>
|
||||||
<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>
|
</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
|
<div
|
||||||
className={`min-w-[15rem] ${
|
className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
|
||||||
activeCollection.members[1] && "mr-3"
|
onClick={() => setEditCollectionSharingModal(true)}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div
|
{collectionOwner.id ? (
|
||||||
onClick={() =>
|
<ProfilePhoto
|
||||||
setModal({
|
src={collectionOwner.image || undefined}
|
||||||
modal: "COLLECTION",
|
name={collectionOwner.name}
|
||||||
state: true,
|
/>
|
||||||
method: "UPDATE",
|
) : undefined}
|
||||||
isOwner: permissions === true,
|
{activeCollection.members
|
||||||
active: activeCollection,
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
defaultIndex: permissions === true ? 1 : 0,
|
.map((e, i) => {
|
||||||
})
|
return (
|
||||||
}
|
<ProfilePhoto
|
||||||
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-center w-fit sm:mr-0 sm:ml-auto cursor-pointer"
|
key={i}
|
||||||
>
|
src={e.user.image ? e.user.image : undefined}
|
||||||
{activeCollection?.members
|
className="-ml-3"
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
name={e.user.name}
|
||||||
.map((e, i) => {
|
/>
|
||||||
return (
|
);
|
||||||
<ProfilePhoto
|
})
|
||||||
key={i}
|
.slice(0, 3)}
|
||||||
src={e.user.image ? e.user.image : undefined}
|
{activeCollection.members.length - 3 > 0 ? (
|
||||||
className={`${
|
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||||
activeCollection.members[1] && "-mr-3"
|
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||||
} border-[3px]`}
|
<span>+{activeCollection.members.length - 3}</span>
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.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}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
</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)}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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
|
<div
|
||||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
tabIndex={0}
|
||||||
id="expand-dropdown"
|
role="button"
|
||||||
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"
|
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faEllipsis}
|
icon={faEllipsis}
|
||||||
id="expand-dropdown"
|
|
||||||
title="More"
|
title="More"
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{expandDropdown ? (
|
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
||||||
<Dropdown
|
{permissions === true ? (
|
||||||
items={[
|
<li>
|
||||||
permissions === true
|
<div
|
||||||
? {
|
role="button"
|
||||||
name: "Edit Collection Info",
|
tabIndex={0}
|
||||||
onClick: () => {
|
onClick={() => {
|
||||||
activeCollection &&
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
setModal({
|
setEditCollectionModal(true);
|
||||||
modal: "COLLECTION",
|
}}
|
||||||
state: true,
|
>
|
||||||
method: "UPDATE",
|
Edit Collection Info
|
||||||
isOwner: permissions === true,
|
</div>
|
||||||
active: activeCollection,
|
</li>
|
||||||
});
|
) : undefined}
|
||||||
setExpandDropdown(false);
|
<li>
|
||||||
},
|
<div
|
||||||
}
|
role="button"
|
||||||
: undefined,
|
tabIndex={0}
|
||||||
{
|
onClick={() => {
|
||||||
name:
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
permissions === true
|
setEditCollectionSharingModal(true);
|
||||||
? "Share/Collaborate"
|
}}
|
||||||
: "View Team",
|
>
|
||||||
onClick: () => {
|
{permissions === true
|
||||||
activeCollection &&
|
? "Share and Collaborate"
|
||||||
setModal({
|
: "View Team"}
|
||||||
modal: "COLLECTION",
|
</div>
|
||||||
state: true,
|
</li>
|
||||||
method: "UPDATE",
|
<li>
|
||||||
isOwner: permissions === true,
|
<div
|
||||||
active: activeCollection,
|
role="button"
|
||||||
defaultIndex: permissions === true ? 1 : 0,
|
tabIndex={0}
|
||||||
});
|
onClick={() => {
|
||||||
setExpandDropdown(false);
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
},
|
setDeleteCollectionModal(true);
|
||||||
},
|
}}
|
||||||
|
>
|
||||||
{
|
{permissions === true
|
||||||
name:
|
? "Delete Collection"
|
||||||
permissions === true
|
: "Leave Collection"}
|
||||||
? "Delete Collection"
|
</div>
|
||||||
: "Leave Collection",
|
</li>
|
||||||
onClick: () => {
|
</ul>
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
|
{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">
|
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
|
||||||
{links
|
{links
|
||||||
|
@ -238,6 +232,28 @@ export default function Index() {
|
||||||
<NoLinksFound />
|
<NoLinksFound />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,32 +3,29 @@ import {
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faFolder,
|
faFolder,
|
||||||
faPlus,
|
faPlus,
|
||||||
faSort,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import CollectionCard from "@/components/CollectionCard";
|
import CollectionCard from "@/components/CollectionCard";
|
||||||
import Dropdown from "@/components/Dropdown";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import SortDropdown from "@/components/SortDropdown";
|
import SortDropdown from "@/components/SortDropdown";
|
||||||
import { Sort } from "@/types/global";
|
import { Sort } from "@/types/global";
|
||||||
import useSort from "@/hooks/useSort";
|
import useSort from "@/hooks/useSort";
|
||||||
|
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
|
||||||
|
|
||||||
export default function Collections() {
|
export default function Collections() {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
const [sortedCollections, setSortedCollections] = useState(collections);
|
const [sortedCollections, setSortedCollections] = useState(collections);
|
||||||
|
|
||||||
const { data } = useSession();
|
const { data } = useSession();
|
||||||
|
|
||||||
const { setModal } = useModalStore();
|
|
||||||
|
|
||||||
useSort({ sortBy, setData: setSortedCollections, data: collections });
|
useSort({ sortBy, setData: setSortedCollections, data: collections });
|
||||||
|
|
||||||
|
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="p-5">
|
<div className="p-5">
|
||||||
|
@ -37,77 +34,20 @@ export default function Collections() {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
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>
|
<div>
|
||||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
<p className="text-3xl capitalize font-thin">
|
||||||
Your Collections
|
Your Collections
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">
|
<p>Collections you own</p>
|
||||||
Collections you own
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-2">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -119,21 +59,13 @@ export default function Collections() {
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div
|
<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"
|
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={() => {
|
onClick={() => setNewCollectionModal(true)}
|
||||||
setModal({
|
|
||||||
modal: "COLLECTION",
|
|
||||||
state: true,
|
|
||||||
method: "CREATE",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<p className="text-black dark:text-white group-hover:opacity-0 duration-100">
|
<p className="group-hover:opacity-0 duration-100">New Collection</p>
|
||||||
New Collection
|
|
||||||
</p>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faPlus}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,16 +75,14 @@ export default function Collections() {
|
||||||
<div className="flex items-center gap-3 my-5">
|
<div className="flex items-center gap-3 my-5">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faFolder}
|
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>
|
<div>
|
||||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
<p className="text-3xl capitalize font-thin">
|
||||||
Other Collections
|
Other Collections
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">
|
<p>Shared collections you're a member of</p>
|
||||||
Shared collections you're a member of
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -166,6 +96,9 @@ export default function Collections() {
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
|
{newCollectionModal ? (
|
||||||
|
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||||
|
) : undefined}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ import React from "react";
|
||||||
export default function EmailConfirmaion() {
|
export default function EmailConfirmaion() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<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 ">
|
<p className="text-center text-2xl sm:text-3xl font-extralight mb-2 ">
|
||||||
Please check your Email
|
Please check your Email
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700 my-3" />
|
<div className="divider my-3"></div>
|
||||||
|
|
||||||
<p>A sign in link has been sent to your email address.</p>
|
<p>A sign in link has been sent to your email address.</p>
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ import React from "react";
|
||||||
import useModalStore from "@/store/modals";
|
import useModalStore from "@/store/modals";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
|
||||||
import DashboardItem from "@/components/DashboardItem";
|
import DashboardItem from "@/components/DashboardItem";
|
||||||
|
import NewLinkModal from "@/components/ModalContent/NewLinkModal";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { collections } = useCollectionStore();
|
const { collections } = useCollectionStore();
|
||||||
|
@ -63,8 +63,6 @@ export default function Dashboard() {
|
||||||
handleNumberOfLinksToShow();
|
handleNumberOfLinksToShow();
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
const [importDropdown, setImportDropdown] = useState(false);
|
|
||||||
|
|
||||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
||||||
const file: File = e.target.files[0];
|
const file: File = e.target.files[0];
|
||||||
|
|
||||||
|
@ -92,8 +90,6 @@ export default function Dashboard() {
|
||||||
|
|
||||||
toast.success("Imported the Bookmarks! Reloading the page...");
|
toast.success("Imported the Bookmarks! Reloading the page...");
|
||||||
|
|
||||||
setImportDropdown(false);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
@ -104,35 +100,32 @@ export default function Dashboard() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
|
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faChartSimple}
|
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>
|
<div>
|
||||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
<p className="text-3xl capitalize font-thin">Dashboard</p>
|
||||||
Dashboard
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white">
|
<p>A brief overview of your data</p>
|
||||||
A brief overview of your data
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<DashboardItem
|
||||||
name={numberOfLinks === 1 ? "Link" : "Links"}
|
name={numberOfLinks === 1 ? "Link" : "Links"}
|
||||||
value={numberOfLinks}
|
value={numberOfLinks}
|
||||||
icon={faLink}
|
icon={faLink}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
|
<div className="divider md:divider-horizontal"></div>
|
||||||
<div className="h-24 border-1 border-l border-sky-100 dark:border-neutral-700 hidden md:block"></div>
|
|
||||||
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={collections.length === 1 ? "Collection" : "Collections"}
|
name={collections.length === 1 ? "Collection" : "Collections"}
|
||||||
|
@ -140,8 +133,7 @@ export default function Dashboard() {
|
||||||
icon={faFolder}
|
icon={faFolder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
|
<div className="divider md:divider-horizontal"></div>
|
||||||
<div className="h-24 border-1 border-r border-sky-100 dark:border-neutral-700 hidden md:block"></div>
|
|
||||||
|
|
||||||
<DashboardItem
|
<DashboardItem
|
||||||
name={tags.length === 1 ? "Tag" : "Tags"}
|
name={tags.length === 1 ? "Tag" : "Tags"}
|
||||||
|
@ -155,21 +147,16 @@ export default function Dashboard() {
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faClockRotateLeft}
|
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">
|
<p className="text-2xl">Recently Added Links</p>
|
||||||
Recently Added Links
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/links"
|
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
|
View All
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
|
||||||
icon={faChevronRight}
|
|
||||||
className={`w-4 h-4 text-black dark:text-white`}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -180,7 +167,7 @@ export default function Dashboard() {
|
||||||
{links[0] ? (
|
{links[0] ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<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) => (
|
{links.slice(0, showLinks).map((e, i) => (
|
||||||
<LinkCard key={i} link={e} count={i} />
|
<LinkCard key={i} link={e} count={i} />
|
||||||
|
@ -190,101 +177,87 @@ export default function Dashboard() {
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{ flex: "1 1 auto" }}
|
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!
|
View Your Recently Added Links Here!
|
||||||
</p>
|
</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
|
This section will view your latest added Links across every
|
||||||
Collections you have access to.
|
Collections you have access to.
|
||||||
</p>
|
</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
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModal({
|
setNewLinkModal(true);
|
||||||
modal: "LINK",
|
|
||||||
state: true,
|
|
||||||
method: "CREATE",
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
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
|
<FontAwesomeIcon
|
||||||
icon={faPlus}
|
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">
|
<span className="group-hover:opacity-0 text-right w-full duration-100">
|
||||||
Create New Link
|
Create New Link
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="dropdown dropdown-bottom">
|
||||||
<div
|
<div
|
||||||
onClick={() => setImportDropdown(!importDropdown)}
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
|
||||||
id="import-dropdown"
|
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
|
<FontAwesomeIcon
|
||||||
icon={faFileImport}
|
icon={faFileImport}
|
||||||
className="w-5 h-5 duration-100"
|
className="w-5 h-5 duration-100"
|
||||||
id="import-dropdown"
|
|
||||||
/>
|
/>
|
||||||
<span
|
<p>Import From</p>
|
||||||
className="text-right w-full duration-100"
|
|
||||||
id="import-dropdown"
|
|
||||||
>
|
|
||||||
Import Your Bookmarks
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{importDropdown ? (
|
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||||
<ClickAwayHandler
|
<li>
|
||||||
onClickOutside={(e: Event) => {
|
<label
|
||||||
const target = e.target as HTMLInputElement;
|
tabIndex={0}
|
||||||
if (target.id !== "import-dropdown")
|
role="button"
|
||||||
setImportDropdown(false);
|
htmlFor="import-linkwarden-file"
|
||||||
}}
|
title="JSON File"
|
||||||
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`}
|
>
|
||||||
>
|
From Linkwarden
|
||||||
<div className="cursor-pointer rounded-md">
|
<input
|
||||||
<label
|
type="file"
|
||||||
htmlFor="import-linkwarden-file"
|
name="photo"
|
||||||
title="JSON File"
|
id="import-linkwarden-file"
|
||||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
accept=".json"
|
||||||
>
|
className="hidden"
|
||||||
Linkwarden File...
|
onChange={(e) =>
|
||||||
<input
|
importBookmarks(e, MigrationFormat.linkwarden)
|
||||||
type="file"
|
}
|
||||||
name="photo"
|
/>
|
||||||
id="import-linkwarden-file"
|
</label>
|
||||||
accept=".json"
|
</li>
|
||||||
className="hidden"
|
<li>
|
||||||
onChange={(e) =>
|
<label
|
||||||
importBookmarks(e, MigrationFormat.linkwarden)
|
tabIndex={0}
|
||||||
}
|
role="button"
|
||||||
/>
|
htmlFor="import-html-file"
|
||||||
</label>
|
title="HTML File"
|
||||||
<label
|
>
|
||||||
htmlFor="import-html-file"
|
From Bookmarks HTML file
|
||||||
title="HTML File"
|
<input
|
||||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
type="file"
|
||||||
>
|
name="photo"
|
||||||
Bookmarks HTML file...
|
id="import-html-file"
|
||||||
<input
|
accept=".html"
|
||||||
type="file"
|
className="hidden"
|
||||||
name="photo"
|
onChange={(e) =>
|
||||||
id="import-html-file"
|
importBookmarks(e, MigrationFormat.htmlFile)
|
||||||
accept=".html"
|
}
|
||||||
className="hidden"
|
/>
|
||||||
onChange={(e) =>
|
</label>
|
||||||
importBookmarks(e, MigrationFormat.htmlFile)
|
</li>
|
||||||
}
|
</ul>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</ClickAwayHandler>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -295,19 +268,16 @@ export default function Dashboard() {
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faThumbTack}
|
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>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/links/pinned"
|
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
|
View All
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
|
||||||
icon={faChevronRight}
|
|
||||||
className={`w-4 h-4 text-black dark:text-white`}
|
|
||||||
/>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -318,10 +288,9 @@ export default function Dashboard() {
|
||||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div
|
<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
|
{links
|
||||||
|
|
||||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||||
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
|
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
|
||||||
.slice(0, showLinks)}
|
.slice(0, showLinks)}
|
||||||
|
@ -330,12 +299,12 @@ export default function Dashboard() {
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{ flex: "1 1 auto" }}
|
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!
|
Pin Your Favorite Links Here!
|
||||||
</p>
|
</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
|
You can Pin your favorite Links by clicking on the three dots on
|
||||||
each Link and clicking{" "}
|
each Link and clicking{" "}
|
||||||
<span className="font-semibold">Pin to Dashboard</span>.
|
<span className="font-semibold">Pin to Dashboard</span>.
|
||||||
|
@ -344,6 +313,9 @@ export default function Dashboard() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{newLinkModal ? (
|
||||||
|
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||||
|
) : undefined}
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,34 +43,32 @@ export default function Forgot() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<CenteredForm>
|
||||||
<form onSubmit={sendConfirmation}>
|
<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">
|
<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 text-black dark:text-white font-extralight">
|
<p className="text-3xl text-center font-extralight">
|
||||||
Password Recovery
|
Password Recovery
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white">
|
<p>
|
||||||
Enter your email so we can send you a link to recover your
|
Enter your email so we can send you a link to recover your
|
||||||
account. Make sure to change your password in the profile settings
|
account. Make sure to change your password in the profile settings
|
||||||
afterwards.
|
afterwards.
|
||||||
</p>
|
</p>
|
||||||
<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.
|
You wont get logged in if you haven't created an account yet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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">Email</p>
|
||||||
Email
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus
|
autoFocus
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="johnny@example.com"
|
placeholder="johnny@example.com"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,10 +80,7 @@ export default function Forgot() {
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-baseline gap-1 justify-center">
|
<div className="flex items-baseline gap-1 justify-center">
|
||||||
<Link
|
<Link href={"/login"} className="block font-bold">
|
||||||
href={"/login"}
|
|
||||||
className="block text-black dark:text-white font-bold"
|
|
||||||
>
|
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,14 +9,18 @@ import {
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import ColorThief, { RGBColor } from "colorthief";
|
import ColorThief, { RGBColor } from "colorthief";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 useModalStore from "@/store/modals";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
|
||||||
type LinkContent = {
|
type LinkContent = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -31,10 +35,11 @@ type LinkContent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { theme } = useTheme();
|
|
||||||
const { links, getLink } = useLinkStore();
|
const { links, getLink } = useLinkStore();
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const { settings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const userId = session.data?.user.id;
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
@ -117,19 +122,19 @@ export default function Index() {
|
||||||
|
|
||||||
if (colorPalette && banner && bannerInner) {
|
if (colorPalette && banner && bannerInner) {
|
||||||
if (colorPalette[0] && colorPalette[1]) {
|
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][0],
|
||||||
colorPalette[0][1],
|
colorPalette[0][1],
|
||||||
colorPalette[0][2]
|
colorPalette[0][2]
|
||||||
)}30, ${rgbToHex(
|
)}20, ${rgbToHex(
|
||||||
colorPalette[1][0],
|
colorPalette[1][0],
|
||||||
colorPalette[1][1],
|
colorPalette[1][1],
|
||||||
colorPalette[1][2]
|
colorPalette[1][2]
|
||||||
)}30)`;
|
)}20)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (colorPalette[2] && colorPalette[3]) {
|
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][0],
|
||||||
colorPalette[2][1],
|
colorPalette[2][1],
|
||||||
colorPalette[2][2]
|
colorPalette[2][2]
|
||||||
|
@ -140,23 +145,19 @@ export default function Index() {
|
||||||
)})30`;
|
)})30`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [colorPalette, theme]);
|
}, [colorPalette]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkLayout>
|
<LinkLayout>
|
||||||
<div
|
<div className={`flex flex-col max-w-screen-md h-full`}>
|
||||||
className={`flex flex-col max-w-screen-md h-full ${
|
|
||||||
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
id="link-banner"
|
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={`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 && (
|
{!imageError && link?.url && (
|
||||||
<Image
|
<Image
|
||||||
src={`https://t2.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${link.url}&size=32`}
|
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}
|
height={42}
|
||||||
alt=""
|
alt=""
|
||||||
id={"favicon-" + link.id}
|
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"
|
draggable="false"
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
try {
|
try {
|
||||||
|
@ -183,92 +184,92 @@ export default function Index() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
|
<p className="text-xl">
|
||||||
<p className=" min-w-fit">
|
{unescapeString(link?.name || link?.description || "")}
|
||||||
{link?.createdAt
|
|
||||||
? new Date(link?.createdAt).toLocaleString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})
|
|
||||||
: undefined}
|
|
||||||
</p>
|
</p>
|
||||||
{link?.url ? (
|
{link?.url ? (
|
||||||
<>
|
<Link
|
||||||
<p>•</p>
|
href={link?.url || ""}
|
||||||
<Link
|
title={link?.url}
|
||||||
href={link?.url || ""}
|
target="_blank"
|
||||||
title={link?.url}
|
className="hover:opacity-60 duration-100 break-all text-sm flex items-center gap-1 text-neutral w-fit"
|
||||||
target="_blank"
|
>
|
||||||
className="hover:opacity-60 duration-100 break-all"
|
<FontAwesomeIcon icon={faLink} className="w-4 h-4" />
|
||||||
>
|
|
||||||
{isValidUrl(link?.url || "")
|
{isValidUrl(link?.url || "")
|
||||||
? new URL(link?.url as string).host
|
? new URL(link?.url as string).host
|
||||||
: undefined}
|
: undefined}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex gap-1 items-center flex-wrap">
|
||||||
<p className="capitalize text-2xl sm:text-3xl font-thin">
|
<Link
|
||||||
{unescapeString(link?.name || link?.description || "")}
|
href={`/collections/${link?.collection.id}`}
|
||||||
</p>
|
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">
|
<FontAwesomeIcon
|
||||||
<Link
|
icon={faFolder}
|
||||||
href={`/collections/${link?.collection.id}`}
|
className="w-5 h-5 drop-shadow"
|
||||||
className="flex items-center gap-1 cursor-pointer hover:opacity-60 duration-100 mr-2 z-10"
|
style={{ color: link?.collection.color }}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
title={link?.collection.name}
|
||||||
|
className="text-lg truncate max-w-[12rem]"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
{link?.collection.name}
|
||||||
icon={faFolder}
|
</p>
|
||||||
className="w-5 h-5 drop-shadow"
|
</Link>
|
||||||
style={{ color: link?.collection.color }}
|
{link?.tags.map((e, i) => (
|
||||||
/>
|
<Link key={i} href={`/tags/${e.id}`} className="z-10">
|
||||||
<p
|
<p
|
||||||
title={link?.collection.name}
|
title={e.name}
|
||||||
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
|
className="btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
>
|
>
|
||||||
{link?.collection.name}
|
#{e.name}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</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>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<div className="divider"></div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5 h-full">
|
<div className="flex flex-col gap-5 h-full">
|
||||||
{link?.readabilityPath?.startsWith("archives") ? (
|
{link?.readabilityPath?.startsWith("archives") ? (
|
||||||
<div
|
<div
|
||||||
className="line-break px-3 reader-view"
|
className="line-break px-1 reader-view"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
__html: DOMPurify.sanitize(linkContent?.content || "") || "",
|
||||||
}}
|
}}
|
||||||
></div>
|
></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" ? (
|
{link?.readabilityPath === "pending" ? (
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
Generating readable format, please wait...
|
Generating readable format, please wait...
|
||||||
</p>
|
</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
|
There is no reader view for this webpage
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center text-sm text-black dark:text-white">
|
<p className="text-center text-sm">
|
||||||
{link?.collection.ownerId === userId
|
{link?.collection.ownerId === userId
|
||||||
? "You can update (refetch) the preserved formats by managing them below"
|
? "You can update (refetch) the preserved formats by managing them below"
|
||||||
: "The collections owners can refetch the preserved formats"}
|
: "The collections owners can refetch the preserved formats"}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import useLinks from "@/hooks/useLinks";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { Sort } from "@/types/global";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function Links() {
|
export default function Links() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
useLinks({ sort: sortBy });
|
useLinks({ sort: sortBy });
|
||||||
|
@ -24,39 +23,17 @@ export default function Links() {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faLink}
|
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>
|
<div>
|
||||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
<p className="text-3xl capitalize font-thin">All Links</p>
|
||||||
All Links
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white">
|
<p>Links from every Collections</p>
|
||||||
Links from every Collections
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-2">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{links[0] ? (
|
{links[0] ? (
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import LinkCard from "@/components/LinkCard";
|
import LinkCard from "@/components/LinkCard";
|
||||||
import NoLinksFound from "@/components/NoLinksFound";
|
|
||||||
import SortDropdown from "@/components/SortDropdown";
|
import SortDropdown from "@/components/SortDropdown";
|
||||||
import useLinks from "@/hooks/useLinks";
|
import useLinks from "@/hooks/useLinks";
|
||||||
import MainLayout from "@/layouts/MainLayout";
|
import MainLayout from "@/layouts/MainLayout";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import { Sort } from "@/types/global";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function PinnedLinks() {
|
export default function PinnedLinks() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
useLinks({ sort: sortBy, pinnedOnly: true });
|
useLinks({ sort: sortBy, pinnedOnly: true });
|
||||||
|
@ -24,39 +22,17 @@ export default function PinnedLinks() {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faThumbTack}
|
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>
|
<div>
|
||||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
<p className="text-3xl capitalize font-thin">Pinned Links</p>
|
||||||
Pinned Links
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-black dark:text-white">
|
<p>Pinned Links from your Collections</p>
|
||||||
Pinned Links from your Collections
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative mt-2">
|
<div className="relative mt-2">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||||
|
@ -68,12 +44,12 @@ export default function PinnedLinks() {
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{ flex: "1 1 auto" }}
|
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!
|
Pin Your Favorite Links Here!
|
||||||
</p>
|
</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
|
You can Pin your favorite Links by clicking on the three dots on
|
||||||
each Link and clicking{" "}
|
each Link and clicking{" "}
|
||||||
<span className="font-semibold">Pin to Dashboard</span>.
|
<span className="font-semibold">Pin to Dashboard</span>.
|
||||||
|
|
|
@ -76,18 +76,17 @@ export default function Login() {
|
||||||
return (
|
return (
|
||||||
<CenteredForm text="Sign in to your account">
|
<CenteredForm text="Sign in to your account">
|
||||||
<form onSubmit={loginUser}>
|
<form onSubmit={loginUser}>
|
||||||
<div className="p-4 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" ? (
|
{process.env.NEXT_PUBLIC_DISABLE_LOGIN !== "true" ? (
|
||||||
<div>
|
<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
|
Enter your credentials
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<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
|
Username
|
||||||
{emailEnabled ? " or Email" : undefined}
|
{emailEnabled ? " or Email" : undefined}
|
||||||
</p>
|
</p>
|
||||||
|
@ -96,29 +95,24 @@ export default function Login() {
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder="johnny"
|
placeholder="johnny"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<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">Password</p>
|
||||||
Password
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
/>
|
/>
|
||||||
{emailEnabled && (
|
{emailEnabled && (
|
||||||
<div className="w-fit ml-auto mt-1">
|
<div className="w-fit ml-auto mt-1">
|
||||||
<Link
|
<Link href={"/forgot"} className="text-neutral font-semibold">
|
||||||
href={"/forgot"}
|
|
||||||
className="text-gray-500 dark:text-gray-400 font-semibold"
|
|
||||||
>
|
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -154,13 +148,8 @@ export default function Login() {
|
||||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
|
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
|
||||||
"true" ? undefined : (
|
"true" ? undefined : (
|
||||||
<div className="flex items-baseline gap-1 justify-center">
|
<div className="flex items-baseline gap-1 justify-center">
|
||||||
<p className="w-fit text-gray-500 dark:text-gray-400">
|
<p className="w-fit text-neutral">New here?</p>
|
||||||
New here?
|
<Link href={"/register"} className="block font-semibold">
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href={"/register"}
|
|
||||||
className="block text-black dark:text-white font-semibold"
|
|
||||||
>
|
|
||||||
Sign Up
|
Sign Up
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,18 +9,16 @@ import Head from "next/head";
|
||||||
import useLinks from "@/hooks/useLinks";
|
import useLinks from "@/hooks/useLinks";
|
||||||
import useLinkStore from "@/store/links";
|
import useLinkStore from "@/store/links";
|
||||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||||
import useModalStore from "@/store/modals";
|
|
||||||
import ModalManagement from "@/components/ModalManagement";
|
import ModalManagement from "@/components/ModalManagement";
|
||||||
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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 FilterSearchDropdown from "@/components/FilterSearchDropdown";
|
||||||
import SortDropdown from "@/components/SortDropdown";
|
import SortDropdown from "@/components/SortDropdown";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
import SearchBar from "@/components/SearchBar";
|
||||||
|
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
|
||||||
|
|
||||||
const cardVariants: Variants = {
|
const cardVariants: Variants = {
|
||||||
offscreen: {
|
offscreen: {
|
||||||
|
@ -38,15 +36,8 @@ const cardVariants: Variants = {
|
||||||
|
|
||||||
export default function PublicCollections() {
|
export default function PublicCollections() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
const { modal, setModal } = useModalStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { settings } = useLocalSettingsStore();
|
||||||
modal
|
|
||||||
? (document.body.style.overflow = "hidden")
|
|
||||||
: (document.body.style.overflow = "auto");
|
|
||||||
}, [modal]);
|
|
||||||
|
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -65,8 +56,6 @@ export default function PublicCollections() {
|
||||||
tags: true,
|
tags: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [filterDropdown, setFilterDropdown] = useState(false);
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
useLinks({
|
useLinks({
|
||||||
|
@ -101,13 +90,16 @@ export default function PublicCollections() {
|
||||||
fetchOwner();
|
fetchOwner();
|
||||||
}, [collection]);
|
}, [collection]);
|
||||||
|
|
||||||
|
const [editCollectionSharingModal, setEditCollectionSharingModal] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
return collection ? (
|
return collection ? (
|
||||||
<div
|
<div
|
||||||
className="h-screen"
|
className="h-screen"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
||||||
theme === "dark" ? "#262626" : "#f3f4f6"
|
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||||
} 50%, ${theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalManagement />
|
<ModalManagement />
|
||||||
|
@ -128,63 +120,57 @@ export default function PublicCollections() {
|
||||||
{collection.name}
|
{collection.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 items-center mt-8 min-w-fit">
|
<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">
|
<Link href="https://linkwarden.app/" target="_blank">
|
||||||
<Image
|
<Image
|
||||||
src={`/icon.png`}
|
src={`/icon.png`}
|
||||||
width={551}
|
width={551}
|
||||||
height={551}
|
height={551}
|
||||||
alt="Linkwarden"
|
alt="Linkwarden"
|
||||||
title="Linkwarden"
|
title="Created with Linkwarden"
|
||||||
className="h-8 w-fit mx-auto"
|
className="h-8 w-fit mx-auto rounded"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="mt-3">
|
||||||
<div className={`min-w-[15rem]`}>
|
<div className={`min-w-[15rem]`}>
|
||||||
<div
|
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||||
onClick={() =>
|
<div
|
||||||
setModal({
|
className="flex items-center btn px-2 btn-ghost rounded-full"
|
||||||
modal: "COLLECTION",
|
onClick={() => setEditCollectionSharingModal(true)}
|
||||||
state: true,
|
>
|
||||||
method: "VIEW_TEAM",
|
{collectionOwner.id ? (
|
||||||
isOwner: false,
|
<ProfilePhoto
|
||||||
active: collection,
|
src={collectionOwner.image || undefined}
|
||||||
defaultIndex: 0,
|
name={collectionOwner.name}
|
||||||
})
|
/>
|
||||||
}
|
) : undefined}
|
||||||
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-start w-fit cursor-pointer"
|
{collection.members
|
||||||
>
|
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||||
{collectionOwner.id ? (
|
.map((e, i) => {
|
||||||
<ProfilePhoto
|
return (
|
||||||
src={
|
<ProfilePhoto
|
||||||
collectionOwner.image ? collectionOwner.image : undefined
|
key={i}
|
||||||
}
|
src={e.user.image ? e.user.image : undefined}
|
||||||
className={`w-8 h-8 border-2`}
|
className="-ml-3"
|
||||||
/>
|
name={e.user.name}
|
||||||
) : undefined}
|
/>
|
||||||
{collection.members
|
);
|
||||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
})
|
||||||
.map((e, i) => {
|
.slice(0, 3)}
|
||||||
return (
|
{collection.members.length - 3 > 0 ? (
|
||||||
<ProfilePhoto
|
<div className={`avatar drop-shadow-md placeholder -ml-3`}>
|
||||||
key={i}
|
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||||
src={e.user.image ? e.user.image : undefined}
|
<span>+{collection.members.length - 3}</span>
|
||||||
className={`w-8 h-8 border-2`}
|
</div>
|
||||||
/>
|
</div>
|
||||||
);
|
) : null}
|
||||||
})
|
</div>
|
||||||
.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}
|
|
||||||
|
|
||||||
<p className="ml-2 mt-1 text-gray-500 dark:text-gray-300">
|
<p className="text-neutral text-sm font-semibold">
|
||||||
By {collectionOwner.name}
|
By {collectionOwner.name}
|
||||||
{collection.members.length > 0
|
{collection.members.length > 0
|
||||||
? ` and ${collection.members.length} others`
|
? ` and ${collection.members.length} others`
|
||||||
|
@ -197,57 +183,24 @@ export default function PublicCollections() {
|
||||||
|
|
||||||
<p className="mt-5">{collection.description}</p>
|
<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 mb-5 mt-10 flex-col gap-5">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<PublicSearchBar
|
<SearchBar
|
||||||
placeHolder={`Search ${collection._count?.links} Links`}
|
placeholder={`Search ${collection._count?.links} Links`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<FilterSearchDropdown
|
||||||
onClick={() => setFilterDropdown(!filterDropdown)}
|
searchFilter={searchFilter}
|
||||||
id="filter-dropdown"
|
setSearchFilter={setSearchFilter}
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -271,11 +224,17 @@ export default function PublicCollections() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <p className="text-center text-gray-500">
|
{/* <p className="text-center text-neutral">
|
||||||
List created with <span className="text-black">Linkwarden.</span>
|
List created with <span className="text-black">Linkwarden.</span>
|
||||||
</p> */}
|
</p> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{editCollectionSharingModal ? (
|
||||||
|
<EditCollectionSharingModal
|
||||||
|
onClose={() => setEditCollectionSharingModal(false)}
|
||||||
|
activeCollection={collection}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|
|
@ -9,14 +9,14 @@ import {
|
||||||
} from "@/types/global";
|
} from "@/types/global";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import ColorThief, { RGBColor } from "colorthief";
|
import ColorThief, { RGBColor } from "colorthief";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import unescapeString from "@/lib/client/unescapeString";
|
import unescapeString from "@/lib/client/unescapeString";
|
||||||
import isValidUrl from "@/lib/client/isValidUrl";
|
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
|
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
import useModalStore from "@/store/modals";
|
import useModalStore from "@/store/modals";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
|
||||||
type LinkContent = {
|
type LinkContent = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -31,10 +31,11 @@ type LinkContent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { theme } = useTheme();
|
|
||||||
const { links, getLink } = useLinkStore();
|
const { links, getLink } = useLinkStore();
|
||||||
const { setModal } = useModalStore();
|
const { setModal } = useModalStore();
|
||||||
|
|
||||||
|
const { settings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const userId = session.data?.user.id;
|
const userId = session.data?.user.id;
|
||||||
|
|
||||||
|
@ -140,18 +141,18 @@ export default function Index() {
|
||||||
)})30`;
|
)})30`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [colorPalette, theme]);
|
}, [colorPalette]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LinkLayout>
|
<LinkLayout>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col max-w-screen-md h-full ${
|
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
|
<div
|
||||||
id="link-banner"
|
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>
|
<div id="link-banner-inner" className="link-banner-inner"></div>
|
||||||
|
|
||||||
|
@ -164,7 +165,7 @@ export default function Index() {
|
||||||
height={42}
|
height={42}
|
||||||
alt=""
|
alt=""
|
||||||
id={"favicon-" + link.id}
|
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"
|
draggable="false"
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
try {
|
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">
|
<p className=" min-w-fit">
|
||||||
{link?.createdAt
|
{link?.createdAt
|
||||||
? new Date(link?.createdAt).toLocaleString("en-US", {
|
? new Date(link?.createdAt).toLocaleString("en-US", {
|
||||||
|
@ -229,19 +230,19 @@ export default function Index() {
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
title={link?.collection?.name}
|
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}
|
{link?.collection?.name}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
{link?.tags.map((e, i) => (
|
{link?.tags.map((e, i) => (
|
||||||
<Link key={i} href={`/tags/${e.id}`} className="z-10">
|
<Link
|
||||||
<p
|
key={i}
|
||||||
title={e.name}
|
href={"/public/collections/20?q=" + 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]"
|
title={e.name}
|
||||||
>
|
className="z-10 btn btn-xs btn-ghost truncate max-w-[19rem]"
|
||||||
{e.name}
|
>
|
||||||
</p>
|
#{e.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,17 +259,17 @@ export default function Index() {
|
||||||
}}
|
}}
|
||||||
></div>
|
></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" ? (
|
{link?.readabilityPath === "pending" ? (
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
Generating readable format, please wait...
|
Generating readable format, please wait...
|
||||||
</p>
|
</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
|
There is no reader view for this webpage
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center text-sm text-black dark:text-white">
|
<p className="text-center text-sm">
|
||||||
{link?.collection?.ownerId === userId
|
{link?.collection?.ownerId === userId
|
||||||
? "You can update (refetch) the preserved formats by managing them below"
|
? "You can update (refetch) the preserved formats by managing them below"
|
||||||
: "The collections owners can refetch the preserved formats"}
|
: "The collections owners can refetch the preserved formats"}
|
||||||
|
|
|
@ -104,7 +104,7 @@ export default function Register() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
||||||
<div className="p-4 flex flex-col gap-3 justify-between 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>
|
<p>
|
||||||
Registration is disabled for this instance, please contact the admin
|
Registration is disabled for this instance, please contact the admin
|
||||||
in case of any issues.
|
in case of any issues.
|
||||||
|
@ -112,37 +112,33 @@ export default function Register() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={registerUser}>
|
<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">
|
<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-black dark:text-white text-center font-extralight">
|
<p className="text-3xl text-center font-extralight">
|
||||||
Enter your details
|
Enter your details
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<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">Display Name</p>
|
||||||
Display Name
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
placeholder="Johnny"
|
placeholder="Johnny"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{emailEnabled ? undefined : (
|
{emailEnabled ? undefined : (
|
||||||
<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</p>
|
||||||
Username
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
placeholder="john"
|
placeholder="john"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, username: e.target.value })
|
setForm({ ...form, username: e.target.value })
|
||||||
}
|
}
|
||||||
|
@ -152,36 +148,32 @@ export default function Register() {
|
||||||
|
|
||||||
{emailEnabled ? (
|
{emailEnabled ? (
|
||||||
<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">Email</p>
|
||||||
Email
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="johnny@example.com"
|
placeholder="johnny@example.com"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
<div className="w-full">
|
<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">Password</p>
|
||||||
Password
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<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
|
Confirm Password
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -189,7 +181,7 @@ export default function Register() {
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
value={form.passwordConfirmation}
|
value={form.passwordConfirmation}
|
||||||
className="bg-white"
|
className="bg-base-100"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({ ...form, passwordConfirmation: e.target.value })
|
setForm({ ...form, passwordConfirmation: e.target.value })
|
||||||
}
|
}
|
||||||
|
@ -198,7 +190,7 @@ export default function Register() {
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||||
<div>
|
<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{" "}
|
By signing up, you agree to our{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://linkwarden.app/tos"
|
href="https://linkwarden.app/tos"
|
||||||
|
@ -215,7 +207,7 @@ export default function Register() {
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
<p className="text-xs text-neutral">
|
||||||
Need help?{" "}
|
Need help?{" "}
|
||||||
<Link
|
<Link
|
||||||
href="mailto:support@linkwarden.app"
|
href="mailto:support@linkwarden.app"
|
||||||
|
@ -235,13 +227,8 @@ export default function Register() {
|
||||||
<p className="text-center w-full font-bold">Sign Up</p>
|
<p className="text-center w-full font-bold">Sign Up</p>
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-baseline gap-1 justify-center">
|
<div className="flex items-baseline gap-1 justify-center">
|
||||||
<p className="w-fit text-gray-500 dark:text-gray-400">
|
<p className="w-fit text-neutral">Already have an account?</p>
|
||||||
Already have an account?
|
<Link href={"/login"} className="block font-bold">
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href={"/login"}
|
|
||||||
className="block text-black dark:text-white font-bold"
|
|
||||||
>
|
|
||||||
Login
|
Login
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,6 @@ export default function Search() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const [filterDropdown, setFilterDropdown] = useState(false);
|
const [filterDropdown, setFilterDropdown] = useState(false);
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
useLinks({
|
useLinks({
|
||||||
|
@ -45,57 +44,24 @@ export default function Search() {
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faSearch}
|
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
|
Search Results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<FilterSearchDropdown
|
||||||
onClick={() => setFilterDropdown(!filterDropdown)}
|
searchFilter={searchFilter}
|
||||||
id="filter-dropdown"
|
setSearchFilter={setSearchFilter}
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
/>
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faFilter}
|
|
||||||
id="filter-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filterDropdown ? (
|
|
||||||
<FilterSearchDropdown
|
|
||||||
setFilterDropdown={setFilterDropdown}
|
|
||||||
searchFilter={searchFilter}
|
|
||||||
setSearchFilter={setSearchFilter}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,7 +72,7 @@ export default function Search() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-black dark:text-white">
|
<p>
|
||||||
Nothing found.{" "}
|
Nothing found.{" "}
|
||||||
<span className="font-bold text-xl" title="Shruggie">
|
<span className="font-bold text-xl" title="Shruggie">
|
||||||
¯\_(ツ)_/¯
|
¯\_(ツ)_/¯
|
||||||
|
|
|
@ -153,37 +153,40 @@ export default function Account() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">Account Settings</p>
|
<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="grid sm:grid-cols-2 gap-3 auto-rows-auto">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">Display Name</p>
|
<p className="mb-2">Display Name</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={user.name || ""}
|
value={user.name || ""}
|
||||||
|
className="bg-base-200"
|
||||||
onChange={(e) => setUser({ ...user, name: e.target.value })}
|
onChange={(e) => setUser({ ...user, name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">Username</p>
|
<p className="mb-2">Username</p>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={user.username || ""}
|
value={user.username || ""}
|
||||||
|
className="bg-base-200"
|
||||||
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{emailEnabled ? (
|
{emailEnabled ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">Email</p>
|
<p className="mb-2">Email</p>
|
||||||
{user.email !== account.email &&
|
{user.email !== account.email &&
|
||||||
process.env.NEXT_PUBLIC_STRIPE === "true" ? (
|
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
|
Updating this field will change your billing email as well
|
||||||
</p>
|
</p>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<TextInput
|
<TextInput
|
||||||
value={user.email || ""}
|
value={user.email || ""}
|
||||||
|
className="bg-base-200"
|
||||||
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -191,14 +194,12 @@ export default function Account() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:row-span-2 sm:justify-self-center mx-auto my-3">
|
<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">
|
<p className="mb-2 text-center">Profile Photo</p>
|
||||||
Profile Photo
|
|
||||||
</p>
|
|
||||||
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
|
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
|
||||||
<ProfilePhoto
|
<ProfilePhoto
|
||||||
priority={true}
|
priority={true}
|
||||||
src={user.image ? user.image : undefined}
|
src={user.image ? user.image : undefined}
|
||||||
className="h-auto border-none w-28"
|
dimensionClass="w-28 h-28"
|
||||||
/>
|
/>
|
||||||
{user.image && (
|
{user.image && (
|
||||||
<div
|
<div
|
||||||
|
@ -208,13 +209,13 @@ export default function Account() {
|
||||||
image: "",
|
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" />
|
<FontAwesomeIcon icon={faClose} className="w-3 h-3" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
|
<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...
|
Browse...
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -232,85 +233,74 @@ export default function Account() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
<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
|
Import & Export
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 className="flex gap-3 flex-col">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">
|
<p className="mb-2">Import your data from other platforms.</p>
|
||||||
Import your data from other platforms.
|
<div className="dropdown dropdown-bottom">
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
onClick={() => setImportDropdown(true)}
|
|
||||||
className="w-fit relative"
|
|
||||||
id="import-dropdown"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
className="flex gap-2 text-sm btn btn-outline btn-neutral btn-xs"
|
||||||
id="import-dropdown"
|
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
|
Import From
|
||||||
</div>
|
</div>
|
||||||
{importDropdown ? (
|
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||||
<ClickAwayHandler
|
<li>
|
||||||
onClickOutside={(e: Event) => {
|
<label
|
||||||
const target = e.target as HTMLInputElement;
|
tabIndex={0}
|
||||||
if (target.id !== "import-dropdown")
|
role="button"
|
||||||
setImportDropdown(false);
|
htmlFor="import-linkwarden-file"
|
||||||
}}
|
title="JSON File"
|
||||||
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`}
|
>
|
||||||
>
|
From Linkwarden
|
||||||
<div className="cursor-pointer rounded-md">
|
<input
|
||||||
<label
|
type="file"
|
||||||
htmlFor="import-linkwarden-file"
|
name="photo"
|
||||||
title="JSON File"
|
id="import-linkwarden-file"
|
||||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
accept=".json"
|
||||||
>
|
className="hidden"
|
||||||
Linkwarden File...
|
onChange={(e) =>
|
||||||
<input
|
importBookmarks(e, MigrationFormat.linkwarden)
|
||||||
type="file"
|
}
|
||||||
name="photo"
|
/>
|
||||||
id="import-linkwarden-file"
|
</label>
|
||||||
accept=".json"
|
</li>
|
||||||
className="hidden"
|
<li>
|
||||||
onChange={(e) =>
|
<label
|
||||||
importBookmarks(e, MigrationFormat.linkwarden)
|
tabIndex={0}
|
||||||
}
|
role="button"
|
||||||
/>
|
htmlFor="import-html-file"
|
||||||
</label>
|
title="HTML File"
|
||||||
<label
|
>
|
||||||
htmlFor="import-html-file"
|
From Bookmarks HTML file
|
||||||
title="HTML File"
|
<input
|
||||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
type="file"
|
||||||
>
|
name="photo"
|
||||||
Bookmarks HTML file...
|
id="import-html-file"
|
||||||
<input
|
accept=".html"
|
||||||
type="file"
|
className="hidden"
|
||||||
name="photo"
|
onChange={(e) =>
|
||||||
id="import-html-file"
|
importBookmarks(e, MigrationFormat.htmlFile)
|
||||||
accept=".html"
|
}
|
||||||
className="hidden"
|
/>
|
||||||
onChange={(e) =>
|
</label>
|
||||||
importBookmarks(e, MigrationFormat.htmlFile)
|
</li>
|
||||||
}
|
</ul>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</ClickAwayHandler>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-black dark:text-white mb-2">
|
<p className="mb-2">Download your data instantly.</p>
|
||||||
Download your data instantly.
|
|
||||||
</p>
|
|
||||||
<Link className="w-fit" href="/api/v1/migration">
|
<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
|
Export Data
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -320,12 +310,12 @@ export default function Account() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
<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
|
Profile Visibility
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-3"></div>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Make profile private"
|
label="Make profile private"
|
||||||
|
@ -333,21 +323,19 @@ export default function Account() {
|
||||||
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
|
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.
|
This will limit who can find and add you to new Collections.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{user.isPrivate && (
|
{user.isPrivate && (
|
||||||
<div className="pl-5">
|
<div className="pl-5">
|
||||||
<p className="text-black dark:text-white mt-2">
|
<p className="mt-2">Whitelisted Users</p>
|
||||||
Whitelisted Users
|
<p className="text-neutral text-sm mb-3">
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 dark:text-gray-300 text-sm mb-3">
|
|
||||||
Please provide the Username of the users you wish to grant
|
Please provide the Username of the users you wish to grant
|
||||||
visibility to your profile. Separated by comma.
|
visibility to your profile. Separated by comma.
|
||||||
</p>
|
</p>
|
||||||
<textarea
|
<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..."
|
placeholder="Your profile is hidden from everyone right now..."
|
||||||
value={whitelistedUsersTextbox}
|
value={whitelistedUsersTextbox}
|
||||||
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
|
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
|
||||||
|
@ -370,7 +358,7 @@ export default function Account() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-3"></div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This will permanently delete ALL the Links, Collections, Tags, and
|
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
|
You will be prompted to enter your password before the deletion
|
||||||
process.
|
process.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link
|
|
||||||
href="/settings/delete"
|
|
||||||
className="mx-auto lg:mx-0 text-white mt-3 flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
|
|
||||||
>
|
|
||||||
<p className="text-center w-full">Delete Your Account</p>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -56,10 +56,10 @@ export default function Api() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">API Keys (Soon)</p>
|
<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="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
|
Status: Under Development
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export default function Api() {
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
For now, you can <i>temporarily</i> use your{" "}
|
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
|
next-auth.session-token
|
||||||
</code>{" "}
|
</code>{" "}
|
||||||
in your browser cookies as the API key for your integrations.
|
in your browser cookies as the API key for your integrations.
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import useAccountStore from "@/store/account";
|
import useAccountStore from "@/store/account";
|
||||||
import { AccountSettings } from "@/types/global";
|
import { AccountSettings } from "@/types/global";
|
||||||
import { toast } from "react-hot-toast";
|
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 SubmitButton from "@/components/SubmitButton";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Checkbox from "@/components/Checkbox";
|
import Checkbox from "@/components/Checkbox";
|
||||||
import LinkPreview from "@/components/LinkPreview";
|
import LinkPreview from "@/components/LinkPreview";
|
||||||
|
import useLocalSettingsStore from "@/store/localSettings";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { updateSettings } = useLocalSettingsStore();
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
setSubmitLoader(true);
|
setSubmitLoader(true);
|
||||||
|
|
||||||
|
@ -70,80 +65,46 @@ export default function Appearance() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">Appearance</p>
|
<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>
|
<div>
|
||||||
<p className="mb-3">Select Theme</p>
|
<p className="mb-3">Select Theme</p>
|
||||||
<div className="flex gap-3 w-full">
|
<div className="flex gap-3 w-full">
|
||||||
<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-black ${
|
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 ${
|
||||||
theme === "dark"
|
localStorage.getItem("theme") === "dark"
|
||||||
? "dark:outline-sky-500 text-sky-500"
|
? "dark:outline-primary text-primary"
|
||||||
: "text-white"
|
: "text-white"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setTheme("dark")}
|
onClick={() => updateSettings({ theme: "dark" })}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faMoon} className="w-1/2 h-1/2" />
|
<FontAwesomeIcon icon={faMoon} className="w-1/2 h-1/2" />
|
||||||
<p className="text-2xl">Dark Theme</p>
|
<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>
|
||||||
<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 ${
|
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 ${
|
||||||
theme === "light"
|
localStorage.getItem("theme") === "light"
|
||||||
? "outline-sky-500 text-sky-500"
|
? "outline-primary text-primary"
|
||||||
: "text-black"
|
: "text-black"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setTheme("light")}
|
onClick={() => updateSettings({ theme: "light" })}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faSun} className="w-1/2 h-1/2" />
|
<FontAwesomeIcon icon={faSun} className="w-1/2 h-1/2" />
|
||||||
<p className="text-2xl">Light Theme</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* <SubmitButton
|
||||||
<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
|
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
label="Save"
|
label="Save"
|
||||||
className="mt-2 mx-auto lg:mx-0"
|
className="mt-2 mx-auto lg:mx-0"
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default function Archive() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">Archive Settings</p>
|
<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>
|
<p>Formats to Archive webpages:</p>
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
|
|
|
@ -13,10 +13,10 @@ export default function Billing() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">Billing Settings</p>
|
<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">
|
<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{" "}
|
To manage/cancel your subscription, visit the{" "}
|
||||||
<a
|
<a
|
||||||
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
|
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
|
||||||
|
@ -27,7 +27,7 @@ export default function Billing() {
|
||||||
.
|
.
|
||||||
</p>
|
</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
|
If you still need help or encountered any issues, feel free to reach
|
||||||
out to us at:{" "}
|
out to us at:{" "}
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Link from "next/link";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
export default function Password() {
|
export default function Delete() {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [comment, setComment] = useState<string>();
|
const [comment, setComment] = useState<string>();
|
||||||
const [feedback, setFeedback] = useState<string>();
|
const [feedback, setFeedback] = useState<string>();
|
||||||
|
@ -54,12 +54,15 @@ export default function Password() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredForm>
|
<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
|
<Link
|
||||||
href="/settings/account"
|
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>
|
</Link>
|
||||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This will permanently delete all the Links, Collections, Tags, and
|
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" ? (
|
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-black dark:text-white">
|
<p className="mb-2">Confirm Your Password</p>
|
||||||
Confirm Your Password
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
|
className="bg-base-100"
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
|
|
||||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||||
<fieldset className="border rounded-md p-2 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-sky-500">
|
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
|
||||||
<b>Optional</b>{" "}
|
<b>Optional</b>{" "}
|
||||||
<i className="min-[390px]:text-sm text-xs">
|
<i className="min-[390px]:text-sm text-xs">
|
||||||
(but it really helps us improve!)
|
(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">
|
<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>
|
<p className="text-sm">Reason for cancellation:</p>
|
||||||
<select
|
<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}
|
value={feedback}
|
||||||
onChange={(e) => setFeedback(e.target.value)}
|
onChange={(e) => setFeedback(e.target.value)}
|
||||||
>
|
>
|
||||||
|
@ -120,7 +122,7 @@ export default function Password() {
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<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
|
More information (the more details, the more helpful it'd
|
||||||
be)
|
be)
|
||||||
</p>
|
</p>
|
||||||
|
@ -129,7 +131,7 @@ export default function Password() {
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={(e) => setComment(e.target.value)}
|
onChange={(e) => setComment(e.target.value)}
|
||||||
placeholder="e.g. I needed a feature that..."
|
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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -47,26 +47,28 @@ export default function Password() {
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">Change Password</p>
|
<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">
|
<p className="mb-3">
|
||||||
To change your password, please fill out the following. Your password
|
To change your password, please fill out the following. Your password
|
||||||
should be at least 8 characters.
|
should be at least 8 characters.
|
||||||
</p>
|
</p>
|
||||||
<div className="w-full flex flex-col gap-2 justify-between">
|
<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
|
<TextInput
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
|
className="bg-base-200"
|
||||||
onChange={(e) => setNewPassword1(e.target.value)}
|
onChange={(e) => setNewPassword1(e.target.value)}
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="text-black dark:text-white">Confirm New Password</p>
|
<p>Confirm New Password</p>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
value={newPassword2}
|
value={newPassword2}
|
||||||
|
className="bg-base-200"
|
||||||
onChange={(e) => setNewPassword2(e.target.value)}
|
onChange={(e) => setNewPassword2(e.target.value)}
|
||||||
placeholder="••••••••••••••"
|
placeholder="••••••••••••••"
|
||||||
type="password"
|
type="password"
|
||||||
|
|
|
@ -30,12 +30,12 @@ export default function Subscribe() {
|
||||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||||
}-day free trial, cancel anytime!`}
|
}-day free trial, cancel anytime!`}
|
||||||
>
|
>
|
||||||
<div className="p-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">
|
<p className="sm:text-3xl text-2xl text-center font-extralight">
|
||||||
Subscribe to Linkwarden!
|
Subscribe to Linkwarden!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
<div className="divider my-0"></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
|
@ -47,10 +47,10 @@ export default function Subscribe() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => setPlan(Plan.monthly)}
|
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
|
plan === Plan.monthly
|
||||||
? "text-white bg-sky-700 dark:bg-sky-700"
|
? "text-white bg-sky-700 dark:bg-sky-700"
|
||||||
: "hover:opacity-80"
|
: "hover:opacity-80"
|
||||||
|
@ -61,7 +61,7 @@ export default function Subscribe() {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setPlan(Plan.yearly)}
|
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
|
plan === Plan.yearly
|
||||||
? "text-white bg-sky-700 dark:bg-sky-700"
|
? "text-white bg-sky-700 dark:bg-sky-700"
|
||||||
: "hover:opacity-80"
|
: "hover:opacity-80"
|
||||||
|
@ -77,15 +77,13 @@ export default function Subscribe() {
|
||||||
<div className="flex flex-col gap-2 justify-center items-center">
|
<div className="flex flex-col gap-2 justify-center items-center">
|
||||||
<p className="text-3xl">
|
<p className="text-3xl">
|
||||||
${plan === Plan.monthly ? "4" : "3"}
|
${plan === Plan.monthly ? "4" : "3"}
|
||||||
<span className="text-base text-gray-500 dark:text-gray-400">
|
<span className="text-base text-neutral">/mo</span>
|
||||||
/mo
|
|
||||||
</span>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="font-semibold">
|
<p className="font-semibold">
|
||||||
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
|
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
|
||||||
</p>
|
</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">
|
<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-sky-100 dark:border-neutral-700 rounded-md text-xl">
|
<legend className="w-fit font-extralight px-2 border border-neutral rounded-md text-xl">
|
||||||
Total
|
Total
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
|
@ -108,7 +106,7 @@ export default function Subscribe() {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
onClick={() => signOut()}
|
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
|
Sign Out
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
faCheck,
|
faCheck,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faHashtag,
|
faHashtag,
|
||||||
faSort,
|
|
||||||
faXmark,
|
faXmark,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
@ -24,7 +23,6 @@ export default function Index() {
|
||||||
const { links } = useLinkStore();
|
const { links } = useLinkStore();
|
||||||
const { tags, updateTag, removeTag } = useTagStore();
|
const { tags, updateTag, removeTag } = useTagStore();
|
||||||
|
|
||||||
const [sortDropdown, setSortDropdown] = useState(false);
|
|
||||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||||
|
|
||||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||||
|
@ -107,7 +105,7 @@ export default function Index() {
|
||||||
<div className="flex gap-2 items-end font-thin">
|
<div className="flex gap-2 items-end font-thin">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faHashtag}
|
icon={faHashtag}
|
||||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
|
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary"
|
||||||
/>
|
/>
|
||||||
{renameTag ? (
|
{renameTag ? (
|
||||||
<>
|
<>
|
||||||
|
@ -115,50 +113,78 @@ export default function Index() {
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
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}
|
value={newTagName}
|
||||||
onChange={(e) => setNewTagName(e.target.value)}
|
onChange={(e) => setNewTagName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
onClick={() => submit()}
|
onClick={() => submit()}
|
||||||
id="expand-dropdown"
|
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
|
<FontAwesomeIcon
|
||||||
icon={faCheck}
|
icon={faCheck}
|
||||||
id="expand-dropdown"
|
id="expand-dropdown"
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() => cancelUpdateTag()}
|
onClick={() => cancelUpdateTag()}
|
||||||
id="expand-dropdown"
|
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
|
<FontAwesomeIcon
|
||||||
icon={faXmark}
|
icon={faXmark}
|
||||||
id="expand-dropdown"
|
id="expand-dropdown"
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
className="w-5 h-5 text-neutral"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
|
<p className="sm:text-4xl text-3xl capitalize">
|
||||||
{activeTag?.name}
|
{activeTag?.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<div className="dropdown dropdown-bottom font-normal">
|
||||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
<div
|
||||||
id="expand-dropdown"
|
tabIndex={0}
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
role="button"
|
||||||
>
|
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||||
<FontAwesomeIcon
|
>
|
||||||
icon={faEllipsis}
|
<FontAwesomeIcon
|
||||||
id="expand-dropdown"
|
icon={faEllipsis}
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
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>
|
</div>
|
||||||
|
|
||||||
{expandDropdown ? (
|
{expandDropdown ? (
|
||||||
|
@ -194,25 +220,7 @@ export default function Index() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||||
onClick={() => setSortDropdown(!sortDropdown)}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSort}
|
|
||||||
id="sort-dropdown"
|
|
||||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sortDropdown ? (
|
|
||||||
<SortDropdown
|
|
||||||
sortBy={sortBy}
|
|
||||||
setSort={setSortBy}
|
|
||||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Link" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'url',
|
||||||
|
ALTER COLUMN "url" DROP NOT NULL;
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `blurredFavicons` on the `User` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `displayLinkIcons` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "blurredFavicons",
|
||||||
|
DROP COLUMN "displayLinkIcons";
|
|
@ -45,8 +45,6 @@ model User {
|
||||||
archiveAsPDF Boolean @default(true)
|
archiveAsPDF Boolean @default(true)
|
||||||
archiveAsWaybackMachine Boolean @default(false)
|
archiveAsWaybackMachine Boolean @default(false)
|
||||||
isPrivate Boolean @default(false)
|
isPrivate Boolean @default(false)
|
||||||
displayLinkIcons Boolean @default(true)
|
|
||||||
blurredFavicons Boolean @default(false)
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt @default(now())
|
updatedAt DateTime @updatedAt @default(now())
|
||||||
}
|
}
|
||||||
|
@ -103,12 +101,13 @@ model UsersAndCollections {
|
||||||
model Link {
|
model Link {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
url String
|
type String @default("url")
|
||||||
description String @default("")
|
description String @default("")
|
||||||
pinnedBy User[]
|
pinnedBy User[]
|
||||||
collection Collection @relation(fields: [collectionId], references: [id])
|
collection Collection @relation(fields: [collectionId], references: [id])
|
||||||
collectionId Int
|
collectionId Int
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
|
url String?
|
||||||
textContent String?
|
textContent String?
|
||||||
screenshotPath String?
|
screenshotPath String?
|
||||||
pdfPath String?
|
pdfPath String?
|
||||||
|
|
|
@ -93,6 +93,8 @@ const useLinkStore = create<LinkStore>()((set) => ({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: response.ok, data: data.response };
|
return { ok: response.ok, data: data.response };
|
||||||
|
|
|
@ -1,23 +1,46 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
type LocalSettings = {
|
type LocalSettings = {
|
||||||
darkMode: boolean;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LocalSettingsStore = {
|
type LocalSettingsStore = {
|
||||||
settings: LocalSettings;
|
settings: LocalSettings;
|
||||||
updateSettings: (settings: LocalSettings) => void;
|
updateSettings: (settings: LocalSettings) => void;
|
||||||
|
setSettings: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
|
const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
|
||||||
settings: {
|
settings: {
|
||||||
darkMode: false,
|
theme: "",
|
||||||
},
|
},
|
||||||
updateSettings: async (newSettings) => {
|
updateSettings: async (newSettings) => {
|
||||||
|
if (
|
||||||
|
newSettings.theme &&
|
||||||
|
newSettings.theme !== localStorage.getItem("theme")
|
||||||
|
) {
|
||||||
|
localStorage.setItem("theme", newSettings.theme);
|
||||||
|
|
||||||
|
const localTheme = localStorage.getItem("theme") || "";
|
||||||
|
|
||||||
|
document.querySelector("html")?.setAttribute("data-theme", localTheme);
|
||||||
|
}
|
||||||
|
|
||||||
set((state) => ({ settings: { ...state.settings, ...newSettings } }));
|
set((state) => ({ settings: { ...state.settings, ...newSettings } }));
|
||||||
},
|
},
|
||||||
|
setSettings: async () => {
|
||||||
|
if (!localStorage.getItem("theme")) {
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
const localTheme = localStorage.getItem("theme") || "";
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
settings: { ...state.settings, theme: localTheme },
|
||||||
|
}));
|
||||||
|
|
||||||
|
document.querySelector("html")?.setAttribute("data-theme", localTheme);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useLocalSettingsStore;
|
export default useLocalSettingsStore;
|
||||||
|
|
||||||
// TODO: Add Dark mode.
|
|
||||||
|
|
|
@ -2,6 +2,31 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--my-color: #fff;
|
||||||
|
--selection-color: #fff;
|
||||||
|
--selection-bg-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--my-color: #000;
|
||||||
|
color-scheme: dark;
|
||||||
|
--selection-color: #000000;
|
||||||
|
--selection-bg-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--my-color: #ffabc8;
|
||||||
|
color-scheme: light;
|
||||||
|
--selection-color: #ffffff;
|
||||||
|
--selection-bg-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: var(--selection-bg-color);
|
||||||
|
color: var(--selection-color);
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
@ -16,11 +41,6 @@ body {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
::selection {
|
|
||||||
background-color: #0ea4e93c;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hyphens {
|
.hyphens {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +69,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-up {
|
.slide-up {
|
||||||
animation: slide-up-animation 70ms;
|
animation: slide-up-animation 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-down {
|
.slide-down {
|
||||||
|
@ -69,7 +89,7 @@ body {
|
||||||
|
|
||||||
@keyframes slide-up-animation {
|
@keyframes slide-up-animation {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(15%);
|
transform: translateY(5%);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
|
@ -140,41 +160,25 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme */
|
/* Theme */
|
||||||
@layer base {
|
|
||||||
body {
|
|
||||||
@apply dark:bg-neutral-900 bg-white text-black dark:text-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* react-select */
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.react-select-container .react-select__control {
|
.react-select-container .react-select__control {
|
||||||
@apply dark:bg-neutral-950 bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-500;
|
@apply bg-base-200 hover:border-neutral-content;
|
||||||
}
|
|
||||||
|
|
||||||
.react-select-container {
|
|
||||||
@apply dark:border-neutral-700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-select-container .react-select__menu {
|
.react-select-container .react-select__menu {
|
||||||
@apply dark:bg-neutral-900 dark:border-neutral-700 border;
|
@apply bg-base-100 border-neutral-content border rounded-md;
|
||||||
}
|
|
||||||
|
|
||||||
.react-select-container .react-select__option {
|
|
||||||
@apply dark:hover:bg-neutral-800;
|
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
.react-select-container .react-select__menu-list {
|
||||||
|
@apply h-20;
|
||||||
|
} */
|
||||||
|
|
||||||
.react-select-container .react-select__input-container,
|
.react-select-container .react-select__input-container,
|
||||||
.react-select-container .react-select__single-value {
|
.react-select-container .react-select__single-value {
|
||||||
@apply dark:text-white;
|
@apply text-base-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.react-select__clear-indicator * {
|
|
||||||
display: none;
|
|
||||||
width: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary-btn-gradient {
|
.primary-btn-gradient {
|
||||||
box-shadow: inset 0px -10px 10px #0071b7;
|
box-shadow: inset 0px -10px 10px #0071b7;
|
||||||
|
@ -245,13 +249,14 @@ body {
|
||||||
.reader-view code {
|
.reader-view code {
|
||||||
padding: 0.15rem 0.4rem 0.15rem 0.4rem;
|
padding: 0.15rem 0.4rem 0.15rem 0.4rem;
|
||||||
}
|
}
|
||||||
[class="dark"] .reader-view code,
|
|
||||||
[class="dark"] .reader-view pre {
|
[data-theme="dark"] .reader-view code,
|
||||||
|
[data-theme="dark"] .reader-view pre {
|
||||||
background-color: rgb(49, 49, 49);
|
background-color: rgb(49, 49, 49);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
[class="light"] .reader-view code,
|
[data-theme="light"] .reader-view code,
|
||||||
[class="light"] .reader-view pre {
|
[data-theme="light"] .reader-view pre {
|
||||||
background-color: rgb(230, 230, 230);
|
background-color: rgb(230, 230, 230);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
@ -281,3 +286,7 @@ body {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-file-input::file-selector-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,44 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const plugin = require("tailwindcss/plugin");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
darkMode: "class",
|
daisyui: {
|
||||||
// daisyui: {
|
themes: [
|
||||||
// themes: ["light", "dark"],
|
{
|
||||||
// },
|
light: {
|
||||||
|
primary: "#0369a1",
|
||||||
|
secondary: "#0891b2",
|
||||||
|
accent: "#6d28d9",
|
||||||
|
neutral: "#6b7280",
|
||||||
|
"neutral-content": "#d1d5db",
|
||||||
|
"base-100": "#ffffff",
|
||||||
|
"base-200": "#f3f4f6",
|
||||||
|
"base-content": "#0a0a0a",
|
||||||
|
info: "#a5f3fc",
|
||||||
|
success: "#22c55e",
|
||||||
|
warning: "#facc15",
|
||||||
|
error: "#dc2626",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dark: {
|
||||||
|
primary: "#7dd3fc",
|
||||||
|
secondary: "#22d3ee",
|
||||||
|
accent: "#6d28d9",
|
||||||
|
neutral: "#9ca3af",
|
||||||
|
"neutral-content": "#404040",
|
||||||
|
"base-100": "#171717",
|
||||||
|
"base-200": "#262626",
|
||||||
|
"base-content": "#fafafa",
|
||||||
|
info: "#009ee4",
|
||||||
|
success: "#00b17d",
|
||||||
|
warning: "#eac700",
|
||||||
|
error: "#f1293c",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
darkMode: ["class", '[data-theme="dark"]'],
|
||||||
content: [
|
content: [
|
||||||
"./app/**/*.{js,ts,jsx,tsx}",
|
"./app/**/*.{js,ts,jsx,tsx}",
|
||||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||||
|
@ -14,6 +48,9 @@ module.exports = {
|
||||||
"./layouts/**/*.{js,ts,jsx,tsx}",
|
"./layouts/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
// require("daisyui")
|
require("daisyui"),
|
||||||
|
plugin(({ addVariant }) => {
|
||||||
|
addVariant("dark", '&[data-theme="dark"]');
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ declare global {
|
||||||
STORAGE_FOLDER?: string;
|
STORAGE_FOLDER?: string;
|
||||||
AUTOSCROLL_TIMEOUT?: string;
|
AUTOSCROLL_TIMEOUT?: string;
|
||||||
RE_ARCHIVE_LIMIT?: string;
|
RE_ARCHIVE_LIMIT?: string;
|
||||||
|
NEXT_PUBLIC_MAX_UPLOAD_SIZE?: string;
|
||||||
|
|
||||||
SPACES_KEY?: string;
|
SPACES_KEY?: string;
|
||||||
SPACES_SECRET?: string;
|
SPACES_SECRET?: string;
|
||||||
|
|
|
@ -117,7 +117,14 @@ export type DeleteUserBody = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ArchivedFormat {
|
export enum ArchivedFormat {
|
||||||
screenshot,
|
png,
|
||||||
|
jpeg,
|
||||||
pdf,
|
pdf,
|
||||||
readability,
|
readability,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LinkType {
|
||||||
|
url,
|
||||||
|
pdf,
|
||||||
|
image,
|
||||||
|
}
|
||||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -1497,6 +1497,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/trusted-types" "*"
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
|
"@types/formidable@^3.4.5":
|
||||||
|
version "3.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-3.4.5.tgz#8e45c053cac5868e2b71cc7410e2bd92872f6b9c"
|
||||||
|
integrity sha512-s7YPsNVfnsng5L8sKnG/Gbb2tiwwJTY1conOkJzTMRvJAlLFW1nEua+ADsJQu8N1c0oTHx9+d5nqg10WuT9gHQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/jsdom@^21.1.3":
|
"@types/jsdom@^21.1.3":
|
||||||
version "21.1.3"
|
version "21.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0"
|
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.3.tgz#a88c5dc65703e1b10b2a7839c12db49662b43ff0"
|
||||||
|
@ -1766,6 +1773,11 @@ array.prototype.tosorted@^1.1.1:
|
||||||
es-shim-unscopables "^1.0.0"
|
es-shim-unscopables "^1.0.0"
|
||||||
get-intrinsic "^1.1.3"
|
get-intrinsic "^1.1.3"
|
||||||
|
|
||||||
|
asap@^2.0.0:
|
||||||
|
version "2.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||||
|
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
|
||||||
|
|
||||||
asn1@~0.2.3:
|
asn1@~0.2.3:
|
||||||
version "0.2.6"
|
version "0.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||||
|
@ -2299,6 +2311,14 @@ detect-libc@^2.0.0, detect-libc@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
|
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
|
||||||
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
|
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
|
||||||
|
|
||||||
|
dezalgo@^1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81"
|
||||||
|
integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==
|
||||||
|
dependencies:
|
||||||
|
asap "^2.0.0"
|
||||||
|
wrappy "1"
|
||||||
|
|
||||||
didyoumean@^1.2.2:
|
didyoumean@^1.2.2:
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
|
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
|
||||||
|
@ -2836,6 +2856,15 @@ form-data@~2.3.2:
|
||||||
combined-stream "^1.0.6"
|
combined-stream "^1.0.6"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
formidable@^3.5.1:
|
||||||
|
version "3.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.1.tgz#9360a23a656f261207868b1484624c4c8d06ee1a"
|
||||||
|
integrity sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==
|
||||||
|
dependencies:
|
||||||
|
dezalgo "^1.0.4"
|
||||||
|
hexoid "^1.0.0"
|
||||||
|
once "^1.4.0"
|
||||||
|
|
||||||
fraction.js@^4.2.0:
|
fraction.js@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
|
||||||
|
@ -3151,6 +3180,11 @@ has@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
hexoid@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
|
||||||
|
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==
|
||||||
|
|
||||||
hoist-non-react-statics@^3.3.1:
|
hoist-non-react-statics@^3.3.1:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||||
|
@ -3825,11 +3859,6 @@ next-auth@^4.22.1:
|
||||||
preact-render-to-string "^5.1.19"
|
preact-render-to-string "^5.1.19"
|
||||||
uuid "^8.3.2"
|
uuid "^8.3.2"
|
||||||
|
|
||||||
next-themes@^0.2.1:
|
|
||||||
version "0.2.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.2.1.tgz#0c9f128e847979daf6c67f70b38e6b6567856e45"
|
|
||||||
integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==
|
|
||||||
|
|
||||||
next@13.4.12:
|
next@13.4.12:
|
||||||
version "13.4.12"
|
version "13.4.12"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df"
|
resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df"
|
||||||
|
|
Ŝarĝante…
Reference in New Issue