@@ -9,7 +9,7 @@ type Props = {
|
||||
|
||||
export default function AnnouncementBar({ toggleAnnouncementBar }: Props) {
|
||||
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-fit font-semibold">
|
||||
🎉️{" "}
|
||||
|
||||
+5
-11
@@ -12,23 +12,17 @@ type Props = {
|
||||
export default function Checkbox({ label, state, className, onClick }: Props) {
|
||||
return (
|
||||
<label
|
||||
className={`cursor-pointer flex items-center gap-2 ${className || ""}`}
|
||||
className={`label cursor-pointer flex gap-2 justify-start ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state}
|
||||
onChange={onClick}
|
||||
className="peer sr-only"
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
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>
|
||||
<span className="label-text">{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
+163
-130
@@ -2,30 +2,25 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEllipsis, faGlobe, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import Link from "next/link";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import Dropdown from "./Dropdown";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ProfilePhoto from "./ProfilePhoto";
|
||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||
import useModalStore from "@/store/modals";
|
||||
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 = {
|
||||
collection: CollectionIncludingMembersAndLinkCount;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type DropdownTrigger =
|
||||
| {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
| false;
|
||||
|
||||
export default function CollectionCard({ collection, className }: Props) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { theme } = useTheme();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const formattedDate = new Date(collection.createdAt as string).toLocaleString(
|
||||
"en-US",
|
||||
@@ -36,144 +31,182 @@ export default function CollectionCard({ collection, className }: Props) {
|
||||
}
|
||||
);
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="relative">
|
||||
<div className="dropdown dropdown-bottom dropdown-end absolute top-3 right-3 z-10">
|
||||
<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
|
||||
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}
|
||||
dimensionClass="w-7 h-7"
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar 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={{
|
||||
backgroundImage: `linear-gradient(45deg, ${collection.color}30 10%, ${
|
||||
theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 50%, ${theme === "dark" ? "#262626" : "#f9fafb"} 100%)`,
|
||||
settings.theme === "dark" ? "oklch(var(--b2))" : "oklch(var(--b2))"
|
||||
} 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 || ""
|
||||
}`}
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content"
|
||||
>
|
||||
<div
|
||||
onClick={(e) => setExpandDropdown({ x: e.clientX, y: e.clientY })}
|
||||
id={"expand-dropdown" + collection.id}
|
||||
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"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
id={"expand-dropdown" + collection.id}
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<div className="card-body flex flex-col justify-between min-h-[12rem]">
|
||||
<div className="flex justify-between">
|
||||
<p className="card-title break-words line-clamp-2 w-full">
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="w-8 h-8 ml-10"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center">
|
||||
<div className="text-right">
|
||||
<div className="font-bold text-sm flex justify-end gap-1 items-center">
|
||||
{collection.isPublic ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faGlobe}
|
||||
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}
|
||||
<FontAwesomeIcon
|
||||
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}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-1 text-gray-500 dark:text-gray-300">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p className="font-bold text-xs">{formattedDate}</p>
|
||||
<div className="flex items-center justify-end gap-1 text-neutral">
|
||||
<p className="font-bold text-xs flex gap-1 items-center">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />{" "}
|
||||
{formattedDate}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
{expandDropdown ? (
|
||||
<Dropdown
|
||||
points={{ x: expandDropdown.x, y: expandDropdown.y }}
|
||||
items={[
|
||||
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"
|
||||
</div>
|
||||
</Link>
|
||||
{editCollectionModal ? (
|
||||
<EditCollectionModal
|
||||
onClose={() => setEditCollectionModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : 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) {
|
||||
return (
|
||||
<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">
|
||||
<FontAwesomeIcon
|
||||
icon={icon}
|
||||
className="w-8 h-8 text-sky-500 dark:text-sky-500"
|
||||
/>
|
||||
<div className="p-4 bg-primary/20 rounded-xl select-none">
|
||||
<FontAwesomeIcon icon={icon} className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<p className="text-gray-500 dark:text-gray-400 text-sm tracking-wider">
|
||||
{name}
|
||||
</p>
|
||||
<p className="font-thin text-6xl text-sky-500 dark:text-sky-500 mt-2">
|
||||
{value}
|
||||
</p>
|
||||
<p className="text-neutral text-sm tracking-wider">{name}</p>
|
||||
<p className="font-thin text-6xl text-primary mt-2">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,13 +78,13 @@ export default function Dropdown({
|
||||
onClickOutside={onClickOutside}
|
||||
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) => {
|
||||
const inner = e && (
|
||||
<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">
|
||||
<p className="text-black dark:text-white select-none">{e.name}</p>
|
||||
<div className="flex items-center gap-2 py-1 px-2 hover:bg-base-100 duration-100">
|
||||
<p className="select-none">{e.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { SetStateAction } from "react";
|
||||
import ClickAwayHandler from "./ClickAwayHandler";
|
||||
import Checkbox from "./Checkbox";
|
||||
import React from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type Props = {
|
||||
setFilterDropdown: (value: SetStateAction<boolean>) => void;
|
||||
setSearchFilter: Function;
|
||||
searchFilter: {
|
||||
name: boolean;
|
||||
@@ -15,64 +14,123 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function FilterSearchDropdown({
|
||||
setFilterDropdown,
|
||||
setSearchFilter,
|
||||
searchFilter,
|
||||
}: Props) {
|
||||
return (
|
||||
<ClickAwayHandler
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "filter-dropdown") setFilterDropdown(false);
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<p className="mb-2 text-black dark:text-white text-center font-semibold">
|
||||
Filter by
|
||||
</p>
|
||||
<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 className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-sm btn-square btn-ghost"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFilter}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</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 { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Select from "react-select";
|
||||
import { styles } from "./styles";
|
||||
import { Options } from "./types";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
|
||||
type Props = {
|
||||
onChange: any;
|
||||
@@ -13,9 +13,14 @@ type Props = {
|
||||
value?: number;
|
||||
}
|
||||
| undefined;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||
export default function CollectionSelection({
|
||||
onChange,
|
||||
defaultValue,
|
||||
id,
|
||||
}: Props) {
|
||||
const { collections } = useCollectionStore();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -43,8 +48,9 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
|
||||
}, [collections]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
isClearable
|
||||
<CreatableSelect
|
||||
key={id || "key"}
|
||||
isClearable={false}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
onChange={onChange}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function TagSelection({ onChange, defaultValue }: Props) {
|
||||
|
||||
return (
|
||||
<CreatableSelect
|
||||
isClearable
|
||||
isClearable={false}
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
onChange={onChange}
|
||||
|
||||
@@ -8,20 +8,27 @@ export const styles: StylesConfig = {
|
||||
...styles,
|
||||
fontFamily: font,
|
||||
cursor: "pointer",
|
||||
backgroundColor: state.isSelected ? "#0ea5e9" : "inherit",
|
||||
backgroundColor: state.isSelected ? "oklch(var(--p))" : "inherit",
|
||||
"&:hover": {
|
||||
backgroundColor: state.isSelected ? "#0ea5e9" : "#e2e8f0",
|
||||
backgroundColor: state.isSelected
|
||||
? "oklch(var(--p))"
|
||||
: "oklch(var(--nc))",
|
||||
},
|
||||
transition: "all 50ms",
|
||||
}),
|
||||
control: (styles) => ({
|
||||
control: (styles, state) => ({
|
||||
...styles,
|
||||
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,
|
||||
border: "1px solid #e0f2fe",
|
||||
height: "full",
|
||||
borderRadius: "0.375rem",
|
||||
lineHeight: "1.25rem",
|
||||
// "@media screen and (min-width: 1024px)": {
|
||||
@@ -58,4 +65,5 @@ export const styles: StylesConfig = {
|
||||
backgroundColor: "#38bdf8",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
};
|
||||
|
||||
+165
-160
@@ -10,11 +10,9 @@ import {
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Dropdown from "./Dropdown";
|
||||
import useLinkStore from "@/store/links";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useAccountStore from "@/store/account";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { faCalendarDays } from "@fortawesome/free-regular-svg-icons";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -22,6 +20,8 @@ import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import Link from "next/link";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import { useRouter } from "next/router";
|
||||
import EditLinkModal from "./ModalContent/EditLinkModal";
|
||||
import DeleteLinkModal from "./ModalContent/DeleteLinkModal";
|
||||
|
||||
type Props = {
|
||||
link: LinkIncludingShortenedCollectionAndTags;
|
||||
@@ -29,22 +29,11 @@ type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type DropdownTrigger =
|
||||
| {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
| false;
|
||||
|
||||
export default function LinkCard({ link, count, className }: Props) {
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const permissions = usePermissions(link.collection.id as number);
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState<DropdownTrigger>(false);
|
||||
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const { links } = useLinkStore();
|
||||
@@ -81,8 +70,6 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||
|
||||
const load = toast.loading("Applying...");
|
||||
|
||||
setExpandDropdown(false);
|
||||
|
||||
const response = await updateLink({
|
||||
...link,
|
||||
pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }],
|
||||
@@ -97,8 +84,6 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||
const updateArchive = async () => {
|
||||
const load = toast.loading("Sending request...");
|
||||
|
||||
setExpandDropdown(false);
|
||||
|
||||
const response = await fetch(`/api/v1/links/${link.id}/archive`, {
|
||||
method: "PUT",
|
||||
});
|
||||
@@ -121,7 +106,6 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||
toast.dismiss(load);
|
||||
|
||||
response.ok && toast.success(`Link Deleted.`);
|
||||
setExpandDropdown(false);
|
||||
};
|
||||
|
||||
const url =
|
||||
@@ -136,167 +120,188 @@ export default function LinkCard({ link, count, className }: Props) {
|
||||
}
|
||||
);
|
||||
|
||||
const [editLinkModal, setEditLinkModal] = useState(false);
|
||||
const [deleteLinkModal, setDeleteLinkModal] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 || ""
|
||||
}`}
|
||||
>
|
||||
{(permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete) && (
|
||||
<div
|
||||
className={`h-fit border border-solid border-neutral-content bg-base-200 shadow-md hover:shadow-none duration-100 rounded-2xl relative ${
|
||||
className || ""
|
||||
}`}
|
||||
>
|
||||
{permissions === true ||
|
||||
permissions?.canUpdate ||
|
||||
permissions?.canDelete ? (
|
||||
<div className="dropdown dropdown-left absolute top-3 right-3 z-20">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
setExpandDropdown({ x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
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"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
title="More"
|
||||
className="w-5 h-5"
|
||||
id={"expand-dropdown" + link.id}
|
||||
id={"expand-dropdown" + collection.id}
|
||||
/>
|
||||
</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();
|
||||
updateArchive();
|
||||
}}
|
||||
>
|
||||
Refresh Link
|
||||
</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}
|
||||
|
||||
<div
|
||||
onClick={() => router.push("/links/" + link.id)}
|
||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-4"
|
||||
>
|
||||
{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-12 duration-100 bg-white rounded-md p-1 bottom-5 right-5 select-none z-10`}
|
||||
draggable="false"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
target.style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() => router.push("/links/" + link.id)}
|
||||
className="flex items-start cursor-pointer gap-5 sm:gap-10 h-full w-full p-4"
|
||||
>
|
||||
{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-neutral">{count + 1}</p>
|
||||
<p className="text-lg 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="truncate capitalize w-full">{collection?.name}</p>
|
||||
</Link>
|
||||
|
||||
<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>
|
||||
{/* {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="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</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 className="absolute w-1/2 top-0 bottom-0 right-0 bg-gradient-to-r from-transparent to-base-200 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-neutral 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-neutral">
|
||||
<FontAwesomeIcon icon={faCalendarDays} className="w-4 h-4" />
|
||||
<p>{formattedDate}</p>
|
||||
</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"
|
||||
{editLinkModal ? (
|
||||
<EditLinkModal
|
||||
onClose={() => setEditLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : undefined}
|
||||
{deleteLinkModal ? (
|
||||
<DeleteLinkModal
|
||||
onClose={() => setDeleteLinkModal(false)}
|
||||
activeLink={link}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function LinkPreview({ link, className, settings }: Props) {
|
||||
return (
|
||||
<>
|
||||
<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 || ""
|
||||
}`}
|
||||
>
|
||||
@@ -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 flex-col justify-between w-full">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">{1}</p>
|
||||
<p className="text-lg text-black dark:text-white truncate capitalize w-full pr-8">
|
||||
<p className="text-sm text-neutral">{1}</p>
|
||||
<p className="text-lg truncate capitalize w-full pr-8">
|
||||
{unescapeString(link.name as string)}
|
||||
</p>
|
||||
</div>
|
||||
<div 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 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">
|
||||
Landing Pages ⚡️
|
||||
</p>
|
||||
<p className="truncate capitalize w-full">Landing Pages ⚡️</p>
|
||||
</div>
|
||||
<A
|
||||
href={link.url as string}
|
||||
@@ -94,12 +92,12 @@ export default function LinkPreview({ link, className, settings }: Props) {
|
||||
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"
|
||||
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" />
|
||||
<p className="truncate w-full">{shortendURL}</p>
|
||||
</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" />
|
||||
<p>{formattedDate}</p>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||
|
||||
return (
|
||||
<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 || ""
|
||||
}`}
|
||||
>
|
||||
@@ -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`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPen}
|
||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faPen} className="w-6 h-6 text-neutral" />
|
||||
|
||||
<p className="text-black dark:text-white truncate w-full lg:hidden">
|
||||
Edit
|
||||
</p>
|
||||
<p className="truncate w-full lg:hidden">Edit</p>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
@@ -99,12 +94,10 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
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">
|
||||
Preserved Formats
|
||||
</p>
|
||||
<p className="truncate w-full lg:hidden">Preserved Formats</p>
|
||||
</div>
|
||||
|
||||
{link?.collection.ownerId === userId ||
|
||||
@@ -124,12 +117,10 @@ export default function LinkSidebar({ className, onClick }: Props) {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
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">
|
||||
Delete
|
||||
</p>
|
||||
<p className="truncate w-full lg:hidden">Delete</p>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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 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={`m-auto w-11/12 max-w-2xl ${className || ""}`}
|
||||
>
|
||||
<div className="slide-up m-auto relative border-neutral-content rounded-2xl border-solid 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 sm:flex-row gap-3">
|
||||
<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">
|
||||
<TextInput
|
||||
value={collection.name}
|
||||
@@ -71,7 +71,7 @@ export default function CollectionInfo({
|
||||
/>
|
||||
<div className="color-picker flex justify-between">
|
||||
<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 }}>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
@@ -79,7 +79,7 @@ export default function CollectionInfo({
|
||||
/>
|
||||
</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={() =>
|
||||
setCollection({ ...collection, color: "#0ea5e9" })
|
||||
}
|
||||
@@ -96,9 +96,9 @@ export default function CollectionInfo({
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<p className="text-black dark:text-white mb-2">Description</p>
|
||||
<p className="mb-2">Description</p>
|
||||
<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..."
|
||||
value={collection.description}
|
||||
onChange={(e) =>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function DeleteCollection({
|
||||
<p className="text-red-500 font-bold text-center">Warning!</p>
|
||||
|
||||
<div className="max-h-[20rem] overflow-y-auto">
|
||||
<div className="text-black dark:text-white">
|
||||
<div>
|
||||
<p>
|
||||
Please note that deleting the collection will permanently remove
|
||||
all its contents, including the following:
|
||||
@@ -82,7 +82,7 @@ export default function DeleteCollection({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-black dark:text-white text-center">
|
||||
<p className="text-center">
|
||||
To confirm, type "
|
||||
<span className="font-bold">{collection.name}</span>
|
||||
" in the box below:
|
||||
@@ -98,9 +98,7 @@ export default function DeleteCollection({
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-black dark:text-white">
|
||||
Click the button below to leave the current collection.
|
||||
</p>
|
||||
<p>Click the button below to leave the current collection.</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function TeamManagement({
|
||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||
{permissions === true && (
|
||||
<>
|
||||
<p className="text-black dark:text-white">Make Public</p>
|
||||
<p>Make Public</p>
|
||||
|
||||
<Checkbox
|
||||
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.
|
||||
</p>
|
||||
</>
|
||||
@@ -120,9 +120,7 @@ export default function TeamManagement({
|
||||
|
||||
{collection.isPublic ? (
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">
|
||||
Public Link (Click to copy)
|
||||
</p>
|
||||
<p className="mb-2">Public Link (Click to copy)</p>
|
||||
<div
|
||||
onClick={() => {
|
||||
try {
|
||||
@@ -133,7 +131,7 @@ export default function TeamManagement({
|
||||
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}
|
||||
</div>
|
||||
@@ -141,12 +139,12 @@ export default function TeamManagement({
|
||||
) : null}
|
||||
|
||||
{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 && (
|
||||
<>
|
||||
<p className="text-black dark:text-white">Member Management</p>
|
||||
<p>Member Management</p>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
@@ -183,7 +181,7 @@ export default function TeamManagement({
|
||||
|
||||
{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.)
|
||||
</p>
|
||||
<div className="flex flex-col gap-3 rounded-md">
|
||||
@@ -193,12 +191,12 @@ export default function TeamManagement({
|
||||
return (
|
||||
<div
|
||||
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 && (
|
||||
<FontAwesomeIcon
|
||||
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"
|
||||
onClick={() => {
|
||||
const updatedMembers = collection.members.filter(
|
||||
@@ -219,25 +217,21 @@ export default function TeamManagement({
|
||||
className="border-[3px]"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-black dark:text-white">
|
||||
{e.user.name}
|
||||
</p>
|
||||
<p className="text-gray-500 dark:text-gray-300">
|
||||
@{e.user.username}
|
||||
</p>
|
||||
<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 text-black dark:text-white ${
|
||||
className={`font-bold text-sm ${
|
||||
permissions === true ? "" : "mb-2"
|
||||
}`}
|
||||
>
|
||||
Permissions
|
||||
</p>
|
||||
{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.)
|
||||
</p>
|
||||
)}
|
||||
@@ -247,7 +241,7 @@ export default function TeamManagement({
|
||||
!e.canCreate &&
|
||||
!e.canUpdate &&
|
||||
!e.canDelete ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||
<p className="text-sm text-neutral">
|
||||
Has no permissions.
|
||||
</p>
|
||||
) : (
|
||||
@@ -287,7 +281,7 @@ export default function TeamManagement({
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||
: ""
|
||||
@@ -332,7 +326,7 @@ export default function TeamManagement({
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||
: ""
|
||||
@@ -377,7 +371,7 @@ export default function TeamManagement({
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
? "hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100"
|
||||
: ""
|
||||
@@ -397,7 +391,7 @@ export default function TeamManagement({
|
||||
)}
|
||||
|
||||
<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.`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -407,21 +401,17 @@ export default function TeamManagement({
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-sm font-bold text-black dark:text-white">
|
||||
{collectionOwner.name}
|
||||
</p>
|
||||
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faCrown}
|
||||
className="w-3 h-3 text-yellow-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-300">
|
||||
@{collectionOwner.username}
|
||||
</p>
|
||||
<p className="text-neutral">@{collectionOwner.username}</p>
|
||||
</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>Full Access (Owner)</p>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +33,8 @@ export default function ViewTeam({ collection }: Props) {
|
||||
<p>Here are all the members who are collaborating on this collection.</p>
|
||||
|
||||
<div
|
||||
className="relative border px-2 rounded-md border-sky-100 dark:border-neutral-700 flex min-h-[4rem] gap-2 justify-between"
|
||||
title={`'@${collectionOwner.username}' is the owner of this collection.`}
|
||||
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.`}
|
||||
>
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<ProfilePhoto
|
||||
@@ -43,9 +43,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||
/>
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-1 w-full justify-between">
|
||||
<p className="text-sm font-bold text-black dark:text-white">
|
||||
{collectionOwner.name}
|
||||
</p>
|
||||
<p className="text-sm font-bold">{collectionOwner.name}</p>
|
||||
<div className="flex text-xs gap-1 items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCrown}
|
||||
@@ -54,9 +52,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-300">
|
||||
@{collectionOwner.username}
|
||||
</p>
|
||||
<p className="text-neutral">@{collectionOwner.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +66,7 @@ export default function ViewTeam({ collection }: Props) {
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
<ProfilePhoto
|
||||
@@ -78,12 +74,8 @@ export default function ViewTeam({ collection }: Props) {
|
||||
className="border-[3px]"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-black dark:text-white">
|
||||
{e.user.name}
|
||||
</p>
|
||||
<p className="text-gray-500 dark:text-gray-300">
|
||||
@{e.user.username}
|
||||
</p>
|
||||
<p className="text-sm font-bold">{e.user.name}</p>
|
||||
<p className="text-neutral">@{e.user.username}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Tab } from "@headlessui/react";
|
||||
import CollectionInfo from "./CollectionInfo";
|
||||
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
||||
import TeamManagement from "./TeamManagement";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import DeleteCollection from "./DeleteCollection";
|
||||
import ViewTeam from "./ViewTeam";
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function CollectionModal({
|
||||
</p>
|
||||
)}
|
||||
{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" && (
|
||||
<>
|
||||
{isOwner && (
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function AddOrEditLink({
|
||||
<div className="flex flex-col gap-3 sm:w-[35rem] w-80">
|
||||
{method === "UPDATE" ? (
|
||||
<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 || ""}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLink} className="w-6 h-6" />
|
||||
@@ -152,15 +152,16 @@ export default function AddOrEditLink({
|
||||
{method === "CREATE" ? (
|
||||
<div className="grid grid-flow-row-dense sm:grid-cols-5 gap-3">
|
||||
<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
|
||||
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="text-black dark:text-white mb-2">Collection</p>
|
||||
<p className="mb-2">Collection</p>
|
||||
{link.collection.name ? (
|
||||
<CollectionSelection
|
||||
onChange={setCollection}
|
||||
@@ -187,20 +188,21 @@ export default function AddOrEditLink({
|
||||
|
||||
{optionsExpanded ? (
|
||||
<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={`${method === "UPDATE" ? "sm:col-span-2" : ""}`}>
|
||||
<p className="text-black dark:text-white mb-2">Name</p>
|
||||
<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>
|
||||
|
||||
{method === "UPDATE" ? (
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">Collection</p>
|
||||
<p className="mb-2">Collection</p>
|
||||
{link.collection.name ? (
|
||||
<CollectionSelection
|
||||
onChange={setCollection}
|
||||
@@ -221,7 +223,7 @@ export default function AddOrEditLink({
|
||||
) : undefined}
|
||||
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">Tags</p>
|
||||
<p className="mb-2">Tags</p>
|
||||
<TagSelection
|
||||
onChange={setTags}
|
||||
defaultValue={link.tags.map((e) => {
|
||||
@@ -231,7 +233,7 @@ export default function AddOrEditLink({
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2">
|
||||
<p className="text-black dark:text-white mb-2">Description</p>
|
||||
<p className="mb-2">Description</p>
|
||||
<textarea
|
||||
value={unescapeString(link.description) as string}
|
||||
onChange={(e) =>
|
||||
@@ -242,7 +244,7 @@ export default function AddOrEditLink({
|
||||
? "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>
|
||||
@@ -254,7 +256,7 @@ export default function AddOrEditLink({
|
||||
onClick={() => setOptionsExpanded(!optionsExpanded)}
|
||||
className={`${
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -90,23 +90,23 @@ export default function PreservedFormats() {
|
||||
return (
|
||||
<div className={`flex flex-col gap-3 sm:w-[35rem] w-80 pt-3`}>
|
||||
{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="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-l-md">
|
||||
<div className="text-white bg-primary p-2 rounded-l-md">
|
||||
<FontAwesomeIcon icon={faFileImage} className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<p className="text-black dark:text-white">Screenshot</p>
|
||||
<p>Screenshot</p>
|
||||
</div>
|
||||
|
||||
<div className="flex text-black dark:text-white gap-1">
|
||||
<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-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 cursor-pointer text-neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function PreservedFormats() {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -129,23 +129,23 @@ export default function PreservedFormats() {
|
||||
) : undefined}
|
||||
|
||||
{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="text-white bg-sky-300 dark:bg-sky-600 p-2 rounded-l-md">
|
||||
<div className="text-white bg-primary p-2 rounded-l-md">
|
||||
<FontAwesomeIcon icon={faFilePdf} className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
<p className="text-black dark:text-white">PDF</p>
|
||||
<p>PDF</p>
|
||||
</div>
|
||||
|
||||
<div className="flex text-black dark:text-white gap-1">
|
||||
<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-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 cursor-pointer text-neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function PreservedFormats() {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@ export default function PreservedFormats() {
|
||||
""
|
||||
)}`}
|
||||
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?.screenshotPath &&
|
||||
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,137 @@
|
||||
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 mb-5 font-thin text-red-500">
|
||||
{permissions === true ? "Delete" : "Leave"} Collection
|
||||
</p>
|
||||
|
||||
<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,89 @@
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
onClose: Function;
|
||||
activeLink: LinkIncludingShortenedCollectionAndTags;
|
||||
};
|
||||
|
||||
export default function DeleteLinkModal({ 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 { removeLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
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.`);
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal toggleModal={onClose}>
|
||||
<p className="text-xl mb-5 font-thin text-red-500">Delete Link</p>
|
||||
<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,117 @@
|
||||
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 mb-5 font-thin">Edit Collection Info</p>
|
||||
|
||||
<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 w-fit ml-auto" onClick={submit}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
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 mb-5">
|
||||
{permissions === true ? "Share and Collaborate" : "Team"}
|
||||
</p>
|
||||
|
||||
<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-primary text-white btn-square"
|
||||
>
|
||||
<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-[7rem] sm:min-h-[5rem] 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
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
<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 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 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 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 w-fit ml-auto mt-3"
|
||||
onClick={submit}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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 mb-5 font-thin">Edit Link</p>
|
||||
|
||||
<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>
|
||||
|
||||
<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" onClick={submit}>
|
||||
Save
|
||||
</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 { 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 mb-5 font-thin">Create a New Collection</p>
|
||||
|
||||
<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 w-fit ml-auto" onClick={submit}>
|
||||
Create Collection
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
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: "",
|
||||
tags: [],
|
||||
screenshotPath: "",
|
||||
pdfPath: "",
|
||||
readabilityPath: "",
|
||||
textContent: "",
|
||||
collection: {
|
||||
name: "",
|
||||
ownerId: data?.user.id as number,
|
||||
},
|
||||
};
|
||||
|
||||
const [link, setLink] =
|
||||
useState<LinkIncludingShortenedCollectionAndTags>(initial);
|
||||
|
||||
const { addLink } = useLinkStore();
|
||||
const [submitLoader, setSubmitLoader] = useState(false);
|
||||
|
||||
const [resetCollectionSelection, setResetCollectionSelection] = useState("");
|
||||
|
||||
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(() => {
|
||||
setResetCollectionSelection(Date.now().toString());
|
||||
console.log(link);
|
||||
|
||||
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) {
|
||||
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 mb-5 font-thin">Create a New Link</p>
|
||||
<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,
|
||||
}}
|
||||
id={resetCollectionSelection}
|
||||
/>
|
||||
) : 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" onClick={submit}>
|
||||
Create Link
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
+113
-92
@@ -1,42 +1,39 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
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 Dropdown from "@/components/Dropdown";
|
||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
import { useRouter } from "next/router";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import useAccountStore from "@/store/account";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { useTheme } from "next-themes";
|
||||
import useWindowDimensions from "@/hooks/useWindowDimensions";
|
||||
import ToggleDarkMode from "./ToggleDarkMode";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||
import NewCollectionModal from "./ModalContent/NewCollectionModal";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Navbar() {
|
||||
const { setModal } = useModalStore();
|
||||
const { settings, updateSettings } = useLocalSettingsStore();
|
||||
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const [profileDropdown, setProfileDropdown] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const handleToggle = () => {
|
||||
if (theme === "dark") {
|
||||
setTheme("light");
|
||||
} else {
|
||||
setTheme("dark");
|
||||
}
|
||||
};
|
||||
|
||||
const [sidebar, setSidebar] = useState(false);
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const handleToggle = () => {
|
||||
if (settings.theme === "dark") {
|
||||
updateSettings({ theme: "light" });
|
||||
} else {
|
||||
updateSettings({ theme: "dark" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSidebar(false);
|
||||
}, [width]);
|
||||
@@ -49,99 +46,123 @@ export default function Navbar() {
|
||||
setSidebar(!sidebar);
|
||||
};
|
||||
|
||||
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||
|
||||
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
|
||||
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" />
|
||||
</div>
|
||||
<SearchBar />
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
onClick={() => {
|
||||
setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
method: "CREATE",
|
||||
});
|
||||
}}
|
||||
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 sm:group-hover:ml-9 sm:absolute duration-100"
|
||||
/>
|
||||
<span className="hidden sm:block group-hover:opacity-0 text-right w-full duration-100">
|
||||
New Link
|
||||
</span>
|
||||
<ToggleDarkMode className="sm:inline-grid hidden" />
|
||||
|
||||
<div className="dropdown dropdown-end">
|
||||
<div className="tooltip tooltip-bottom" data-tip="Create New...">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="flex items-center group btn btn-accent text-white btn-sm px-2"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-5 h-5" />
|
||||
<FontAwesomeIcon
|
||||
icon={faCaretDown}
|
||||
className="w-2 h-2 sm:w-3 sm:h-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<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();
|
||||
setNewCollectionModal(true);
|
||||
}}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
New Collection
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ToggleDarkMode className="sm:flex hidden" />
|
||||
|
||||
<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"
|
||||
>
|
||||
<div className="dropdown dropdown-end">
|
||||
<div tabIndex={0} role="button" className="btn btn-circle btn-ghost">
|
||||
<ProfilePhoto
|
||||
src={account.image ? account.image : undefined}
|
||||
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>
|
||||
{profileDropdown ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/settings/account",
|
||||
},
|
||||
{
|
||||
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}
|
||||
<ul className="dropdown-content z-[1] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||
<li>
|
||||
<Link
|
||||
href="/settings/account"
|
||||
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
<div className="slide-right h-full shadow-lg">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</ClickAwayHandler>
|
||||
</div>
|
||||
) : null}
|
||||
Settings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
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>
|
||||
{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}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+12
-13
@@ -1,40 +1,39 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React from "react";
|
||||
import useModalStore from "@/store/modals";
|
||||
import React, { useState } from "react";
|
||||
import NewLinkModal from "./ModalContent/NewLinkModal";
|
||||
|
||||
type Props = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export default function NoLinksFound({ text }: Props) {
|
||||
const { setModal } = useModalStore();
|
||||
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||
|
||||
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">
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<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 || "You haven't created any Links Here"}
|
||||
</p>
|
||||
<div className="text-center text-black dark:text-white w-full mt-4">
|
||||
<div className="text-center w-full mt-4">
|
||||
<div
|
||||
onClick={() => {
|
||||
setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
method: "CREATE",
|
||||
});
|
||||
setNewLinkModal(true);
|
||||
}}
|
||||
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-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
|
||||
icon={faPlus}
|
||||
className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
|
||||
className="w-5 h-5 left-4 group-hover:ml-[4rem] absolute duration-100"
|
||||
/>
|
||||
<span className="group-hover:opacity-0 text-right w-full duration-100">
|
||||
Create New Link
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{newLinkModal ? (
|
||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+38
-16
@@ -6,11 +6,18 @@ import Image from "next/image";
|
||||
type Props = {
|
||||
src?: string;
|
||||
className?: string;
|
||||
emptyImage?: 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("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,24 +31,39 @@ export default function ProfilePhoto({ src, className, priority }: Props) {
|
||||
|
||||
return !image ? (
|
||||
<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 || ""
|
||||
className={`avatar placeholder ${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>
|
||||
) : (
|
||||
<Image
|
||||
alt=""
|
||||
src={image}
|
||||
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={`avatar drop-shadow-md ${className || ""} ${
|
||||
dimensionClass || "w-8 h-8 "
|
||||
}`}
|
||||
/>
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function LinkCard({ link, count }: Props) {
|
||||
});
|
||||
|
||||
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 flex-col justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -57,14 +57,14 @@ export default function LinkCard({ link, count }: Props) {
|
||||
<Link
|
||||
href={"/public/collections/20?q=" + e.name}
|
||||
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-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</Link>
|
||||
))}
|
||||
</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>·</p>
|
||||
<Link
|
||||
@@ -80,7 +80,7 @@ export default function LinkCard({ link, count }: Props) {
|
||||
{unescapeString(link.description)}{" "}
|
||||
<Link
|
||||
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>
|
||||
<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
|
||||
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
|
||||
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">
|
||||
{label}
|
||||
</span>
|
||||
<span className="rounded select-none">{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export default function RequiredBadge() {
|
||||
return (
|
||||
<span
|
||||
title="Required Field"
|
||||
className="text-black dark:text-white cursor-help"
|
||||
>
|
||||
{" "}
|
||||
*
|
||||
</span>
|
||||
);
|
||||
}
|
||||
+35
-13
@@ -1,23 +1,29 @@
|
||||
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
export default function SearchBar() {
|
||||
type Props = {
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export default function SearchBar({ placeholder }: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const routeQuery = router.query.q;
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState(
|
||||
routeQuery ? decodeURIComponent(routeQuery as 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 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" />
|
||||
</label>
|
||||
@@ -25,18 +31,34 @@ export default function SearchBar() {
|
||||
<input
|
||||
id="search-box"
|
||||
type="text"
|
||||
placeholder="Search for Links"
|
||||
placeholder={placeholder || "Search for Links"}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
e.target.value.includes("%") &&
|
||||
toast.error("The search query should not contain '%'.");
|
||||
setSearchQuery(e.target.value.replace("%", ""));
|
||||
}}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
router.push("/search?q=" + encodeURIComponent(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"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (router.pathname.startsWith("/public")) {
|
||||
if (!searchQuery) {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<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 || ""
|
||||
}`}
|
||||
>
|
||||
@@ -44,18 +44,13 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/account`
|
||||
? "bg-sky-500"
|
||||
: "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`}
|
||||
? "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
|
||||
icon={faUser}
|
||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faUser} className="w-7 h-7 text-primary" />
|
||||
|
||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
||||
Account
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Account</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -63,18 +58,16 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/appearance`
|
||||
? "bg-sky-500"
|
||||
: "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`}
|
||||
? "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
|
||||
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">
|
||||
Appearance
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Appearance</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -82,35 +75,30 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/archive`
|
||||
? "bg-sky-500"
|
||||
: "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`}
|
||||
? "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
|
||||
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">
|
||||
Archive
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Archive</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/settings/api">
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/api` ? "bg-sky-500" : "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`}
|
||||
active === `/settings/api`
|
||||
? "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
|
||||
icon={faKey}
|
||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faKey} className="w-7 h-7 text-primary" />
|
||||
|
||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
||||
API Keys
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">API Keys</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -118,18 +106,13 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/password`
|
||||
? "bg-sky-500"
|
||||
: "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`}
|
||||
? "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
|
||||
icon={faLock}
|
||||
className="w-6 h-6 text-sky-500 dark:text-sky-500"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faLock} className="w-7 h-7 text-primary" />
|
||||
|
||||
<p className="text-black dark:text-white truncate w-full pr-7">
|
||||
Password
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Password</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -138,18 +121,16 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/settings/billing`
|
||||
? "bg-sky-500"
|
||||
: "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`}
|
||||
? "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
|
||||
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">
|
||||
Billing
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Billing</p>
|
||||
</div>
|
||||
</Link>
|
||||
) : undefined}
|
||||
@@ -159,67 +140,59 @@ export default function SettingsSidebar({ className }: { className?: string }) {
|
||||
<Link
|
||||
href={`https://github.com/linkwarden/linkwarden/releases`}
|
||||
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}
|
||||
</Link>
|
||||
<Link href="https://docs.linkwarden.app" target="_blank">
|
||||
<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
|
||||
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">
|
||||
Help
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Help</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="https://github.com/linkwarden/linkwarden" target="_blank">
|
||||
<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
|
||||
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">
|
||||
GitHub
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">GitHub</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="https://twitter.com/LinkwardenHQ" target="_blank">
|
||||
<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
|
||||
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">
|
||||
Twitter
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Twitter</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="https://fosstodon.org/@linkwarden" target="_blank">
|
||||
<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
|
||||
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">
|
||||
Mastodon
|
||||
</p>
|
||||
<p className="truncate w-full pr-7">Mastodon</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+37
-45
@@ -52,7 +52,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
|
||||
return (
|
||||
<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 || ""
|
||||
}`}
|
||||
>
|
||||
@@ -60,64 +60,60 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
<Link href={`/dashboard`}>
|
||||
<div
|
||||
className={`${
|
||||
active === `/dashboard` ? "bg-sky-500" : "hover:bg-slate-500"
|
||||
} 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`}
|
||||
active === `/dashboard` ? "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
|
||||
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">
|
||||
Dashboard
|
||||
</p>
|
||||
<p className="truncate w-full">Dashboard</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={`/links`}>
|
||||
<div
|
||||
className={`${
|
||||
active === `/links` ? "bg-sky-500" : "hover:bg-slate-500"
|
||||
} 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`}
|
||||
active === `/links` ? "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
|
||||
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">
|
||||
All Links
|
||||
</p>
|
||||
<p className="truncate w-full">All Links</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={`/collections`}>
|
||||
<div
|
||||
className={`${
|
||||
active === `/collections` ? "bg-sky-500" : "hover:bg-slate-500"
|
||||
} 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`}
|
||||
active === `/collections`
|
||||
? "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
|
||||
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">
|
||||
All Collections
|
||||
</p>
|
||||
<p className="truncate w-full">All Collections</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href={`/links/pinned`}>
|
||||
<div
|
||||
className={`${
|
||||
active === `/links/pinned` ? "bg-sky-500" : "hover:bg-slate-500"
|
||||
} 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`}
|
||||
active === `/links/pinned`
|
||||
? "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
|
||||
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">
|
||||
Pinned Links
|
||||
</p>
|
||||
<p className="truncate w-full">Pinned Links</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -127,7 +123,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
onClick={() => {
|
||||
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>
|
||||
|
||||
@@ -156,27 +152,25 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/collections/${e.id}`
|
||||
? "bg-sky-500"
|
||||
: "hover:bg-slate-500"
|
||||
} 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`}
|
||||
? "bg-primary/20"
|
||||
: "hover:bg-neutral/20"
|
||||
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="w-6 h-6 drop-shadow"
|
||||
style={{ color: e.color }}
|
||||
/>
|
||||
<p className="text-black dark:text-white truncate w-full">
|
||||
{e.name}
|
||||
</p>
|
||||
<p className="truncate w-full">{e.name}</p>
|
||||
|
||||
{e.isPublic ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faGlobe}
|
||||
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}
|
||||
<div className="drop-shadow text-gray-500 dark:text-gray-300 text-xs">
|
||||
<div className="drop-shadow text-neutral text-xs">
|
||||
{e._count?.links}
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,7 +181,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
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...
|
||||
</p>
|
||||
</div>
|
||||
@@ -200,7 +194,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
onClick={() => {
|
||||
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>
|
||||
<FontAwesomeIcon
|
||||
@@ -226,19 +220,17 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
className={`${
|
||||
active === `/tags/${e.id}`
|
||||
? "bg-sky-500"
|
||||
: "hover:bg-slate-500"
|
||||
} 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`}
|
||||
? "bg-primary/20"
|
||||
: "hover:bg-neutral/20"
|
||||
} duration-100 py-1 px-2 cursor-pointer flex items-center gap-2 w-full rounded-md h-8`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
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">
|
||||
{e.name}
|
||||
</p>
|
||||
<div className="drop-shadow text-gray-500 dark:text-gray-300 text-xs">
|
||||
<p className="truncate w-full pr-7">{e.name}</p>
|
||||
<div className="drop-shadow text-neutral text-xs">
|
||||
{e._count?.links}
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +241,7 @@ export default function Sidebar({ className }: { className?: string }) {
|
||||
<div
|
||||
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...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+120
-55
@@ -1,68 +1,133 @@
|
||||
import React, { Dispatch, SetStateAction } from "react";
|
||||
import ClickAwayHandler from "./ClickAwayHandler";
|
||||
import RadioButton from "./RadioButton";
|
||||
import { Sort } from "@/types/global";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type Props = {
|
||||
sortBy: Sort;
|
||||
setSort: Dispatch<SetStateAction<Sort>>;
|
||||
|
||||
toggleSortDropdown: Function;
|
||||
};
|
||||
|
||||
export default function SortDropdown({
|
||||
sortBy,
|
||||
toggleSortDropdown,
|
||||
setSort,
|
||||
}: Props) {
|
||||
export default function SortDropdown({ sortBy, setSort }: Props) {
|
||||
return (
|
||||
<ClickAwayHandler
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "sort-dropdown") toggleSortDropdown();
|
||||
}}
|
||||
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"
|
||||
>
|
||||
<p className="mb-2 text-black dark:text-white text-center font-semibold">
|
||||
Sort by
|
||||
</p>
|
||||
<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 className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-sm btn-square btn-ghost"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</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 (
|
||||
<button
|
||||
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 ${
|
||||
loading
|
||||
? "bg-sky-600 cursor-auto"
|
||||
: "bg-sky-700 hover:bg-sky-600 cursor-pointer"
|
||||
} ${className || ""}`}
|
||||
className={`btn btn-primary text-white tracking-wider w-fit flex items-center gap-2 ${
|
||||
className || ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!loading && onClick) onClick();
|
||||
}}
|
||||
>
|
||||
{icon && <FontAwesomeIcon icon={icon} className="h-5" />}
|
||||
<p className="text-center w-full">{label}</p>
|
||||
<p>{label}</p>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function TextInput({
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
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 || ""
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -1,31 +1,63 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ToggleDarkMode({ className }: Props) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { settings, updateSettings } = useLocalSettingsStore();
|
||||
|
||||
const handleToggle = () => {
|
||||
if (theme === "dark") {
|
||||
setTheme("light");
|
||||
} else {
|
||||
const [theme, setTheme] = useState(localStorage.getItem("theme"));
|
||||
|
||||
const handleToggle = (e: any) => {
|
||||
if (e.target.checked) {
|
||||
setTheme("dark");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateSettings({ theme: theme as string });
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<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}`}
|
||||
onClick={handleToggle}
|
||||
className="tooltip tooltip-bottom"
|
||||
data-tip={`Switch to ${settings.theme === "light" ? "Dark" : "Light"}`}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={theme === "dark" ? faSun : faMoon}
|
||||
className="w-1/2 h-1/2 text-sky-600 group-hover:text-sky-500"
|
||||
/>
|
||||
<label
|
||||
className={`swap swap-rotate btn-square text-neutral btn btn-ghost btn-sm ${className}`}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useTagStore from "@/store/tags";
|
||||
import useAccountStore from "@/store/account";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
||||
export default function useInitialData() {
|
||||
const { status, data } = useSession();
|
||||
@@ -10,10 +11,12 @@ export default function useInitialData() {
|
||||
const { setTags } = useTagStore();
|
||||
// const { setLinks } = useLinkStore();
|
||||
const { account, setAccount } = useAccountStore();
|
||||
const { setSettings } = useLocalSettingsStore();
|
||||
|
||||
// Get account info
|
||||
useEffect(() => {
|
||||
setSettings();
|
||||
if (status === "authenticated") {
|
||||
// Get account info
|
||||
setAccount(data?.user.id as number);
|
||||
}
|
||||
}, [status, data]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { ReactNode, useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
text?: string;
|
||||
@@ -9,44 +9,29 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function CenteredForm({ text, children }: Props) {
|
||||
const { theme } = useTheme();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
return (
|
||||
<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">
|
||||
{theme ? (
|
||||
{settings.theme ? (
|
||||
<Image
|
||||
src={`/linkwarden_${theme === "dark" ? "dark" : "light"}.png`}
|
||||
src={`/linkwarden_${
|
||||
settings.theme === "dark" ? "dark" : "light"
|
||||
}.png`}
|
||||
width={640}
|
||||
height={136}
|
||||
alt="Linkwarden"
|
||||
className="h-12 w-fit mx-auto"
|
||||
/>
|
||||
) : 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 ? (
|
||||
<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}
|
||||
</p>
|
||||
) : undefined}
|
||||
{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()}{" "}
|
||||
<Link href="https://linkwarden.app" className="font-semibold">
|
||||
Linkwarden
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
<div className="flex gap-3 mb-5 duration-100 items-center justify-between">
|
||||
{/* <div
|
||||
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" />
|
||||
</div> */}
|
||||
@@ -103,7 +103,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
router.push(`/dashboard`);
|
||||
}
|
||||
}}
|
||||
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"
|
||||
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-neutral rounded-md duration-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} className="w-4 h-4" />
|
||||
Back{" "}
|
||||
@@ -138,7 +138,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPen}
|
||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||
className="w-6 h-6 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
@@ -159,7 +159,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faBoxesStacked}
|
||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||
className="w-6 h-6 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faTrashCan}
|
||||
className="w-6 h-6 text-gray-500 dark:text-gray-300"
|
||||
className="w-6 h-6 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
@@ -189,7 +189,7 @@ export default function LinkLayout({ children }: Props) {
|
||||
{children}
|
||||
|
||||
{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
|
||||
className="h-full"
|
||||
onClickOutside={toggleSidebar}
|
||||
|
||||
@@ -53,14 +53,14 @@ export default function SettingsLayout({ children }: Props) {
|
||||
<div className="gap-2 inline-flex mr-3">
|
||||
<div
|
||||
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" />
|
||||
</div>
|
||||
|
||||
<Link
|
||||
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" />
|
||||
</Link>
|
||||
@@ -69,7 +69,7 @@ export default function SettingsLayout({ children }: Props) {
|
||||
{children}
|
||||
|
||||
{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
|
||||
className="h-full"
|
||||
onClickOutside={toggleSidebar}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function htmlDecode(input: string) {
|
||||
export default function unescapeString(input: string) {
|
||||
var doc = new DOMParser().parseFromString(input, "text/html");
|
||||
return doc.documentElement.textContent;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"micro": "^10.0.1",
|
||||
"next": "13.4.12",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"nodemailer": "^6.9.3",
|
||||
"playwright": "^1.35.1",
|
||||
"react": "18.2.0",
|
||||
|
||||
+10
-20
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React from "react";
|
||||
import "@/styles/globals.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import type { AppProps } from "next/app";
|
||||
@@ -6,7 +6,6 @@ import Head from "next/head";
|
||||
import AuthRedirect from "@/layouts/AuthRedirect";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { Session } from "next-auth";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
@@ -14,13 +13,6 @@ export default function App({
|
||||
}: AppProps<{
|
||||
session: Session;
|
||||
}>) {
|
||||
const defaultTheme: "light" | "dark" = "dark";
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem("theme"))
|
||||
localStorage.setItem("theme", defaultTheme);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SessionProvider
|
||||
session={pageProps.session}
|
||||
@@ -50,17 +42,15 @@ export default function App({
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</Head>
|
||||
<AuthRedirect>
|
||||
<ThemeProvider attribute="class">
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
className:
|
||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
<Toaster
|
||||
position="top-center"
|
||||
reverseOrder={false}
|
||||
toastOptions={{
|
||||
className:
|
||||
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
|
||||
}}
|
||||
/>
|
||||
<Component {...pageProps} />
|
||||
</AuthRedirect>
|
||||
</SessionProvider>
|
||||
);
|
||||
|
||||
@@ -41,28 +41,26 @@ export default function ChooseUsername() {
|
||||
return (
|
||||
<CenteredForm>
|
||||
<form onSubmit={submitUsername}>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||
<p className="text-3xl text-center text-black dark:text-white font-extralight">
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
Choose a Username
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Username
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Username</p>
|
||||
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder="john"
|
||||
value={inputedUsername}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setInputedUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-md text-gray-500 dark:text-gray-400 mt-1">
|
||||
<p className="text-md text-neutral mt-1">
|
||||
Feel free to reach out to us at{" "}
|
||||
<a
|
||||
className="font-semibold underline"
|
||||
@@ -83,7 +81,7 @@ export default function ChooseUsername() {
|
||||
|
||||
<div
|
||||
onClick={() => signOut()}
|
||||
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
|
||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||
>
|
||||
Sign Out
|
||||
</div>
|
||||
|
||||
+184
-172
@@ -1,37 +1,32 @@
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import LinkCard from "@/components/LinkCard";
|
||||
import useCollectionStore from "@/store/collections";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global";
|
||||
import {
|
||||
faEllipsis,
|
||||
faFolder,
|
||||
faSort,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEllipsis, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import SortDropdown from "@/components/SortDropdown";
|
||||
import useModalStore from "@/store/modals";
|
||||
import useLinks from "@/hooks/useLinks";
|
||||
import usePermissions from "@/hooks/usePermissions";
|
||||
import NoLinksFound from "@/components/NoLinksFound";
|
||||
import { useTheme } from "next-themes";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import useAccountStore from "@/store/account";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import EditCollectionModal from "@/components/ModalContent/EditCollectionModal";
|
||||
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
|
||||
import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal";
|
||||
|
||||
export default function Index() {
|
||||
const { setModal } = useModalStore();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { links } = useLinkStore();
|
||||
const { collections } = useCollectionStore();
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
const [activeCollection, setActiveCollection] =
|
||||
@@ -47,185 +42,180 @@ export default function Index() {
|
||||
);
|
||||
}, [router, collections]);
|
||||
|
||||
const { account } = useAccountStore();
|
||||
|
||||
const [collectionOwner, setCollectionOwner] = useState({
|
||||
id: null as unknown as number,
|
||||
name: "",
|
||||
username: "",
|
||||
image: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOwner = async () => {
|
||||
if (activeCollection && activeCollection.ownerId !== account.id) {
|
||||
const owner = await getPublicUserData(
|
||||
activeCollection.ownerId as number
|
||||
);
|
||||
setCollectionOwner(owner);
|
||||
} else if (activeCollection && activeCollection.ownerId === account.id) {
|
||||
setCollectionOwner({
|
||||
id: account.id as number,
|
||||
name: account.name,
|
||||
username: account.username as string,
|
||||
image: account.image as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fetchOwner();
|
||||
}, [activeCollection]);
|
||||
|
||||
const [editCollectionModal, setEditCollectionModal] = useState(false);
|
||||
const [editCollectionSharingModal, setEditCollectionSharingModal] =
|
||||
useState(false);
|
||||
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="p-5 flex flex-col gap-5 w-full h-full">
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(-45deg, ${
|
||||
activeCollection?.color
|
||||
}30 10%, ${theme === "dark" ? "#262626" : "#f3f4f6"} 50%, ${
|
||||
theme === "dark" ? "#262626" : "#f9fafb"
|
||||
} 100%)`,
|
||||
}}
|
||||
className="border border-solid border-sky-100 dark:border-neutral-700 rounded-2xl shadow min-h-[10rem] p-5 flex gap-5 flex-col justify-between"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
|
||||
{activeCollection && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
style={{ color: activeCollection?.color }}
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
||||
/>
|
||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white w-full py-1 break-words hyphens-auto font-thin">
|
||||
{activeCollection?.name}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${activeCollection?.color}20 10%, ${
|
||||
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 14rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
className="h-full p-5 flex gap-3 flex-col"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-between sm:items-start">
|
||||
{activeCollection && (
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
style={{ color: activeCollection?.color }}
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-3 drop-shadow"
|
||||
/>
|
||||
<p className="sm:text-4xl text-3xl capitalize w-full py-1 break-words hyphens-auto font-thin">
|
||||
{activeCollection?.name}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{activeCollection ? (
|
||||
{activeCollection ? (
|
||||
<div className={`min-w-[15rem]`}>
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className={`min-w-[15rem] ${
|
||||
activeCollection.members[1] && "mr-3"
|
||||
}`}
|
||||
className="flex items-center btn px-2 btn-ghost rounded-full w-fit"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: activeCollection,
|
||||
defaultIndex: permissions === true ? 1 : 0,
|
||||
})
|
||||
}
|
||||
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-center w-fit sm:mr-0 sm:ml-auto cursor-pointer"
|
||||
>
|
||||
{activeCollection?.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className={`${
|
||||
activeCollection.members[1] && "-mr-3"
|
||||
} border-[3px]`}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 4)}
|
||||
{activeCollection?.members.length &&
|
||||
activeCollection.members.length - 4 > 0 ? (
|
||||
<div className="h-10 w-10 text-white flex items-center justify-center rounded-full border-[3px] bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700 -mr-3">
|
||||
+{activeCollection?.members?.length - 4}
|
||||
{collectionOwner.id ? (
|
||||
<ProfilePhoto src={collectionOwner.image || undefined} />
|
||||
) : undefined}
|
||||
{activeCollection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{activeCollection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{activeCollection.members.length - 3}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="text-black dark:text-white flex justify-between items-end gap-5">
|
||||
<p>{activeCollection?.description}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-black hover:dark:bg-white hover:bg-opacity-10 hover:dark:bg-opacity-10 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<p className="text-neutral text-xs">
|
||||
By {collectionOwner.name}
|
||||
{activeCollection.members.length > 0
|
||||
? ` and ${activeCollection.members.length} others`
|
||||
: undefined}
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
{activeCollection?.description ? (
|
||||
<p>{activeCollection?.description}</p>
|
||||
) : undefined}
|
||||
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div className="flex justify-between items-end gap-5">
|
||||
<p>Showing {activeCollection?._count?.links} results</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
<div className="relative">
|
||||
<div className="dropdown dropdown-bottom dropdown-end">
|
||||
<div
|
||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
||||
id="expand-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-black hover:dark:bg-white hover:bg-opacity-10 hover:dark:bg-opacity-10 duration-100 p-1"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
id="expand-dropdown"
|
||||
title="More"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
{expandDropdown ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
permissions === true
|
||||
? {
|
||||
name: "Edit Collection Info",
|
||||
onClick: () => {
|
||||
activeCollection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: activeCollection,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
name:
|
||||
permissions === true
|
||||
? "Share/Collaborate"
|
||||
: "View Team",
|
||||
onClick: () => {
|
||||
activeCollection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: activeCollection,
|
||||
defaultIndex: permissions === true ? 1 : 0,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name:
|
||||
permissions === true
|
||||
? "Delete Collection"
|
||||
: "Leave Collection",
|
||||
onClick: () => {
|
||||
activeCollection &&
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "UPDATE",
|
||||
isOwner: permissions === true,
|
||||
active: activeCollection,
|
||||
defaultIndex: permissions === true ? 2 : 1,
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "expand-dropdown")
|
||||
setExpandDropdown(false);
|
||||
}}
|
||||
className="absolute top-8 right-0 z-10 w-44"
|
||||
/>
|
||||
) : null}
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-52 mt-1">
|
||||
{permissions === true ? (
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
Edit Collection Info
|
||||
</div>
|
||||
</li>
|
||||
) : undefined}
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setEditCollectionSharingModal(true);
|
||||
}}
|
||||
>
|
||||
{permissions === true
|
||||
? "Share and Collaborate"
|
||||
: "View Team"}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setDeleteCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
{permissions === true
|
||||
? "Delete Collection"
|
||||
: "Leave Collection"}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{links.some((e) => e.collectionId === Number(router.query.id)) ? (
|
||||
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
|
||||
{links
|
||||
@@ -238,6 +228,28 @@ export default function Index() {
|
||||
<NoLinksFound />
|
||||
)}
|
||||
</div>
|
||||
{activeCollection ? (
|
||||
<>
|
||||
{editCollectionModal ? (
|
||||
<EditCollectionModal
|
||||
onClose={() => setEditCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
) : undefined}
|
||||
{editCollectionSharingModal ? (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
) : undefined}
|
||||
{deleteCollectionModal ? (
|
||||
<DeleteCollectionModal
|
||||
onClose={() => setDeleteCollectionModal(false)}
|
||||
activeCollection={activeCollection}
|
||||
/>
|
||||
) : undefined}
|
||||
</>
|
||||
) : undefined}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+43
-81
@@ -3,32 +3,29 @@ import {
|
||||
faEllipsis,
|
||||
faFolder,
|
||||
faPlus,
|
||||
faSort,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import CollectionCard from "@/components/CollectionCard";
|
||||
import Dropdown from "@/components/Dropdown";
|
||||
import { useState } from "react";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useModalStore from "@/store/modals";
|
||||
import SortDropdown from "@/components/SortDropdown";
|
||||
import { Sort } from "@/types/global";
|
||||
import useSort from "@/hooks/useSort";
|
||||
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
|
||||
|
||||
export default function Collections() {
|
||||
const { collections } = useCollectionStore();
|
||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
const [sortedCollections, setSortedCollections] = useState(collections);
|
||||
|
||||
const { data } = useSession();
|
||||
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
useSort({ sortBy, setData: setSortedCollections, data: collections });
|
||||
|
||||
const [newCollectionModal, setNewCollectionModal] = useState(false);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div className="p-5">
|
||||
@@ -37,77 +34,49 @@ export default function Collections() {
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
||||
<p className="text-3xl capitalize font-thin">
|
||||
Your Collections
|
||||
</p>
|
||||
|
||||
<p className="text-black dark:text-white">
|
||||
Collections you own
|
||||
</p>
|
||||
<p>Collections you own</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-2">
|
||||
<div
|
||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
||||
id="expand-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
id="expand-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
<div className="dropdown dropdown-bottom">
|
||||
<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-[20] menu shadow bg-base-200 border border-neutral-content rounded-box w-40 mt-1">
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setNewCollectionModal(true);
|
||||
}}
|
||||
>
|
||||
New Collection
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{expandDropdown ? (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
name: "New Collection",
|
||||
onClick: () => {
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "CREATE",
|
||||
});
|
||||
setExpandDropdown(false);
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "expand-dropdown")
|
||||
setExpandDropdown(false);
|
||||
}}
|
||||
className="absolute top-8 sm:left-0 right-0 sm:right-auto w-36"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,21 +88,13 @@ export default function Collections() {
|
||||
})}
|
||||
|
||||
<div
|
||||
className="p-5 bg-gray-50 dark:bg-neutral-800 self-stretch border border-solid border-sky-100 dark:border-neutral-700 min-h-[12rem] rounded-2xl cursor-pointer shadow duration-100 hover:shadow-none flex flex-col gap-4 justify-center items-center group"
|
||||
onClick={() => {
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "CREATE",
|
||||
});
|
||||
}}
|
||||
className="card card-compact shadow-md hover:shadow-none duration-200 border border-neutral-content p-5 bg-base-200 self-stretch min-h-[12rem] rounded-2xl cursor-pointer flex flex-col gap-4 justify-center items-center group btn"
|
||||
onClick={() => setNewCollectionModal(true)}
|
||||
>
|
||||
<p className="text-black dark:text-white group-hover:opacity-0 duration-100">
|
||||
New Collection
|
||||
</p>
|
||||
<p className="group-hover:opacity-0 duration-100">New Collection</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faPlus}
|
||||
className="w-8 h-8 text-sky-500 dark:text-sky-500 group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100"
|
||||
className="w-8 h-8 text-primary group-hover:w-12 group-hover:h-12 group-hover:-mt-10 duration-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,16 +104,14 @@ export default function Collections() {
|
||||
<div className="flex items-center gap-3 my-5">
|
||||
<FontAwesomeIcon
|
||||
icon={faFolder}
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
||||
<p className="text-3xl capitalize font-thin">
|
||||
Other Collections
|
||||
</p>
|
||||
|
||||
<p className="text-black dark:text-white">
|
||||
Shared collections you're a member of
|
||||
</p>
|
||||
<p>Shared collections you're a member of</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,6 +125,9 @@ export default function Collections() {
|
||||
</>
|
||||
) : undefined}
|
||||
</div>
|
||||
{newCollectionModal ? (
|
||||
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
|
||||
) : undefined}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import React from "react";
|
||||
export default function EmailConfirmaion() {
|
||||
return (
|
||||
<CenteredForm>
|
||||
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-sky-100 dark:border-neutral-700 bg-slate-50 text-black dark:text-white dark:bg-neutral-800">
|
||||
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-neutral-content bg-base-200">
|
||||
<p className="text-center text-2xl sm:text-3xl font-extralight mb-2 ">
|
||||
Please check your Email
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700 my-3" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<p>A sign in link has been sent to your email address.</p>
|
||||
|
||||
|
||||
+79
-107
@@ -23,8 +23,8 @@ import React from "react";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { MigrationFormat, MigrationRequest } from "@/types/global";
|
||||
import ClickAwayHandler from "@/components/ClickAwayHandler";
|
||||
import DashboardItem from "@/components/DashboardItem";
|
||||
import NewLinkModal from "@/components/ModalContent/NewLinkModal";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { collections } = useCollectionStore();
|
||||
@@ -63,8 +63,6 @@ export default function Dashboard() {
|
||||
handleNumberOfLinksToShow();
|
||||
}, [width]);
|
||||
|
||||
const [importDropdown, setImportDropdown] = useState(false);
|
||||
|
||||
const importBookmarks = async (e: any, format: MigrationFormat) => {
|
||||
const file: File = e.target.files[0];
|
||||
|
||||
@@ -92,8 +90,6 @@ export default function Dashboard() {
|
||||
|
||||
toast.success("Imported the Bookmarks! Reloading the page...");
|
||||
|
||||
setImportDropdown(false);
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
@@ -104,35 +100,32 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
const [newLinkModal, setNewLinkModal] = useState(false);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon
|
||||
icon={faChartSimple}
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
||||
Dashboard
|
||||
</p>
|
||||
<p className="text-3xl capitalize font-thin">Dashboard</p>
|
||||
|
||||
<p className="text-black dark:text-white">
|
||||
A brief overview of your data
|
||||
</p>
|
||||
<p>A brief overview of your data</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-sky-100 dark:border-neutral-700 bg-gray-100 dark:bg-neutral-800">
|
||||
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
|
||||
<DashboardItem
|
||||
name={numberOfLinks === 1 ? "Link" : "Links"}
|
||||
value={numberOfLinks}
|
||||
icon={faLink}
|
||||
/>
|
||||
|
||||
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
|
||||
<div className="h-24 border-1 border-l border-sky-100 dark:border-neutral-700 hidden md:block"></div>
|
||||
<div className="divider md:divider-horizontal"></div>
|
||||
|
||||
<DashboardItem
|
||||
name={collections.length === 1 ? "Collection" : "Collections"}
|
||||
@@ -140,8 +133,7 @@ export default function Dashboard() {
|
||||
icon={faFolder}
|
||||
/>
|
||||
|
||||
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
|
||||
<div className="h-24 border-1 border-r border-sky-100 dark:border-neutral-700 hidden md:block"></div>
|
||||
<div className="divider md:divider-horizontal"></div>
|
||||
|
||||
<DashboardItem
|
||||
name={tags.length === 1 ? "Tag" : "Tags"}
|
||||
@@ -155,21 +147,16 @@ export default function Dashboard() {
|
||||
<div className="flex gap-2 items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faClockRotateLeft}
|
||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="w-5 h-5 text-primary drop-shadow"
|
||||
/>
|
||||
<p className="text-2xl text-black dark:text-white">
|
||||
Recently Added Links
|
||||
</p>
|
||||
<p className="text-2xl">Recently Added Links</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/links"
|
||||
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
View All
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={`w-4 h-4 text-black dark:text-white`}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -180,7 +167,7 @@ export default function Dashboard() {
|
||||
{links[0] ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
>
|
||||
{links.slice(0, showLinks).map((e, i) => (
|
||||
<LinkCard key={i} link={e} count={i} />
|
||||
@@ -190,101 +177,87 @@ export default function Dashboard() {
|
||||
) : (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
|
||||
>
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<p className="text-center text-2xl">
|
||||
View Your Recently Added Links Here!
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
|
||||
This section will view your latest added Links across every
|
||||
Collections you have access to.
|
||||
</p>
|
||||
|
||||
<div className="text-center text-black dark:text-white w-full mt-4 flex flex-wrap gap-4 justify-center">
|
||||
<div className="text-center w-full mt-4 flex flex-wrap gap-4 justify-center">
|
||||
<div
|
||||
onClick={() => {
|
||||
setModal({
|
||||
modal: "LINK",
|
||||
state: true,
|
||||
method: "CREATE",
|
||||
});
|
||||
setNewLinkModal(true);
|
||||
}}
|
||||
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group"
|
||||
className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent text-white group"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faPlus}
|
||||
className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
|
||||
className="w-5 h-5 left-4 group-hover:ml-[4rem] absolute duration-100"
|
||||
/>
|
||||
<span className="group-hover:opacity-0 text-right w-full duration-100">
|
||||
Create New Link
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="dropdown dropdown-bottom">
|
||||
<div
|
||||
onClick={() => setImportDropdown(!importDropdown)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
|
||||
id="import-dropdown"
|
||||
className="flex gap-2 select-none text-sm cursor-pointer p-2 px-3 rounded-md border dark:hover:border-sky-600 text-black border-black dark:text-white dark:border-white hover:border-sky-500 hover:dark:border-sky-500 hover:text-sky-500 hover:dark:text-sky-500 duration-100 group"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFileImport}
|
||||
className="w-5 h-5 duration-100"
|
||||
id="import-dropdown"
|
||||
/>
|
||||
<span
|
||||
className="text-right w-full duration-100"
|
||||
id="import-dropdown"
|
||||
>
|
||||
Import Your Bookmarks
|
||||
</span>
|
||||
<p>Import From</p>
|
||||
</div>
|
||||
{importDropdown ? (
|
||||
<ClickAwayHandler
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "import-dropdown")
|
||||
setImportDropdown(false);
|
||||
}}
|
||||
className={`absolute text-black dark:text-white top-10 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
|
||||
>
|
||||
<div className="cursor-pointer rounded-md">
|
||||
<label
|
||||
htmlFor="import-linkwarden-file"
|
||||
title="JSON File"
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
||||
>
|
||||
Linkwarden File...
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-linkwarden-file"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.linkwarden)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="import-html-file"
|
||||
title="HTML File"
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
||||
>
|
||||
Bookmarks HTML file...
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-html-file"
|
||||
accept=".html"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.htmlFile)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</ClickAwayHandler>
|
||||
) : null}
|
||||
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||
<li>
|
||||
<label
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
htmlFor="import-linkwarden-file"
|
||||
title="JSON File"
|
||||
>
|
||||
From Linkwarden
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-linkwarden-file"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.linkwarden)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
htmlFor="import-html-file"
|
||||
title="HTML File"
|
||||
>
|
||||
From Bookmarks HTML file
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-html-file"
|
||||
accept=".html"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.htmlFile)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,19 +268,16 @@ export default function Dashboard() {
|
||||
<div className="flex gap-2 items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faThumbTack}
|
||||
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="w-5 h-5 text-primary drop-shadow"
|
||||
/>
|
||||
<p className="text-2xl text-black dark:text-white">Pinned Links</p>
|
||||
<p className="text-2xl">Pinned Links</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/links/pinned"
|
||||
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
View All
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className={`w-4 h-4 text-black dark:text-white`}
|
||||
/>
|
||||
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -318,10 +288,9 @@ export default function Dashboard() {
|
||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||
<div className="w-full">
|
||||
<div
|
||||
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
|
||||
>
|
||||
{links
|
||||
|
||||
.filter((e) => e.pinnedBy && e.pinnedBy[0])
|
||||
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
|
||||
.slice(0, showLinks)}
|
||||
@@ -330,12 +299,12 @@ export default function Dashboard() {
|
||||
) : (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
|
||||
>
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<p className="text-center text-2xl">
|
||||
Pin Your Favorite Links Here!
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
|
||||
You can Pin your favorite Links by clicking on the three dots on
|
||||
each Link and clicking{" "}
|
||||
<span className="font-semibold">Pin to Dashboard</span>.
|
||||
@@ -344,6 +313,9 @@ export default function Dashboard() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{newLinkModal ? (
|
||||
<NewLinkModal onClose={() => setNewLinkModal(false)} />
|
||||
) : undefined}
|
||||
</MainLayout>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-13
@@ -43,34 +43,32 @@ export default function Forgot() {
|
||||
return (
|
||||
<CenteredForm>
|
||||
<form onSubmit={sendConfirmation}>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||
<p className="text-3xl text-center text-black dark:text-white font-extralight">
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
Password Recovery
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div>
|
||||
<p className="text-black dark:text-white">
|
||||
<p>
|
||||
Enter your email so we can send you a link to recover your
|
||||
account. Make sure to change your password in the profile settings
|
||||
afterwards.
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<p className="text-sm text-neutral">
|
||||
You wont get logged in if you haven't created an account yet.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Email
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Email</p>
|
||||
|
||||
<TextInput
|
||||
autoFocus
|
||||
type="email"
|
||||
placeholder="johnny@example.com"
|
||||
value={form.email}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
@@ -82,10 +80,7 @@ export default function Forgot() {
|
||||
loading={submitLoader}
|
||||
/>
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<Link
|
||||
href={"/login"}
|
||||
className="block text-black dark:text-white font-bold"
|
||||
>
|
||||
<Link href={"/login"} className="block font-bold">
|
||||
Go back
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+13
-12
@@ -9,7 +9,6 @@ import {
|
||||
} from "@/types/global";
|
||||
import Image from "next/image";
|
||||
import ColorThief, { RGBColor } from "colorthief";
|
||||
import { useTheme } from "next-themes";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import DOMPurify from "dompurify";
|
||||
@@ -17,6 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
||||
type LinkContent = {
|
||||
title: string;
|
||||
@@ -31,10 +31,11 @@ type LinkContent = {
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const { theme } = useTheme();
|
||||
const { links, getLink } = useLinkStore();
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
const session = useSession();
|
||||
const userId = session.data?.user.id;
|
||||
|
||||
@@ -140,18 +141,18 @@ export default function Index() {
|
||||
)})30`;
|
||||
}
|
||||
}
|
||||
}, [colorPalette, theme]);
|
||||
}, [colorPalette]);
|
||||
|
||||
return (
|
||||
<LinkLayout>
|
||||
<div
|
||||
className={`flex flex-col max-w-screen-md h-full ${
|
||||
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||
settings.theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
id="link-banner"
|
||||
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
|
||||
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-neutral-content shadow-md"
|
||||
>
|
||||
<div id="link-banner-inner" className="link-banner-inner"></div>
|
||||
|
||||
@@ -164,7 +165,7 @@ export default function Index() {
|
||||
height={42}
|
||||
alt=""
|
||||
id={"favicon-" + link.id}
|
||||
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
|
||||
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-base-100 bg-base-100 aspect-square"
|
||||
draggable="false"
|
||||
onLoad={(e) => {
|
||||
try {
|
||||
@@ -184,7 +185,7 @@ export default function Index() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
|
||||
<div className="flex gap-2 text-sm text-neutral">
|
||||
<p className=" min-w-fit">
|
||||
{link?.createdAt
|
||||
? new Date(link?.createdAt).toLocaleString("en-US", {
|
||||
@@ -229,7 +230,7 @@ export default function Index() {
|
||||
/>
|
||||
<p
|
||||
title={link?.collection.name}
|
||||
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
|
||||
className="text-lg truncate max-w-[12rem]"
|
||||
>
|
||||
{link?.collection.name}
|
||||
</p>
|
||||
@@ -238,7 +239,7 @@ export default function Index() {
|
||||
<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]"
|
||||
className="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</p>
|
||||
@@ -258,17 +259,17 @@ export default function Index() {
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
|
||||
<div className="border border-solid border-neutral-content w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-base-200">
|
||||
{link?.readabilityPath === "pending" ? (
|
||||
<p className="text-center">
|
||||
Generating readable format, please wait...
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<p className="text-center text-2xl">
|
||||
There is no reader view for this webpage
|
||||
</p>
|
||||
<p className="text-center text-sm text-black dark:text-white">
|
||||
<p className="text-center text-sm">
|
||||
{link?.collection.ownerId === userId
|
||||
? "You can update (refetch) the preserved formats by managing them below"
|
||||
: "The collections owners can refetch the preserved formats"}
|
||||
|
||||
+5
-28
@@ -5,14 +5,13 @@ import useLinks from "@/hooks/useLinks";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { Sort } from "@/types/global";
|
||||
import { faLink, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Links() {
|
||||
const { links } = useLinkStore();
|
||||
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
useLinks({ sort: sortBy });
|
||||
@@ -24,39 +23,17 @@ export default function Links() {
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon
|
||||
icon={faLink}
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
||||
All Links
|
||||
</p>
|
||||
<p className="text-3xl capitalize font-thin">All Links</p>
|
||||
|
||||
<p className="text-black dark:text-white">
|
||||
Links from every Collections
|
||||
</p>
|
||||
<p>Links from every Collections</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
{links[0] ? (
|
||||
|
||||
+8
-32
@@ -1,18 +1,16 @@
|
||||
import LinkCard from "@/components/LinkCard";
|
||||
import NoLinksFound from "@/components/NoLinksFound";
|
||||
import SortDropdown from "@/components/SortDropdown";
|
||||
import useLinks from "@/hooks/useLinks";
|
||||
import MainLayout from "@/layouts/MainLayout";
|
||||
import useLinkStore from "@/store/links";
|
||||
import { Sort } from "@/types/global";
|
||||
import { faSort, faThumbTack } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faThumbTack } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function PinnedLinks() {
|
||||
const { links } = useLinkStore();
|
||||
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
useLinks({ sort: sortBy, pinnedOnly: true });
|
||||
@@ -24,39 +22,17 @@ export default function PinnedLinks() {
|
||||
<div className="flex items-center gap-3">
|
||||
<FontAwesomeIcon
|
||||
icon={faThumbTack}
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-3xl capitalize text-black dark:text-white font-thin">
|
||||
Pinned Links
|
||||
</p>
|
||||
<p className="text-3xl capitalize font-thin">Pinned Links</p>
|
||||
|
||||
<p className="text-black dark:text-white">
|
||||
Pinned Links from your Collections
|
||||
</p>
|
||||
<p>Pinned Links from your Collections</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mt-2">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
|
||||
@@ -68,12 +44,12 @@ export default function PinnedLinks() {
|
||||
) : (
|
||||
<div
|
||||
style={{ flex: "1 1 auto" }}
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
|
||||
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
|
||||
>
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<p className="text-center text-2xl">
|
||||
Pin Your Favorite Links Here!
|
||||
</p>
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
|
||||
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
|
||||
You can Pin your favorite Links by clicking on the three dots on
|
||||
each Link and clicking{" "}
|
||||
<span className="font-semibold">Pin to Dashboard</span>.
|
||||
|
||||
+10
-20
@@ -63,15 +63,15 @@ export default function Login() {
|
||||
return (
|
||||
<CenteredForm text="Sign in to your account">
|
||||
<form onSubmit={loginUser}>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||
<p className="text-3xl text-black dark:text-white text-center font-extralight">
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
Enter your credentials
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
<p className="text-sm w-fit font-semibold mb-1">
|
||||
Username
|
||||
{emailEnabled ? " or Email" : undefined}
|
||||
</p>
|
||||
@@ -80,29 +80,24 @@ export default function Login() {
|
||||
autoFocus={true}
|
||||
placeholder="johnny"
|
||||
value={form.username}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Password
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Password</p>
|
||||
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
{emailEnabled && (
|
||||
<div className="w-fit ml-auto mt-1">
|
||||
<Link
|
||||
href={"/forgot"}
|
||||
className="text-gray-500 dark:text-gray-400 font-semibold"
|
||||
>
|
||||
<Link href={"/forgot"} className="text-neutral font-semibold">
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
@@ -127,13 +122,8 @@ export default function Login() {
|
||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION ===
|
||||
"true" ? undefined : (
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<p className="w-fit text-gray-500 dark:text-gray-400">
|
||||
New here?
|
||||
</p>
|
||||
<Link
|
||||
href={"/register"}
|
||||
className="block text-black dark:text-white font-semibold"
|
||||
>
|
||||
<p className="w-fit text-neutral">New here?</p>
|
||||
<Link href={"/register"} className="block font-semibold">
|
||||
Sign Up
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -9,18 +9,16 @@ import Head from "next/head";
|
||||
import useLinks from "@/hooks/useLinks";
|
||||
import useLinkStore from "@/store/links";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import useModalStore from "@/store/modals";
|
||||
import ModalManagement from "@/components/ModalManagement";
|
||||
import ToggleDarkMode from "@/components/ToggleDarkMode";
|
||||
import { useTheme } from "next-themes";
|
||||
import getPublicUserData from "@/lib/client/getPublicUserData";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import PublicSearchBar from "@/components/PublicPage/PublicSearchBar";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFilter, faSort } from "@fortawesome/free-solid-svg-icons";
|
||||
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
|
||||
import SortDropdown from "@/components/SortDropdown";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
|
||||
|
||||
const cardVariants: Variants = {
|
||||
offscreen: {
|
||||
@@ -38,15 +36,8 @@ const cardVariants: Variants = {
|
||||
|
||||
export default function PublicCollections() {
|
||||
const { links } = useLinkStore();
|
||||
const { modal, setModal } = useModalStore();
|
||||
|
||||
useEffect(() => {
|
||||
modal
|
||||
? (document.body.style.overflow = "hidden")
|
||||
: (document.body.style.overflow = "auto");
|
||||
}, [modal]);
|
||||
|
||||
const { theme } = useTheme();
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -65,8 +56,6 @@ export default function PublicCollections() {
|
||||
tags: true,
|
||||
});
|
||||
|
||||
const [filterDropdown, setFilterDropdown] = useState(false);
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
useLinks({
|
||||
@@ -101,13 +90,16 @@ export default function PublicCollections() {
|
||||
fetchOwner();
|
||||
}, [collection]);
|
||||
|
||||
const [editCollectionSharingModal, setEditCollectionSharingModal] =
|
||||
useState(false);
|
||||
|
||||
return collection ? (
|
||||
<div
|
||||
className="h-screen"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
|
||||
theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 50%, ${theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
settings.theme === "dark" ? "#262626" : "#f3f4f6"
|
||||
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
|
||||
}}
|
||||
>
|
||||
<ModalManagement />
|
||||
@@ -128,63 +120,56 @@ export default function PublicCollections() {
|
||||
{collection.name}
|
||||
</p>
|
||||
<div className="flex gap-2 items-center mt-8 min-w-fit">
|
||||
<ToggleDarkMode className="w-8 h-8 flex" />
|
||||
<ToggleDarkMode />
|
||||
|
||||
<Link href="https://linkwarden.app/" target="_blank">
|
||||
<Image
|
||||
src={`/icon.png`}
|
||||
width={551}
|
||||
height={551}
|
||||
alt="Linkwarden"
|
||||
title="Linkwarden"
|
||||
className="h-8 w-fit mx-auto"
|
||||
title="Created with Linkwarden"
|
||||
className="h-8 w-fit mx-auto rounded"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mt-3">
|
||||
<div className={`min-w-[15rem]`}>
|
||||
<div
|
||||
onClick={() =>
|
||||
setModal({
|
||||
modal: "COLLECTION",
|
||||
state: true,
|
||||
method: "VIEW_TEAM",
|
||||
isOwner: false,
|
||||
active: collection,
|
||||
defaultIndex: 0,
|
||||
})
|
||||
}
|
||||
className="hover:opacity-80 duration-100 flex justify-center sm:justify-end items-start w-fit cursor-pointer"
|
||||
>
|
||||
{collectionOwner.id ? (
|
||||
<ProfilePhoto
|
||||
src={
|
||||
collectionOwner.image ? collectionOwner.image : undefined
|
||||
}
|
||||
className={`w-8 h-8 border-2`}
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className={`w-8 h-8 border-2`}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection?.members.length &&
|
||||
collection.members.length - 3 > 0 ? (
|
||||
<div className="w-8 h-8 min-w-[2rem] text-white text-sm flex items-center justify-center rounded-full border-2 bg-sky-600 dark:bg-sky-600 border-slate-200 dark:border-neutral-700">
|
||||
+{collection?.members?.length - 3}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex gap-1 justify-center sm:justify-end items-center w-fit">
|
||||
<div
|
||||
className="flex items-center btn px-2 btn-ghost rounded-full"
|
||||
onClick={() => setEditCollectionSharingModal(true)}
|
||||
>
|
||||
{collectionOwner.id ? (
|
||||
<ProfilePhoto
|
||||
src={collectionOwner.image || undefined}
|
||||
dimensionClass="w-7 h-7"
|
||||
/>
|
||||
) : undefined}
|
||||
{collection.members
|
||||
.sort((a, b) => (a.userId as number) - (b.userId as number))
|
||||
.map((e, i) => {
|
||||
return (
|
||||
<ProfilePhoto
|
||||
key={i}
|
||||
src={e.user.image ? e.user.image : undefined}
|
||||
className="-ml-3"
|
||||
/>
|
||||
);
|
||||
})
|
||||
.slice(0, 3)}
|
||||
{collection.members.length - 3 > 0 ? (
|
||||
<div className={`avatar placeholder -ml-3`}>
|
||||
<div className="bg-base-100 text-neutral rounded-full w-8 h-8 ring-2 ring-neutral-content">
|
||||
<span>+{collection.members.length - 3}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<p className="ml-2 mt-1 text-gray-500 dark:text-gray-300">
|
||||
<p className="text-neutral text-xs">
|
||||
By {collectionOwner.name}
|
||||
{collection.members.length > 0
|
||||
? ` and ${collection.members.length} others`
|
||||
@@ -197,57 +182,24 @@ export default function PublicCollections() {
|
||||
|
||||
<p className="mt-5">{collection.description}</p>
|
||||
|
||||
<hr className="mt-5 border-1 border-neutral-500" />
|
||||
<div className="divider mt-5 mb-0"></div>
|
||||
|
||||
<div className="flex mb-5 mt-10 flex-col gap-5">
|
||||
<div className="flex justify-between">
|
||||
<PublicSearchBar
|
||||
placeHolder={`Search ${collection._count?.links} Links`}
|
||||
<SearchBar
|
||||
placeholder={`Search ${collection._count?.links} Links`}
|
||||
/>
|
||||
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setFilterDropdown(!filterDropdown)}
|
||||
id="filter-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFilter}
|
||||
id="filter-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filterDropdown ? (
|
||||
<FilterSearchDropdown
|
||||
setFilterDropdown={setFilterDropdown}
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
/>
|
||||
) : null}
|
||||
<FilterSearchDropdown
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-neutral-500 hover:bg-opacity-40 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,11 +223,17 @@ export default function PublicCollections() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* <p className="text-center text-gray-500">
|
||||
{/* <p className="text-center text-neutral">
|
||||
List created with <span className="text-black">Linkwarden.</span>
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
{editCollectionSharingModal ? (
|
||||
<EditCollectionSharingModal
|
||||
onClose={() => setEditCollectionSharingModal(false)}
|
||||
activeCollection={collection}
|
||||
/>
|
||||
) : undefined}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
+18
-13
@@ -9,7 +9,6 @@ import {
|
||||
} from "@/types/global";
|
||||
import Image from "next/image";
|
||||
import ColorThief, { RGBColor } from "colorthief";
|
||||
import { useTheme } from "next-themes";
|
||||
import unescapeString from "@/lib/client/unescapeString";
|
||||
import isValidUrl from "@/lib/shared/isValidUrl";
|
||||
import DOMPurify from "dompurify";
|
||||
@@ -17,6 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBoxesStacked, faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||
import useModalStore from "@/store/modals";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
||||
type LinkContent = {
|
||||
title: string;
|
||||
@@ -31,10 +31,11 @@ type LinkContent = {
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const { theme } = useTheme();
|
||||
const { links, getLink } = useLinkStore();
|
||||
const { setModal } = useModalStore();
|
||||
|
||||
const { settings } = useLocalSettingsStore();
|
||||
|
||||
const session = useSession();
|
||||
const userId = session.data?.user.id;
|
||||
|
||||
@@ -140,18 +141,18 @@ export default function Index() {
|
||||
)})30`;
|
||||
}
|
||||
}
|
||||
}, [colorPalette, theme]);
|
||||
}, [colorPalette]);
|
||||
|
||||
return (
|
||||
<LinkLayout>
|
||||
<div
|
||||
className={`flex flex-col max-w-screen-md h-full ${
|
||||
theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||
settings.theme === "dark" ? "banner-dark-mode" : "banner-light-mode"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
id="link-banner"
|
||||
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-sky-100 dark:border-neutral-700 shadow-md"
|
||||
className="link-banner p-5 mb-4 relative bg-opacity-10 border border-solid border-neutral-content shadow-md"
|
||||
>
|
||||
<div id="link-banner-inner" className="link-banner-inner"></div>
|
||||
|
||||
@@ -164,7 +165,7 @@ export default function Index() {
|
||||
height={42}
|
||||
alt=""
|
||||
id={"favicon-" + link.id}
|
||||
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-white dark:border-neutral-900 bg-white dark:bg-neutral-900 aspect-square"
|
||||
className="select-none mt-2 w-10 rounded-md shadow border-[3px] border-base-100 bg-base-100 aspect-square"
|
||||
draggable="false"
|
||||
onLoad={(e) => {
|
||||
try {
|
||||
@@ -184,7 +185,7 @@ export default function Index() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 text-sm text-gray-500 dark:text-gray-300">
|
||||
<div className="flex gap-2 text-sm text-neutral">
|
||||
<p className=" min-w-fit">
|
||||
{link?.createdAt
|
||||
? new Date(link?.createdAt).toLocaleString("en-US", {
|
||||
@@ -229,16 +230,20 @@ export default function Index() {
|
||||
/>
|
||||
<p
|
||||
title={link?.collection?.name}
|
||||
className="text-black dark:text-white text-lg truncate max-w-[12rem]"
|
||||
className="text-lg truncate max-w-[12rem]"
|
||||
>
|
||||
{link?.collection?.name}
|
||||
</p>
|
||||
</Link>
|
||||
{link?.tags.map((e, i) => (
|
||||
<Link key={i} href={`/tags/${e.id}`} className="z-10">
|
||||
<Link
|
||||
key={i}
|
||||
href={"/public/collections/20?q=" + e.name}
|
||||
className="z-10"
|
||||
>
|
||||
<p
|
||||
title={e.name}
|
||||
className="px-2 py-1 bg-sky-200 text-black dark:text-white dark:bg-sky-900 text-xs rounded-3xl cursor-pointer hover:opacity-60 duration-100 truncate max-w-[19rem]"
|
||||
className="btn btn-xs btn-outline truncate max-w-[19rem]"
|
||||
>
|
||||
{e.name}
|
||||
</p>
|
||||
@@ -258,17 +263,17 @@ export default function Index() {
|
||||
}}
|
||||
></div>
|
||||
) : (
|
||||
<div className="border border-solid border-sky-100 dark:border-neutral-700 w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800">
|
||||
<div className="border border-solid border-neutral-content w-full h-full flex flex-col justify-center p-10 rounded-2xl bg-base-200">
|
||||
{link?.readabilityPath === "pending" ? (
|
||||
<p className="text-center">
|
||||
Generating readable format, please wait...
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-center text-2xl text-black dark:text-white">
|
||||
<p className="text-center text-2xl">
|
||||
There is no reader view for this webpage
|
||||
</p>
|
||||
<p className="text-center text-sm text-black dark:text-white">
|
||||
<p className="text-center text-sm">
|
||||
{link?.collection?.ownerId === userId
|
||||
? "You can update (refetch) the preserved formats by managing them below"
|
||||
: "The collections owners can refetch the preserved formats"}
|
||||
|
||||
+18
-31
@@ -104,7 +104,7 @@ export default function Register() {
|
||||
}
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
|
||||
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p>
|
||||
Registration is disabled for this instance, please contact the admin
|
||||
in case of any issues.
|
||||
@@ -112,37 +112,33 @@ export default function Register() {
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={registerUser}>
|
||||
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
|
||||
<p className="text-3xl text-black dark:text-white text-center font-extralight">
|
||||
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="text-3xl text-center font-extralight">
|
||||
Enter your details
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Display Name
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Display Name</p>
|
||||
|
||||
<TextInput
|
||||
autoFocus={true}
|
||||
placeholder="Johnny"
|
||||
value={form.name}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{emailEnabled ? undefined : (
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Username
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Username</p>
|
||||
|
||||
<TextInput
|
||||
placeholder="john"
|
||||
value={form.username}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, username: e.target.value })
|
||||
}
|
||||
@@ -152,36 +148,32 @@ export default function Register() {
|
||||
|
||||
{emailEnabled ? (
|
||||
<div>
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Email
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Email</p>
|
||||
|
||||
<TextInput
|
||||
type="email"
|
||||
placeholder="johnny@example.com"
|
||||
value={form.email}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
<div className="w-full">
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
Password
|
||||
</p>
|
||||
<p className="text-sm w-fit font-semibold mb-1">Password</p>
|
||||
|
||||
<TextInput
|
||||
type="password"
|
||||
placeholder="••••••••••••••"
|
||||
value={form.password}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) => setForm({ ...form, password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
|
||||
<p className="text-sm w-fit font-semibold mb-1">
|
||||
Confirm Password
|
||||
</p>
|
||||
|
||||
@@ -189,7 +181,7 @@ export default function Register() {
|
||||
type="password"
|
||||
placeholder="••••••••••••••"
|
||||
value={form.passwordConfirmation}
|
||||
className="bg-white"
|
||||
className="bg-base-100"
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, passwordConfirmation: e.target.value })
|
||||
}
|
||||
@@ -198,7 +190,7 @@ export default function Register() {
|
||||
|
||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-neutral">
|
||||
By signing up, you agree to our{" "}
|
||||
<Link
|
||||
href="https://linkwarden.app/tos"
|
||||
@@ -215,7 +207,7 @@ export default function Register() {
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-neutral">
|
||||
Need help?{" "}
|
||||
<Link
|
||||
href="mailto:support@linkwarden.app"
|
||||
@@ -235,13 +227,8 @@ export default function Register() {
|
||||
<p className="text-center w-full font-bold">Sign Up</p>
|
||||
</button>
|
||||
<div className="flex items-baseline gap-1 justify-center">
|
||||
<p className="w-fit text-gray-500 dark:text-gray-400">
|
||||
Already have an account?
|
||||
</p>
|
||||
<Link
|
||||
href={"/login"}
|
||||
className="block text-black dark:text-white font-bold"
|
||||
>
|
||||
<p className="w-fit text-neutral">Already have an account?</p>
|
||||
<Link href={"/login"} className="block font-bold">
|
||||
Login
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+9
-43
@@ -24,7 +24,6 @@ export default function Search() {
|
||||
});
|
||||
|
||||
const [filterDropdown, setFilterDropdown] = useState(false);
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
useLinks({
|
||||
@@ -45,57 +44,24 @@ export default function Search() {
|
||||
<div className="flex gap-2">
|
||||
<FontAwesomeIcon
|
||||
icon={faSearch}
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary drop-shadow"
|
||||
/>
|
||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
|
||||
<p className="sm:text-4xl text-3xl capitalize font-thin">
|
||||
Search Results
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setFilterDropdown(!filterDropdown)}
|
||||
id="filter-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faFilter}
|
||||
id="filter-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filterDropdown ? (
|
||||
<FilterSearchDropdown
|
||||
setFilterDropdown={setFilterDropdown}
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
/>
|
||||
) : null}
|
||||
<FilterSearchDropdown
|
||||
searchFilter={searchFilter}
|
||||
setSearchFilter={setSearchFilter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,7 +72,7 @@ export default function Search() {
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-black dark:text-white">
|
||||
<p>
|
||||
Nothing found.{" "}
|
||||
<span className="font-bold text-xl" title="Shruggie">
|
||||
¯\_(ツ)_/¯
|
||||
|
||||
+78
-90
@@ -153,37 +153,40 @@ export default function Account() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">Account Settings</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">Display Name</p>
|
||||
<p className="mb-2">Display Name</p>
|
||||
<TextInput
|
||||
value={user.name || ""}
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setUser({ ...user, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">Username</p>
|
||||
<p className="mb-2">Username</p>
|
||||
<TextInput
|
||||
value={user.username || ""}
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setUser({ ...user, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{emailEnabled ? (
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">Email</p>
|
||||
<p className="mb-2">Email</p>
|
||||
{user.email !== account.email &&
|
||||
process.env.NEXT_PUBLIC_STRIPE === "true" ? (
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-2 text-sm">
|
||||
<p className="text-neutral mb-2 text-sm">
|
||||
Updating this field will change your billing email as well
|
||||
</p>
|
||||
) : undefined}
|
||||
<TextInput
|
||||
value={user.email || ""}
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setUser({ ...user, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
@@ -191,14 +194,12 @@ export default function Account() {
|
||||
</div>
|
||||
|
||||
<div className="sm:row-span-2 sm:justify-self-center mx-auto my-3">
|
||||
<p className="text-black dark:text-white mb-2 text-center">
|
||||
Profile Photo
|
||||
</p>
|
||||
<p className="mb-2 text-center">Profile Photo</p>
|
||||
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
|
||||
<ProfilePhoto
|
||||
priority={true}
|
||||
src={user.image ? user.image : undefined}
|
||||
className="h-auto border-none w-28"
|
||||
dimensionClass="w-28 h-28"
|
||||
/>
|
||||
{user.image && (
|
||||
<div
|
||||
@@ -208,13 +209,13 @@ export default function Account() {
|
||||
image: "",
|
||||
})
|
||||
}
|
||||
className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500"
|
||||
className="absolute top-1 left-1 btn btn-xs btn-circle btn-neutral btn-outline bg-base-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
|
||||
<label className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
|
||||
<label className="btn btn-xs btn-neutral btn-outline bg-base-100">
|
||||
Browse...
|
||||
<input
|
||||
type="file"
|
||||
@@ -232,85 +233,74 @@ export default function Account() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
|
||||
<p className="truncate w-full pr-7 text-3xl font-thin">
|
||||
Import & Export
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="flex gap-3 flex-col">
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">
|
||||
Import your data from other platforms.
|
||||
</p>
|
||||
<div
|
||||
onClick={() => setImportDropdown(true)}
|
||||
className="w-fit relative"
|
||||
id="import-dropdown"
|
||||
>
|
||||
<p className="mb-2">Import your data from other platforms.</p>
|
||||
<div className="dropdown dropdown-bottom">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="flex gap-2 text-sm btn btn-outline btn-neutral btn-xs"
|
||||
id="import-dropdown"
|
||||
className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600"
|
||||
>
|
||||
Import From
|
||||
</div>
|
||||
{importDropdown ? (
|
||||
<ClickAwayHandler
|
||||
onClickOutside={(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.id !== "import-dropdown")
|
||||
setImportDropdown(false);
|
||||
}}
|
||||
className={`absolute top-7 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
|
||||
>
|
||||
<div className="cursor-pointer rounded-md">
|
||||
<label
|
||||
htmlFor="import-linkwarden-file"
|
||||
title="JSON File"
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
||||
>
|
||||
Linkwarden File...
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-linkwarden-file"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.linkwarden)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label
|
||||
htmlFor="import-html-file"
|
||||
title="HTML File"
|
||||
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
|
||||
>
|
||||
Bookmarks HTML file...
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-html-file"
|
||||
accept=".html"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.htmlFile)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</ClickAwayHandler>
|
||||
) : null}
|
||||
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
|
||||
<li>
|
||||
<label
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
htmlFor="import-linkwarden-file"
|
||||
title="JSON File"
|
||||
>
|
||||
From Linkwarden
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-linkwarden-file"
|
||||
accept=".json"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.linkwarden)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
<li>
|
||||
<label
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
htmlFor="import-html-file"
|
||||
title="HTML File"
|
||||
>
|
||||
From Bookmarks HTML file
|
||||
<input
|
||||
type="file"
|
||||
name="photo"
|
||||
id="import-html-file"
|
||||
accept=".html"
|
||||
className="hidden"
|
||||
onChange={(e) =>
|
||||
importBookmarks(e, MigrationFormat.htmlFile)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-black dark:text-white mb-2">
|
||||
Download your data instantly.
|
||||
</p>
|
||||
<p className="mb-2">Download your data instantly.</p>
|
||||
<Link className="w-fit" href="/api/v1/migration">
|
||||
<div className="border w-fit border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
|
||||
<div className="btn btn-outline btn-neutral btn-xs">
|
||||
Export Data
|
||||
</div>
|
||||
</Link>
|
||||
@@ -320,12 +310,12 @@ export default function Account() {
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
|
||||
<p className="truncate w-full pr-7 text-3xl font-thin">
|
||||
Profile Visibility
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<Checkbox
|
||||
label="Make profile private"
|
||||
@@ -333,21 +323,19 @@ export default function Account() {
|
||||
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
|
||||
/>
|
||||
|
||||
<p className="text-gray-500 dark:text-gray-300 text-sm">
|
||||
<p className="text-neutral text-sm">
|
||||
This will limit who can find and add you to new Collections.
|
||||
</p>
|
||||
|
||||
{user.isPrivate && (
|
||||
<div className="pl-5">
|
||||
<p className="text-black dark:text-white mt-2">
|
||||
Whitelisted Users
|
||||
</p>
|
||||
<p className="text-gray-500 dark:text-gray-300 text-sm mb-3">
|
||||
<p className="mt-2">Whitelisted Users</p>
|
||||
<p className="text-neutral text-sm mb-3">
|
||||
Please provide the Username of the users you wish to grant
|
||||
visibility to your profile. Separated by comma.
|
||||
</p>
|
||||
<textarea
|
||||
className="w-full resize-none border rounded-md duration-100 bg-gray-50 dark:bg-neutral-950 p-2 outline-none border-sky-100 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600"
|
||||
className="w-full resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
|
||||
placeholder="Your profile is hidden from everyone right now..."
|
||||
value={whitelistedUsersTextbox}
|
||||
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
|
||||
@@ -370,7 +358,7 @@ export default function Account() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<p>
|
||||
This will permanently delete ALL the Links, Collections, Tags, and
|
||||
@@ -381,14 +369,14 @@ export default function Account() {
|
||||
You will be prompted to enter your password before the deletion
|
||||
process.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/settings/delete"
|
||||
className="mx-auto lg:mx-0 text-white mt-3 flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
|
||||
>
|
||||
<p className="text-center w-full">Delete Your Account</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/settings/delete"
|
||||
className="mx-auto lg:mx-0 text-white flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
|
||||
>
|
||||
<p className="text-center w-full">Delete Your Account</p>
|
||||
</Link>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
||||
@@ -56,10 +56,10 @@ export default function Api() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">API Keys (Soon)</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="badge bg-orange-500 rounded-md border border-black w-fit px-2 text-black">
|
||||
<div className="badge badge-warning rounded-md w-fit p-4">
|
||||
Status: Under Development
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function Api() {
|
||||
|
||||
<p>
|
||||
For now, you can <i>temporarily</i> use your{" "}
|
||||
<code className="text-xs whitespace-nowrap bg-gray-500/40 rounded-md px-2 py-1">
|
||||
<code className="text-xs whitespace-nowrap bg-black/40 rounded-md px-2 py-1">
|
||||
next-auth.session-token
|
||||
</code>{" "}
|
||||
in your browser cookies as the API key for your integrations.
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||
import { useTheme } from "next-themes";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useState, useEffect } from "react";
|
||||
import { faClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import useAccountStore from "@/store/account";
|
||||
import { AccountSettings } from "@/types/global";
|
||||
import { toast } from "react-hot-toast";
|
||||
import TextInput from "@/components/TextInput";
|
||||
import { resizeImage } from "@/lib/client/resizeImage";
|
||||
import ProfilePhoto from "@/components/ProfilePhoto";
|
||||
import SubmitButton from "@/components/SubmitButton";
|
||||
import React from "react";
|
||||
import Checkbox from "@/components/Checkbox";
|
||||
import LinkPreview from "@/components/LinkPreview";
|
||||
import useLocalSettingsStore from "@/store/localSettings";
|
||||
|
||||
export default function Appearance() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
const { updateSettings } = useLocalSettingsStore();
|
||||
const submit = async () => {
|
||||
setSubmitLoader(true);
|
||||
|
||||
@@ -70,47 +65,47 @@ export default function Appearance() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">Appearance</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<p className="mb-3">Select Theme</p>
|
||||
<div className="flex gap-3 w-full">
|
||||
<div
|
||||
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
|
||||
theme === "dark"
|
||||
? "dark:outline-sky-500 text-sky-500"
|
||||
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-black ${
|
||||
localStorage.getItem("theme") === "dark"
|
||||
? "dark:outline-primary text-primary"
|
||||
: "text-white"
|
||||
}`}
|
||||
onClick={() => setTheme("dark")}
|
||||
onClick={() => updateSettings({ theme: "dark" })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faMoon} className="w-1/2 h-1/2" />
|
||||
<p className="text-2xl">Dark Theme</p>
|
||||
|
||||
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
|
||||
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
|
||||
</div>
|
||||
<div
|
||||
className={`w-full text-center outline-solid outline-sky-100 outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
|
||||
theme === "light"
|
||||
? "outline-sky-500 text-sky-500"
|
||||
className={`w-full text-center outline-solid outline-neutral-content outline dark:outline-neutral-700 h-40 duration-100 rounded-md flex items-center justify-center cursor-pointer select-none bg-white ${
|
||||
localStorage.getItem("theme") === "light"
|
||||
? "outline-primary text-primary"
|
||||
: "text-black"
|
||||
}`}
|
||||
onClick={() => setTheme("light")}
|
||||
onClick={() => updateSettings({ theme: "light" })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSun} className="w-1/2 h-1/2" />
|
||||
<p className="text-2xl">Light Theme</p>
|
||||
{/* <hr className="my-3 outline-1 outline-sky-100 dark:outline-neutral-700" /> */}
|
||||
{/* <hr className="my-3 outline-1 outline-neutral-content dark:outline-neutral-700" /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
|
||||
Link Card
|
||||
</p>
|
||||
<p className="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" />
|
||||
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<Checkbox
|
||||
label="Display Icons"
|
||||
state={user.displayLinkIcons}
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function Archive() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">Archive Settings</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<p>Formats to Archive webpages:</p>
|
||||
<div className="p-3">
|
||||
|
||||
@@ -13,10 +13,10 @@ export default function Billing() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">Billing Settings</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<div className="w-full mx-auto flex flex-col gap-3 justify-between">
|
||||
<p className="text-md text-black dark:text-white">
|
||||
<p className="text-md">
|
||||
To manage/cancel your subscription, visit the{" "}
|
||||
<a
|
||||
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
|
||||
@@ -27,7 +27,7 @@ export default function Billing() {
|
||||
.
|
||||
</p>
|
||||
|
||||
<p className="text-md text-black dark:text-white">
|
||||
<p className="text-md">
|
||||
If you still need help or encountered any issues, feel free to reach
|
||||
out to us at:{" "}
|
||||
<a
|
||||
|
||||
+15
-13
@@ -7,7 +7,7 @@ import Link from "next/link";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export default function Password() {
|
||||
export default function Delete() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [comment, setComment] = useState<string>();
|
||||
const [feedback, setFeedback] = useState<string>();
|
||||
@@ -54,12 +54,15 @@ export default function Password() {
|
||||
|
||||
return (
|
||||
<CenteredForm>
|
||||
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 dark:border-neutral-700 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<Link
|
||||
href="/settings/account"
|
||||
className="absolute top-4 left-4 gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
|
||||
className="absolute top-4 left-4 btn btn-ghost btn-square btn-sm"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronLeft}
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center gap-2 w-full rounded-md h-8">
|
||||
<p className="text-red-500 dark:text-red-500 truncate w-full text-3xl text-center">
|
||||
@@ -67,7 +70,7 @@ export default function Password() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<p>
|
||||
This will permanently delete all the Links, Collections, Tags, and
|
||||
@@ -80,22 +83,21 @@ export default function Password() {
|
||||
|
||||
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? (
|
||||
<div>
|
||||
<p className="mb-2 text-black dark:text-white">
|
||||
Confirm Your Password
|
||||
</p>
|
||||
<p className="mb-2">Confirm Your Password</p>
|
||||
|
||||
<TextInput
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••••••••"
|
||||
className="bg-base-100"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
|
||||
{process.env.NEXT_PUBLIC_STRIPE ? (
|
||||
<fieldset className="border rounded-md p-2 border-sky-500">
|
||||
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-sky-500">
|
||||
<fieldset className="border rounded-md p-2 border-primary">
|
||||
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
|
||||
<b>Optional</b>{" "}
|
||||
<i className="min-[390px]:text-sm text-xs">
|
||||
(but it really helps us improve!)
|
||||
@@ -104,7 +106,7 @@ export default function Password() {
|
||||
<label className="w-full flex min-[430px]:items-center items-start gap-2 mb-3 min-[430px]:flex-row flex-col">
|
||||
<p className="text-sm">Reason for cancellation:</p>
|
||||
<select
|
||||
className="rounded-md p-1 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
|
||||
className="rounded-md p-1 outline-none"
|
||||
value={feedback}
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
>
|
||||
@@ -120,7 +122,7 @@ export default function Password() {
|
||||
</select>
|
||||
</label>
|
||||
<div>
|
||||
<p className="text-sm mb-2 text-black dark:text-white">
|
||||
<p className="text-sm mb-2">
|
||||
More information (the more details, the more helpful it'd
|
||||
be)
|
||||
</p>
|
||||
@@ -129,7 +131,7 @@ export default function Password() {
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
placeholder="e.g. I needed a feature that..."
|
||||
className="resize-none w-full rounded-md p-2 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
|
||||
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-100 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -47,26 +47,28 @@ export default function Password() {
|
||||
<SettingsLayout>
|
||||
<p className="capitalize text-3xl font-thin inline">Change Password</p>
|
||||
|
||||
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-3"></div>
|
||||
|
||||
<p className="mb-3">
|
||||
To change your password, please fill out the following. Your password
|
||||
should be at least 8 characters.
|
||||
</p>
|
||||
<div className="w-full flex flex-col gap-2 justify-between">
|
||||
<p className="text-black dark:text-white">New Password</p>
|
||||
<p>New Password</p>
|
||||
|
||||
<TextInput
|
||||
value={newPassword}
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setNewPassword1(e.target.value)}
|
||||
placeholder="••••••••••••••"
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<p className="text-black dark:text-white">Confirm New Password</p>
|
||||
<p>Confirm New Password</p>
|
||||
|
||||
<TextInput
|
||||
value={newPassword2}
|
||||
className="bg-base-200"
|
||||
onChange={(e) => setNewPassword2(e.target.value)}
|
||||
placeholder="••••••••••••••"
|
||||
type="password"
|
||||
|
||||
+9
-11
@@ -30,12 +30,12 @@ export default function Subscribe() {
|
||||
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
|
||||
}-day free trial, cancel anytime!`}
|
||||
>
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between dark:border-neutral-700 max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
|
||||
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
|
||||
<p className="sm:text-3xl text-2xl text-center font-extralight">
|
||||
Subscribe to Linkwarden!
|
||||
</p>
|
||||
|
||||
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
|
||||
<div className="divider my-0"></div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
@@ -47,10 +47,10 @@ export default function Subscribe() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex text-white dark:text-black gap-3 border border-solid border-sky-100 dark:border-neutral-700 w-4/5 mx-auto p-1 rounded-xl relative">
|
||||
<div className="flex text-white dark:text-black gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
|
||||
<button
|
||||
onClick={() => setPlan(Plan.monthly)}
|
||||
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
|
||||
className={`w-full duration-100 text-sm rounded-lg p-1 ${
|
||||
plan === Plan.monthly
|
||||
? "text-white bg-sky-700 dark:bg-sky-700"
|
||||
: "hover:opacity-80"
|
||||
@@ -61,7 +61,7 @@ export default function Subscribe() {
|
||||
|
||||
<button
|
||||
onClick={() => setPlan(Plan.yearly)}
|
||||
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
|
||||
className={`w-full duration-100 text-sm rounded-lg p-1 ${
|
||||
plan === Plan.yearly
|
||||
? "text-white bg-sky-700 dark:bg-sky-700"
|
||||
: "hover:opacity-80"
|
||||
@@ -77,15 +77,13 @@ export default function Subscribe() {
|
||||
<div className="flex flex-col gap-2 justify-center items-center">
|
||||
<p className="text-3xl">
|
||||
${plan === Plan.monthly ? "4" : "3"}
|
||||
<span className="text-base text-gray-500 dark:text-gray-400">
|
||||
/mo
|
||||
</span>
|
||||
<span className="text-base text-neutral">/mo</span>
|
||||
</p>
|
||||
<p className="font-semibold">
|
||||
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
|
||||
</p>
|
||||
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700">
|
||||
<legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl">
|
||||
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-neutral-content">
|
||||
<legend className="w-fit font-extralight px-2 border border-neutral rounded-md text-xl">
|
||||
Total
|
||||
</legend>
|
||||
|
||||
@@ -108,7 +106,7 @@ export default function Subscribe() {
|
||||
|
||||
<div
|
||||
onClick={() => signOut()}
|
||||
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
|
||||
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
|
||||
>
|
||||
Sign Out
|
||||
</div>
|
||||
|
||||
+46
-38
@@ -4,7 +4,6 @@ import {
|
||||
faCheck,
|
||||
faEllipsis,
|
||||
faHashtag,
|
||||
faSort,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
@@ -24,7 +23,6 @@ export default function Index() {
|
||||
const { links } = useLinkStore();
|
||||
const { tags, updateTag, removeTag } = useTagStore();
|
||||
|
||||
const [sortDropdown, setSortDropdown] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
|
||||
|
||||
const [expandDropdown, setExpandDropdown] = useState(false);
|
||||
@@ -107,7 +105,7 @@ export default function Index() {
|
||||
<div className="flex gap-2 items-end font-thin">
|
||||
<FontAwesomeIcon
|
||||
icon={faHashtag}
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
|
||||
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary"
|
||||
/>
|
||||
{renameTag ? (
|
||||
<>
|
||||
@@ -115,50 +113,78 @@ export default function Index() {
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
className="sm:text-4xl text-3xl capitalize text-black dark:text-white bg-transparent h-10 w-3/4 outline-none border-b border-b-sky-100 dark:border-b-neutral-700"
|
||||
className="sm:text-4xl text-3xl capitalize bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<div
|
||||
onClick={() => submit()}
|
||||
id="expand-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
className="btn btn-ghost btn-square btn-sm"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
id="expand-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => cancelUpdateTag()}
|
||||
id="expand-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
className="btn btn-ghost btn-square btn-sm"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
id="expand-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
className="w-5 h-5 text-neutral"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
|
||||
<p className="sm:text-4xl text-3xl capitalize">
|
||||
{activeTag?.name}
|
||||
</p>
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setExpandDropdown(!expandDropdown)}
|
||||
id="expand-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
id="expand-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
<div className="dropdown dropdown-bottom font-normal">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-ghost btn-sm btn-square text-neutral"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faEllipsis}
|
||||
title="More"
|
||||
className="w-5 h-5"
|
||||
/>
|
||||
</div>
|
||||
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-36 mt-1">
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
setRenameTag(true);
|
||||
}}
|
||||
>
|
||||
Rename Tag
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
(document?.activeElement as HTMLElement)?.blur();
|
||||
remove();
|
||||
}}
|
||||
>
|
||||
Remove Tag
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{expandDropdown ? (
|
||||
@@ -194,25 +220,7 @@ export default function Index() {
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
onClick={() => setSortDropdown(!sortDropdown)}
|
||||
id="sort-dropdown"
|
||||
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faSort}
|
||||
id="sort-dropdown"
|
||||
className="w-5 h-5 text-gray-500 dark:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sortDropdown ? (
|
||||
<SortDropdown
|
||||
sortBy={sortBy}
|
||||
setSort={setSortBy}
|
||||
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
|
||||
/>
|
||||
) : null}
|
||||
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
|
||||
|
||||
+27
-4
@@ -1,23 +1,46 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
type LocalSettings = {
|
||||
darkMode: boolean;
|
||||
theme: string;
|
||||
};
|
||||
|
||||
type LocalSettingsStore = {
|
||||
settings: LocalSettings;
|
||||
updateSettings: (settings: LocalSettings) => void;
|
||||
setSettings: () => void;
|
||||
};
|
||||
|
||||
const useLocalSettingsStore = create<LocalSettingsStore>((set) => ({
|
||||
settings: {
|
||||
darkMode: false,
|
||||
theme: "",
|
||||
},
|
||||
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 } }));
|
||||
},
|
||||
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;
|
||||
|
||||
// TODO: Add Dark mode.
|
||||
|
||||
+39
-34
@@ -2,6 +2,31 @@
|
||||
@tailwind components;
|
||||
@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,
|
||||
body {
|
||||
scroll-behavior: smooth;
|
||||
@@ -16,11 +41,6 @@ body {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #0ea4e93c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hyphens {
|
||||
hyphens: auto;
|
||||
}
|
||||
@@ -49,7 +69,7 @@ body {
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slide-up-animation 70ms;
|
||||
animation: slide-up-animation 200ms;
|
||||
}
|
||||
|
||||
.slide-down {
|
||||
@@ -69,7 +89,7 @@ body {
|
||||
|
||||
@keyframes slide-up-animation {
|
||||
0% {
|
||||
transform: translateY(15%);
|
||||
transform: translateY(5%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
@@ -140,41 +160,25 @@ body {
|
||||
}
|
||||
|
||||
/* Theme */
|
||||
@layer base {
|
||||
body {
|
||||
@apply dark:bg-neutral-900 bg-white text-black dark:text-white;
|
||||
}
|
||||
}
|
||||
|
||||
/* react-select */
|
||||
@layer components {
|
||||
.react-select-container .react-select__control {
|
||||
@apply dark:bg-neutral-950 bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-500;
|
||||
}
|
||||
|
||||
.react-select-container {
|
||||
@apply dark:border-neutral-700;
|
||||
@apply bg-base-200 hover:border-neutral-content;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__menu {
|
||||
@apply dark:bg-neutral-900 dark:border-neutral-700 border;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__option {
|
||||
@apply dark:hover:bg-neutral-800;
|
||||
@apply bg-base-100 border-neutral-content border rounded-md;
|
||||
}
|
||||
/*
|
||||
.react-select-container .react-select__menu-list {
|
||||
@apply h-20;
|
||||
} */
|
||||
|
||||
.react-select-container .react-select__input-container,
|
||||
.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 {
|
||||
box-shadow: inset 0px -10px 10px #0071b7;
|
||||
@@ -245,13 +249,14 @@ body {
|
||||
.reader-view code {
|
||||
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);
|
||||
border-radius: 8px;
|
||||
}
|
||||
[class="light"] .reader-view code,
|
||||
[class="light"] .reader-view pre {
|
||||
[data-theme="light"] .reader-view code,
|
||||
[data-theme="light"] .reader-view pre {
|
||||
background-color: rgb(230, 230, 230);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
+42
-5
@@ -1,10 +1,44 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const plugin = require("tailwindcss/plugin");
|
||||
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
// daisyui: {
|
||||
// themes: ["light", "dark"],
|
||||
// },
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
light: {
|
||||
primary: "#0284c7",
|
||||
secondary: "#0891b2",
|
||||
accent: "#6366f1",
|
||||
neutral: "#6b7280",
|
||||
"neutral-content": "#d1d5db",
|
||||
"base-100": "#ffffff",
|
||||
"base-200": "#f3f4f6",
|
||||
"base-content": "#0a0a0a",
|
||||
info: "#a5f3fc",
|
||||
success: "#22c55e",
|
||||
warning: "#facc15",
|
||||
error: "#dc2626",
|
||||
},
|
||||
},
|
||||
{
|
||||
dark: {
|
||||
primary: "#38bdf8",
|
||||
secondary: "#22d3ee",
|
||||
accent: "#6366f1",
|
||||
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: [
|
||||
"./app/**/*.{js,ts,jsx,tsx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
@@ -14,6 +48,9 @@ module.exports = {
|
||||
"./layouts/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
plugins: [
|
||||
// require("daisyui")
|
||||
require("daisyui"),
|
||||
plugin(({ addVariant }) => {
|
||||
addVariant("dark", '&[data-theme="dark"]');
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -3825,11 +3825,6 @@ next-auth@^4.22.1:
|
||||
preact-render-to-string "^5.1.19"
|
||||
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:
|
||||
version "13.4.12"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.4.12.tgz#809b21ea0aabbe88ced53252c88c4a5bd5af95df"
|
||||
|
||||
Reference in New Issue
Block a user