Merge pull request #325 from linkwarden/dev

Dev
This commit is contained in:
Daniel
2023-12-02 21:58:44 +03:30
committed by GitHub
76 changed files with 3111 additions and 1886 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+4 -11
View File
@@ -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>
);
+3 -3
View File
@@ -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>
);
+115 -57
View File
@@ -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>
);
}
+10 -4
View File
@@ -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}
+1 -1
View File
@@ -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}
+14 -6
View File
@@ -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
View File
@@ -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>
);
}
+7 -9
View File
@@ -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>
+7 -16
View File
@@ -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>
+31
View File
@@ -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 &quot;
<span className="font-bold">{collection.name}</span>
&quot; 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
+21 -31
View File
@@ -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>
+7 -15
View File
@@ -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 -2
View File
@@ -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 && (
+12 -10
View File
@@ -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>
+13 -13
View File
@@ -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" &&
-34
View File
@@ -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 &quot;
<span className="font-bold">{collection.name}</span>
&quot; 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>
);
}
+164
View File
@@ -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>
);
}
+204
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+4 -4
View File
@@ -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
-59
View File
@@ -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>
);
}
+3 -5
View File
@@ -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>
);
}
-11
View File
@@ -1,11 +0,0 @@
export default function RequiredBadge() {
return (
<span
title="Required Field"
className="text-black dark:text-white cursor-help"
>
{" "}
*
</span>
);
}
+35 -13
View File
@@ -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>
);
+45 -72
View File
@@ -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
View File
@@ -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
View File
@@ -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>
);
}
+4 -6
View File
@@ -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>
);
}
+1 -1
View File
@@ -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 || ""
}`}
/>
+46 -14
View File
@@ -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>
);
}
+4 -1
View File
@@ -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]);
+9 -24
View File
@@ -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
+6 -6
View File
@@ -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}
+3 -3
View File
@@ -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 -1
View File
@@ -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;
}
-1
View File
@@ -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
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React from "react";
import "@/styles/globals.css";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
@@ -6,7 +6,6 @@ import Head from "next/head";
import AuthRedirect from "@/layouts/AuthRedirect";
import { Toaster } from "react-hot-toast";
import { Session } from "next-auth";
import { ThemeProvider } from "next-themes";
export default function App({
Component,
@@ -14,13 +13,6 @@ export default function App({
}: AppProps<{
session: Session;
}>) {
const defaultTheme: "light" | "dark" = "dark";
useEffect(() => {
if (!localStorage.getItem("theme"))
localStorage.setItem("theme", defaultTheme);
}, []);
return (
<SessionProvider
session={pageProps.session}
@@ -50,17 +42,15 @@ export default function App({
<link rel="manifest" href="/site.webmanifest" />
</Head>
<AuthRedirect>
<ThemeProvider attribute="class">
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{
className:
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
}}
/>
<Component {...pageProps} />
</ThemeProvider>
<Toaster
position="top-center"
reverseOrder={false}
toastOptions={{
className:
"border border-sky-100 dark:border-neutral-700 dark:bg-neutral-900 dark:text-white",
}}
/>
<Component {...pageProps} />
</AuthRedirect>
</SessionProvider>
);
+7 -9
View File
@@ -41,28 +41,26 @@ export default function ChooseUsername() {
return (
<CenteredForm>
<form onSubmit={submitUsername}>
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<p className="text-3xl text-center text-black dark:text-white font-extralight">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Choose a Username
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username
</p>
<p className="text-sm w-fit font-semibold mb-1">Username</p>
<TextInput
autoFocus
placeholder="john"
value={inputedUsername}
className="bg-white"
className="bg-base-100"
onChange={(e) => setInputedUsername(e.target.value)}
/>
</div>
<div>
<p className="text-md text-gray-500 dark:text-gray-400 mt-1">
<p className="text-md text-neutral mt-1">
Feel free to reach out to us at{" "}
<a
className="font-semibold underline"
@@ -83,7 +81,7 @@ export default function ChooseUsername() {
<div
onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
>
Sign Out
</div>
+184 -172
View File
@@ -1,37 +1,32 @@
import Dropdown from "@/components/Dropdown";
import LinkCard from "@/components/LinkCard";
import useCollectionStore from "@/store/collections";
import useLinkStore from "@/store/links";
import { CollectionIncludingMembersAndLinkCount, Sort } from "@/types/global";
import {
faEllipsis,
faFolder,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { faEllipsis, faFolder } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import ProfilePhoto from "@/components/ProfilePhoto";
import SortDropdown from "@/components/SortDropdown";
import useModalStore from "@/store/modals";
import useLinks from "@/hooks/useLinks";
import usePermissions from "@/hooks/usePermissions";
import NoLinksFound from "@/components/NoLinksFound";
import { useTheme } from "next-themes";
import useLocalSettingsStore from "@/store/localSettings";
import useAccountStore from "@/store/account";
import getPublicUserData from "@/lib/client/getPublicUserData";
import EditCollectionModal from "@/components/ModalContent/EditCollectionModal";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
import DeleteCollectionModal from "@/components/ModalContent/DeleteCollectionModal";
export default function Index() {
const { setModal } = useModalStore();
const { settings } = useLocalSettingsStore();
const router = useRouter();
const { theme } = useTheme();
const { links } = useLinkStore();
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [activeCollection, setActiveCollection] =
@@ -47,185 +42,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
View File
@@ -3,32 +3,29 @@ import {
faEllipsis,
faFolder,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import CollectionCard from "@/components/CollectionCard";
import Dropdown from "@/components/Dropdown";
import { useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import { useSession } from "next-auth/react";
import useModalStore from "@/store/modals";
import SortDropdown from "@/components/SortDropdown";
import { Sort } from "@/types/global";
import useSort from "@/hooks/useSort";
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
export default function Collections() {
const { collections } = useCollectionStore();
const [expandDropdown, setExpandDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [sortedCollections, setSortedCollections] = useState(collections);
const { data } = useSession();
const { setModal } = useModalStore();
useSort({ sortBy, setData: setSortedCollections, data: collections });
const [newCollectionModal, setNewCollectionModal] = useState(false);
return (
<MainLayout>
<div className="p-5">
@@ -37,77 +34,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&apos;re a member of
</p>
<p>Shared collections you&apos;re a member of</p>
</div>
</div>
@@ -166,6 +125,9 @@ export default function Collections() {
</>
) : undefined}
</div>
{newCollectionModal ? (
<NewCollectionModal onClose={() => setNewCollectionModal(false)} />
) : undefined}
</MainLayout>
);
}
+2 -2
View File
@@ -5,12 +5,12 @@ import React from "react";
export default function EmailConfirmaion() {
return (
<CenteredForm>
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-sky-100 dark:border-neutral-700 bg-slate-50 text-black dark:text-white dark:bg-neutral-800">
<div className="p-4 max-w-[30rem] min-w-80 w-full rounded-2xl shadow-md mx-auto border border-neutral-content bg-base-200">
<p className="text-center text-2xl sm:text-3xl font-extralight mb-2 ">
Please check your Email
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700 my-3" />
<div className="divider my-3"></div>
<p>A sign in link has been sent to your email address.</p>
+79 -107
View File
@@ -23,8 +23,8 @@ import React from "react";
import useModalStore from "@/store/modals";
import { toast } from "react-hot-toast";
import { MigrationFormat, MigrationRequest } from "@/types/global";
import ClickAwayHandler from "@/components/ClickAwayHandler";
import DashboardItem from "@/components/DashboardItem";
import NewLinkModal from "@/components/ModalContent/NewLinkModal";
export default function Dashboard() {
const { collections } = useCollectionStore();
@@ -63,8 +63,6 @@ export default function Dashboard() {
handleNumberOfLinksToShow();
}, [width]);
const [importDropdown, setImportDropdown] = useState(false);
const importBookmarks = async (e: any, format: MigrationFormat) => {
const file: File = e.target.files[0];
@@ -92,8 +90,6 @@ export default function Dashboard() {
toast.success("Imported the Bookmarks! Reloading the page...");
setImportDropdown(false);
setTimeout(() => {
location.reload();
}, 2000);
@@ -104,35 +100,32 @@ export default function Dashboard() {
}
};
const [newLinkModal, setNewLinkModal] = useState(false);
return (
<MainLayout>
<div style={{ flex: "1 1 auto" }} className="p-5 flex flex-col gap-5">
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faChartSimple}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
Dashboard
</p>
<p className="text-3xl capitalize font-thin">Dashboard</p>
<p className="text-black dark:text-white">
A brief overview of your data
</p>
<p>A brief overview of your data</p>
</div>
</div>
<div>
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-sky-100 dark:border-neutral-700 bg-gray-100 dark:bg-neutral-800">
<div className="flex justify-evenly flex-col md:flex-row md:items-center gap-2 md:w-full h-full rounded-2xl p-8 border border-neutral-content bg-base-200">
<DashboardItem
name={numberOfLinks === 1 ? "Link" : "Links"}
value={numberOfLinks}
icon={faLink}
/>
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
<div className="h-24 border-1 border-l border-sky-100 dark:border-neutral-700 hidden md:block"></div>
<div className="divider md:divider-horizontal"></div>
<DashboardItem
name={collections.length === 1 ? "Collection" : "Collections"}
@@ -140,8 +133,7 @@ export default function Dashboard() {
icon={faFolder}
/>
<hr className="border-sky-100 dark:border-neutral-700 md:hidden my-5" />
<div className="h-24 border-1 border-r border-sky-100 dark:border-neutral-700 hidden md:block"></div>
<div className="divider md:divider-horizontal"></div>
<DashboardItem
name={tags.length === 1 ? "Tag" : "Tags"}
@@ -155,21 +147,16 @@ export default function Dashboard() {
<div className="flex gap-2 items-center">
<FontAwesomeIcon
icon={faClockRotateLeft}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
className="w-5 h-5 text-primary drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">
Recently Added Links
</p>
<p className="text-2xl">Recently Added Links</p>
</div>
<Link
href="/links"
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
className="flex items-center gap-2 cursor-pointer"
>
View All
<FontAwesomeIcon
icon={faChevronRight}
className={`w-4 h-4 text-black dark:text-white`}
/>
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
</Link>
</div>
@@ -180,7 +167,7 @@ export default function Dashboard() {
{links[0] ? (
<div className="w-full">
<div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links.slice(0, showLinks).map((e, i) => (
<LinkCard key={i} link={e} count={i} />
@@ -190,101 +177,87 @@ export default function Dashboard() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
View Your Recently Added Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
This section will view your latest added Links across every
Collections you have access to.
</p>
<div className="text-center text-black dark:text-white w-full mt-4 flex flex-wrap gap-4 justify-center">
<div className="text-center w-full mt-4 flex flex-wrap gap-4 justify-center">
<div
onClick={() => {
setModal({
modal: "LINK",
state: true,
method: "CREATE",
});
setNewLinkModal(true);
}}
className="inline-flex gap-1 relative w-[11.4rem] items-center font-semibold select-none cursor-pointer p-2 px-3 rounded-md dark:hover:bg-sky-600 text-white bg-sky-700 hover:bg-sky-600 duration-100 group"
className="inline-flex gap-1 relative w-[11rem] items-center btn btn-accent text-white group"
>
<FontAwesomeIcon
icon={faPlus}
className="w-5 h-5 group-hover:ml-[4.325rem] absolute duration-100"
className="w-5 h-5 left-4 group-hover:ml-[4rem] absolute duration-100"
/>
<span className="group-hover:opacity-0 text-right w-full duration-100">
Create New Link
</span>
</div>
<div className="relative">
<div className="dropdown dropdown-bottom">
<div
onClick={() => setImportDropdown(!importDropdown)}
tabIndex={0}
role="button"
className="flex gap-2 text-sm btn btn-outline btn-neutral group"
id="import-dropdown"
className="flex gap-2 select-none text-sm cursor-pointer p-2 px-3 rounded-md border dark:hover:border-sky-600 text-black border-black dark:text-white dark:border-white hover:border-sky-500 hover:dark:border-sky-500 hover:text-sky-500 hover:dark:text-sky-500 duration-100 group"
>
<FontAwesomeIcon
icon={faFileImport}
className="w-5 h-5 duration-100"
id="import-dropdown"
/>
<span
className="text-right w-full duration-100"
id="import-dropdown"
>
Import Your Bookmarks
</span>
<p>Import From</p>
</div>
{importDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown")
setImportDropdown(false);
}}
className={`absolute text-black dark:text-white top-10 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-linkwarden-file"
title="JSON File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Linkwarden File...
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
<label
htmlFor="import-html-file"
title="HTML File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</div>
</ClickAwayHandler>
) : null}
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-linkwarden-file"
title="JSON File"
>
From Linkwarden
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
</div>
@@ -295,19 +268,16 @@ export default function Dashboard() {
<div className="flex gap-2 items-center">
<FontAwesomeIcon
icon={faThumbTack}
className="w-5 h-5 text-sky-500 dark:text-sky-500 drop-shadow"
className="w-5 h-5 text-primary drop-shadow"
/>
<p className="text-2xl text-black dark:text-white">Pinned Links</p>
<p className="text-2xl">Pinned Links</p>
</div>
<Link
href="/links/pinned"
className="text-black dark:text-white flex items-center gap-2 cursor-pointer"
className="flex items-center gap-2 cursor-pointer"
>
View All
<FontAwesomeIcon
icon={faChevronRight}
className={`w-4 h-4 text-black dark:text-white`}
/>
<FontAwesomeIcon icon={faChevronRight} className={`w-4 h-4`} />
</Link>
</div>
@@ -318,10 +288,9 @@ export default function Dashboard() {
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
<div className="w-full">
<div
className={`grid overflow-hidden 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
className={`grid 2xl:grid-cols-3 xl:grid-cols-2 grid-cols-1 gap-5 w-full`}
>
{links
.filter((e) => e.pinnedBy && e.pinnedBy[0])
.map((e, i) => <LinkCard key={i} link={e} count={i} />)
.slice(0, showLinks)}
@@ -330,12 +299,12 @@ export default function Dashboard() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
Pin Your Favorite Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
You can Pin your favorite Links by clicking on the three dots on
each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
@@ -344,6 +313,9 @@ export default function Dashboard() {
)}
</div>
</div>
{newLinkModal ? (
<NewLinkModal onClose={() => setNewLinkModal(false)} />
) : undefined}
</MainLayout>
);
}
+8 -13
View File
@@ -43,34 +43,32 @@ export default function Forgot() {
return (
<CenteredForm>
<form onSubmit={sendConfirmation}>
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:border-neutral-700 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<p className="text-3xl text-center text-black dark:text-white font-extralight">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Password Recovery
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-black dark:text-white">
<p>
Enter your email so we can send you a link to recover your
account. Make sure to change your password in the profile settings
afterwards.
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
<p className="text-sm text-neutral">
You wont get logged in if you haven&apos;t created an account yet.
</p>
</div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email
</p>
<p className="text-sm w-fit font-semibold mb-1">Email</p>
<TextInput
autoFocus
type="email"
placeholder="johnny@example.com"
value={form.email}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
</div>
@@ -82,10 +80,7 @@ export default function Forgot() {
loading={submitLoader}
/>
<div className="flex items-baseline gap-1 justify-center">
<Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
<Link href={"/login"} className="block font-bold">
Go back
</Link>
</div>
+13 -12
View File
@@ -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
View File
@@ -5,14 +5,13 @@ import useLinks from "@/hooks/useLinks";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import { Sort } from "@/types/global";
import { faLink, faSort } from "@fortawesome/free-solid-svg-icons";
import { faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
export default function Links() {
const { links } = useLinkStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ sort: sortBy });
@@ -24,39 +23,17 @@ export default function Links() {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faLink}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
All Links
</p>
<p className="text-3xl capitalize font-thin">All Links</p>
<p className="text-black dark:text-white">
Links from every Collections
</p>
<p>Links from every Collections</p>
</div>
</div>
<div className="relative mt-2">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{sortDropdown ? (
<SortDropdown
sortBy={sortBy}
setSort={setSortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
{links[0] ? (
+8 -32
View File
@@ -1,18 +1,16 @@
import LinkCard from "@/components/LinkCard";
import NoLinksFound from "@/components/NoLinksFound";
import SortDropdown from "@/components/SortDropdown";
import useLinks from "@/hooks/useLinks";
import MainLayout from "@/layouts/MainLayout";
import useLinkStore from "@/store/links";
import { Sort } from "@/types/global";
import { faSort, faThumbTack } from "@fortawesome/free-solid-svg-icons";
import { faThumbTack } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
export default function PinnedLinks() {
const { links } = useLinkStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({ sort: sortBy, pinnedOnly: true });
@@ -24,39 +22,17 @@ export default function PinnedLinks() {
<div className="flex items-center gap-3">
<FontAwesomeIcon
icon={faThumbTack}
className="sm:w-10 sm:h-10 w-6 h-6 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-10 sm:h-10 w-6 h-6 text-primary drop-shadow"
/>
<div>
<p className="text-3xl capitalize text-black dark:text-white font-thin">
Pinned Links
</p>
<p className="text-3xl capitalize font-thin">Pinned Links</p>
<p className="text-black dark:text-white">
Pinned Links from your Collections
</p>
<p>Pinned Links from your Collections</p>
</div>
</div>
<div className="relative mt-2">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{sortDropdown ? (
<SortDropdown
sortBy={sortBy}
setSort={setSortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
@@ -68,12 +44,12 @@ export default function PinnedLinks() {
) : (
<div
style={{ flex: "1 1 auto" }}
className="sky-shadow flex flex-col justify-center h-full border border-solid border-sky-100 dark:border-neutral-700 w-full mx-auto p-10 rounded-2xl bg-gray-50 dark:bg-neutral-800"
className="sky-shadow flex flex-col justify-center h-full border border-solid border-neutral-content w-full mx-auto p-10 rounded-2xl bg-base-200"
>
<p className="text-center text-2xl text-black dark:text-white">
<p className="text-center text-2xl">
Pin Your Favorite Links Here!
</p>
<p className="text-center mx-auto max-w-96 w-fit text-gray-500 dark:text-gray-300 text-sm mt-2">
<p className="text-center mx-auto max-w-96 w-fit text-neutral text-sm mt-2">
You can Pin your favorite Links by clicking on the three dots on
each Link and clicking{" "}
<span className="font-semibold">Pin to Dashboard</span>.
+10 -20
View File
@@ -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>
+62 -104
View File
@@ -9,18 +9,16 @@ import Head from "next/head";
import useLinks from "@/hooks/useLinks";
import useLinkStore from "@/store/links";
import ProfilePhoto from "@/components/ProfilePhoto";
import useModalStore from "@/store/modals";
import ModalManagement from "@/components/ModalManagement";
import ToggleDarkMode from "@/components/ToggleDarkMode";
import { useTheme } from "next-themes";
import getPublicUserData from "@/lib/client/getPublicUserData";
import Image from "next/image";
import Link from "next/link";
import PublicSearchBar from "@/components/PublicPage/PublicSearchBar";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFilter, faSort } from "@fortawesome/free-solid-svg-icons";
import FilterSearchDropdown from "@/components/FilterSearchDropdown";
import SortDropdown from "@/components/SortDropdown";
import useLocalSettingsStore from "@/store/localSettings";
import SearchBar from "@/components/SearchBar";
import EditCollectionSharingModal from "@/components/ModalContent/EditCollectionSharingModal";
const cardVariants: Variants = {
offscreen: {
@@ -38,15 +36,8 @@ const cardVariants: Variants = {
export default function PublicCollections() {
const { links } = useLinkStore();
const { modal, setModal } = useModalStore();
useEffect(() => {
modal
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "auto");
}, [modal]);
const { theme } = useTheme();
const { settings } = useLocalSettingsStore();
const router = useRouter();
@@ -65,8 +56,6 @@ export default function PublicCollections() {
tags: true,
});
const [filterDropdown, setFilterDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({
@@ -101,13 +90,16 @@ export default function PublicCollections() {
fetchOwner();
}, [collection]);
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
return collection ? (
<div
className="h-screen"
style={{
backgroundImage: `linear-gradient(${collection?.color}30 10%, ${
theme === "dark" ? "#262626" : "#f3f4f6"
} 50%, ${theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
settings.theme === "dark" ? "#262626" : "#f3f4f6"
} 18rem, ${settings.theme === "dark" ? "#171717" : "#ffffff"} 100%)`,
}}
>
<ModalManagement />
@@ -128,63 +120,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
View File
@@ -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
View File
@@ -104,7 +104,7 @@ export default function Register() {
}
>
{process.env.NEXT_PUBLIC_DISABLE_REGISTRATION === "true" ? (
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p>
Registration is disabled for this instance, please contact the admin
in case of any issues.
@@ -112,37 +112,33 @@ export default function Register() {
</div>
) : (
<form onSubmit={registerUser}>
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100 dark:border-neutral-700">
<p className="text-3xl text-black dark:text-white text-center font-extralight">
<div className="p-4 flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full mx-auto bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="text-3xl text-center font-extralight">
Enter your details
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Display Name
</p>
<p className="text-sm w-fit font-semibold mb-1">Display Name</p>
<TextInput
autoFocus={true}
placeholder="Johnny"
value={form.name}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
</div>
{emailEnabled ? undefined : (
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Username
</p>
<p className="text-sm w-fit font-semibold mb-1">Username</p>
<TextInput
placeholder="john"
value={form.username}
className="bg-white"
className="bg-base-100"
onChange={(e) =>
setForm({ ...form, username: e.target.value })
}
@@ -152,36 +148,32 @@ export default function Register() {
{emailEnabled ? (
<div>
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Email
</p>
<p className="text-sm w-fit font-semibold mb-1">Email</p>
<TextInput
type="email"
placeholder="johnny@example.com"
value={form.email}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, email: e.target.value })}
/>
</div>
) : undefined}
<div className="w-full">
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
Password
</p>
<p className="text-sm w-fit font-semibold mb-1">Password</p>
<TextInput
type="password"
placeholder="••••••••••••••"
value={form.password}
className="bg-white"
className="bg-base-100"
onChange={(e) => setForm({ ...form, password: e.target.value })}
/>
</div>
<div className="w-full">
<p className="text-sm text-black dark:text-white w-fit font-semibold mb-1">
<p className="text-sm w-fit font-semibold mb-1">
Confirm Password
</p>
@@ -189,7 +181,7 @@ export default function Register() {
type="password"
placeholder="••••••••••••••"
value={form.passwordConfirmation}
className="bg-white"
className="bg-base-100"
onChange={(e) =>
setForm({ ...form, passwordConfirmation: e.target.value })
}
@@ -198,7 +190,7 @@ export default function Register() {
{process.env.NEXT_PUBLIC_STRIPE ? (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-neutral">
By signing up, you agree to our{" "}
<Link
href="https://linkwarden.app/tos"
@@ -215,7 +207,7 @@ export default function Register() {
</Link>
.
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
<p className="text-xs text-neutral">
Need help?{" "}
<Link
href="mailto:support@linkwarden.app"
@@ -235,13 +227,8 @@ export default function Register() {
<p className="text-center w-full font-bold">Sign Up</p>
</button>
<div className="flex items-baseline gap-1 justify-center">
<p className="w-fit text-gray-500 dark:text-gray-400">
Already have an account?
</p>
<Link
href={"/login"}
className="block text-black dark:text-white font-bold"
>
<p className="w-fit text-neutral">Already have an account?</p>
<Link href={"/login"} className="block font-bold">
Login
</Link>
</div>
+9 -43
View File
@@ -24,7 +24,6 @@ export default function Search() {
});
const [filterDropdown, setFilterDropdown] = useState(false);
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
useLinks({
@@ -45,57 +44,24 @@ export default function Search() {
<div className="flex gap-2">
<FontAwesomeIcon
icon={faSearch}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500 drop-shadow"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary drop-shadow"
/>
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white font-thin">
<p className="sm:text-4xl text-3xl capitalize font-thin">
Search Results
</p>
</div>
</div>
<div className="flex gap-3 items-center">
<div className="flex gap-2 items-center">
<div className="relative">
<div
onClick={() => setFilterDropdown(!filterDropdown)}
id="filter-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faFilter}
id="filter-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{filterDropdown ? (
<FilterSearchDropdown
setFilterDropdown={setFilterDropdown}
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
) : null}
<FilterSearchDropdown
searchFilter={searchFilter}
setSearchFilter={setSearchFilter}
/>
</div>
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{sortDropdown ? (
<SortDropdown
sortBy={sortBy}
setSort={setSortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
</div>
@@ -106,7 +72,7 @@ export default function Search() {
})}
</div>
) : (
<p className="text-black dark:text-white">
<p>
Nothing found.{" "}
<span className="font-bold text-xl" title="Shruggie">
¯\_()_/¯
+78 -90
View File
@@ -153,37 +153,40 @@ export default function Account() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Account Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-5">
<div className="grid sm:grid-cols-2 gap-3 auto-rows-auto">
<div className="flex flex-col gap-3">
<div>
<p className="text-black dark:text-white mb-2">Display Name</p>
<p className="mb-2">Display Name</p>
<TextInput
value={user.name || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, name: e.target.value })}
/>
</div>
<div>
<p className="text-black dark:text-white mb-2">Username</p>
<p className="mb-2">Username</p>
<TextInput
value={user.username || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, username: e.target.value })}
/>
</div>
{emailEnabled ? (
<div>
<p className="text-black dark:text-white mb-2">Email</p>
<p className="mb-2">Email</p>
{user.email !== account.email &&
process.env.NEXT_PUBLIC_STRIPE === "true" ? (
<p className="text-gray-500 dark:text-gray-400 mb-2 text-sm">
<p className="text-neutral mb-2 text-sm">
Updating this field will change your billing email as well
</p>
) : undefined}
<TextInput
value={user.email || ""}
className="bg-base-200"
onChange={(e) => setUser({ ...user, email: e.target.value })}
/>
</div>
@@ -191,14 +194,12 @@ export default function Account() {
</div>
<div className="sm:row-span-2 sm:justify-self-center mx-auto my-3">
<p className="text-black dark:text-white mb-2 text-center">
Profile Photo
</p>
<p className="mb-2 text-center">Profile Photo</p>
<div className="w-28 h-28 flex items-center justify-center rounded-full relative">
<ProfilePhoto
priority={true}
src={user.image ? user.image : undefined}
className="h-auto border-none w-28"
dimensionClass="w-28 h-28"
/>
{user.image && (
<div
@@ -208,13 +209,13 @@ export default function Account() {
image: "",
})
}
className="absolute top-1 left-1 w-5 h-5 flex items-center justify-center border p-1 border-slate-200 dark:border-neutral-700 rounded-full bg-white dark:bg-neutral-800 text-center select-none cursor-pointer duration-100 hover:text-red-500"
className="absolute top-1 left-1 btn btn-xs btn-circle btn-neutral btn-outline bg-base-100"
>
<FontAwesomeIcon icon={faClose} className="w-3 h-3" />
</div>
)}
<div className="absolute -bottom-3 left-0 right-0 mx-auto w-fit text-center">
<label className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
<label className="btn btn-xs btn-neutral btn-outline bg-base-100">
Browse...
<input
type="file"
@@ -232,85 +233,74 @@ export default function Account() {
<div>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
<p className="truncate w-full pr-7 text-3xl font-thin">
Import & Export
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex gap-3 flex-col">
<div>
<p className="text-black dark:text-white mb-2">
Import your data from other platforms.
</p>
<div
onClick={() => setImportDropdown(true)}
className="w-fit relative"
id="import-dropdown"
>
<p className="mb-2">Import your data from other platforms.</p>
<div className="dropdown dropdown-bottom">
<div
tabIndex={0}
role="button"
className="flex gap-2 text-sm btn btn-outline btn-neutral btn-xs"
id="import-dropdown"
className="border border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600"
>
Import From
</div>
{importDropdown ? (
<ClickAwayHandler
onClickOutside={(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.id !== "import-dropdown")
setImportDropdown(false);
}}
className={`absolute top-7 left-0 w-52 py-1 shadow-md border border-sky-100 dark:border-neutral-700 bg-gray-50 dark:bg-neutral-800 rounded-md flex flex-col z-20`}
>
<div className="cursor-pointer rounded-md">
<label
htmlFor="import-linkwarden-file"
title="JSON File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Linkwarden File...
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
<label
htmlFor="import-html-file"
title="HTML File"
className="flex items-center gap-2 py-1 px-2 hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 cursor-pointer"
>
Bookmarks HTML file...
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</div>
</ClickAwayHandler>
) : null}
<ul className="shadow menu dropdown-content z-[1] bg-base-200 border border-neutral-content rounded-box mt-1 w-60">
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-linkwarden-file"
title="JSON File"
>
From Linkwarden
<input
type="file"
name="photo"
id="import-linkwarden-file"
accept=".json"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.linkwarden)
}
/>
</label>
</li>
<li>
<label
tabIndex={0}
role="button"
htmlFor="import-html-file"
title="HTML File"
>
From Bookmarks HTML file
<input
type="file"
name="photo"
id="import-html-file"
accept=".html"
className="hidden"
onChange={(e) =>
importBookmarks(e, MigrationFormat.htmlFile)
}
/>
</label>
</li>
</ul>
</div>
</div>
<div>
<p className="text-black dark:text-white mb-2">
Download your data instantly.
</p>
<p className="mb-2">Download your data instantly.</p>
<Link className="w-fit" href="/api/v1/migration">
<div className="border w-fit border-slate-200 dark:border-neutral-700 rounded-md bg-white dark:bg-neutral-800 px-2 text-center select-none cursor-pointer duration-100 hover:border-sky-300 hover:dark:border-sky-600">
<div className="btn btn-outline btn-neutral btn-xs">
Export Data
</div>
</Link>
@@ -320,12 +310,12 @@ export default function Account() {
<div>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-black dark:text-white truncate w-full pr-7 text-3xl font-thin">
<p className="truncate w-full pr-7 text-3xl font-thin">
Profile Visibility
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<Checkbox
label="Make profile private"
@@ -333,21 +323,19 @@ export default function Account() {
onClick={() => setUser({ ...user, isPrivate: !user.isPrivate })}
/>
<p className="text-gray-500 dark:text-gray-300 text-sm">
<p className="text-neutral text-sm">
This will limit who can find and add you to new Collections.
</p>
{user.isPrivate && (
<div className="pl-5">
<p className="text-black dark:text-white mt-2">
Whitelisted Users
</p>
<p className="text-gray-500 dark:text-gray-300 text-sm mb-3">
<p className="mt-2">Whitelisted Users</p>
<p className="text-neutral text-sm mb-3">
Please provide the Username of the users you wish to grant
visibility to your profile. Separated by comma.
</p>
<textarea
className="w-full resize-none border rounded-md duration-100 bg-gray-50 dark:bg-neutral-950 p-2 outline-none border-sky-100 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600"
className="w-full resize-none border rounded-md duration-100 bg-base-200 p-2 outline-none border-neutral-content focus:border-primary"
placeholder="Your profile is hidden from everyone right now..."
value={whitelistedUsersTextbox}
onChange={(e) => setWhiteListedUsersTextbox(e.target.value)}
@@ -370,7 +358,7 @@ export default function Account() {
</p>
</div>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p>
This will permanently delete ALL the Links, Collections, Tags, and
@@ -381,14 +369,14 @@ export default function Account() {
You will be prompted to enter your password before the deletion
process.
</p>
<Link
href="/settings/delete"
className="mx-auto lg:mx-0 text-white mt-3 flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
>
<p className="text-center w-full">Delete Your Account</p>
</Link>
</div>
<Link
href="/settings/delete"
className="mx-auto lg:mx-0 text-white flex items-center gap-2 py-1 px-3 rounded-md text-lg tracking-wide select-none font-semibold duration-100 w-fit bg-red-500 hover:bg-red-400 cursor-pointer"
>
<p className="text-center w-full">Delete Your Account</p>
</Link>
</div>
</SettingsLayout>
);
+3 -3
View File
@@ -56,10 +56,10 @@ export default function Api() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">API Keys (Soon)</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="flex flex-col gap-3">
<div className="badge bg-orange-500 rounded-md border border-black w-fit px-2 text-black">
<div className="badge badge-warning rounded-md w-fit p-4">
Status: Under Development
</div>
@@ -67,7 +67,7 @@ export default function Api() {
<p>
For now, you can <i>temporarily</i> use your{" "}
<code className="text-xs whitespace-nowrap bg-gray-500/40 rounded-md px-2 py-1">
<code className="text-xs whitespace-nowrap bg-black/40 rounded-md px-2 py-1">
next-auth.session-token
</code>{" "}
in your browser cookies as the API key for your integrations.
+18 -23
View File
@@ -1,23 +1,18 @@
import SettingsLayout from "@/layouts/SettingsLayout";
import { useTheme } from "next-themes";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
import { useState, useEffect } from "react";
import { faClose } from "@fortawesome/free-solid-svg-icons";
import useAccountStore from "@/store/account";
import { AccountSettings } from "@/types/global";
import { toast } from "react-hot-toast";
import TextInput from "@/components/TextInput";
import { resizeImage } from "@/lib/client/resizeImage";
import ProfilePhoto from "@/components/ProfilePhoto";
import SubmitButton from "@/components/SubmitButton";
import React from "react";
import Checkbox from "@/components/Checkbox";
import LinkPreview from "@/components/LinkPreview";
import useLocalSettingsStore from "@/store/localSettings";
export default function Appearance() {
const { theme, setTheme } = useTheme();
const { updateSettings } = useLocalSettingsStore();
const submit = async () => {
setSubmitLoader(true);
@@ -70,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}
+1 -1
View File
@@ -59,7 +59,7 @@ export default function Archive() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Archive Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p>Formats to Archive webpages:</p>
<div className="p-3">
+3 -3
View File
@@ -13,10 +13,10 @@ export default function Billing() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Billing Settings</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<div className="w-full mx-auto flex flex-col gap-3 justify-between">
<p className="text-md text-black dark:text-white">
<p className="text-md">
To manage/cancel your subscription, visit the{" "}
<a
href={process.env.NEXT_PUBLIC_STRIPE_BILLING_PORTAL_URL}
@@ -27,7 +27,7 @@ export default function Billing() {
.
</p>
<p className="text-md text-black dark:text-white">
<p className="text-md">
If you still need help or encountered any issues, feel free to reach
out to us at:{" "}
<a
+15 -13
View File
@@ -7,7 +7,7 @@ import Link from "next/link";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
export default function Password() {
export default function Delete() {
const [password, setPassword] = useState("");
const [comment, setComment] = useState<string>();
const [feedback, setFeedback] = useState<string>();
@@ -54,12 +54,15 @@ export default function Password() {
return (
<CenteredForm>
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 dark:border-neutral-700 bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<div className="p-4 mx-auto relative flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<Link
href="/settings/account"
className="absolute top-4 left-4 gap-1 items-center select-none cursor-pointer p-2 text-gray-500 dark:text-gray-300 rounded-md duration-100 hover:bg-slate-200 dark:hover:bg-neutral-700"
className="absolute top-4 left-4 btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5" />
<FontAwesomeIcon
icon={faChevronLeft}
className="w-5 h-5 text-neutral"
/>
</Link>
<div className="flex items-center gap-2 w-full rounded-md h-8">
<p className="text-red-500 dark:text-red-500 truncate w-full text-3xl text-center">
@@ -67,7 +70,7 @@ export default function Password() {
</p>
</div>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<p>
This will permanently delete all the Links, Collections, Tags, and
@@ -80,22 +83,21 @@ export default function Password() {
{process.env.NEXT_PUBLIC_KEYCLOAK_ENABLED !== "true" ? (
<div>
<p className="mb-2 text-black dark:text-white">
Confirm Your Password
</p>
<p className="mb-2">Confirm Your Password</p>
<TextInput
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••••"
className="bg-base-100"
type="password"
/>
</div>
) : undefined}
{process.env.NEXT_PUBLIC_STRIPE ? (
<fieldset className="border rounded-md p-2 border-sky-500">
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-sky-500">
<fieldset className="border rounded-md p-2 border-primary">
<legend className="px-3 py-1 text-sm sm:text-base border rounded-md border-primary">
<b>Optional</b>{" "}
<i className="min-[390px]:text-sm text-xs">
(but it really helps us improve!)
@@ -104,7 +106,7 @@ export default function Password() {
<label className="w-full flex min-[430px]:items-center items-start gap-2 mb-3 min-[430px]:flex-row flex-col">
<p className="text-sm">Reason for cancellation:</p>
<select
className="rounded-md p-1 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
className="rounded-md p-1 outline-none"
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
>
@@ -120,7 +122,7 @@ export default function Password() {
</select>
</label>
<div>
<p className="text-sm mb-2 text-black dark:text-white">
<p className="text-sm mb-2">
More information (the more details, the more helpful it&apos;d
be)
</p>
@@ -129,7 +131,7 @@ export default function Password() {
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="e.g. I needed a feature that..."
className="resize-none w-full rounded-md p-2 border-sky-100 bg-gray-50 dark:border-neutral-700 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100 dark:bg-neutral-950"
className="resize-none w-full rounded-md p-2 border-neutral-content bg-base-100 focus:border-sky-300 dark:focus:border-sky-600 border-solid border outline-none duration-100"
/>
</div>
</fieldset>
+5 -3
View File
@@ -47,26 +47,28 @@ export default function Password() {
<SettingsLayout>
<p className="capitalize text-3xl font-thin inline">Change Password</p>
<hr className="my-3 border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-3"></div>
<p className="mb-3">
To change your password, please fill out the following. Your password
should be at least 8 characters.
</p>
<div className="w-full flex flex-col gap-2 justify-between">
<p className="text-black dark:text-white">New Password</p>
<p>New Password</p>
<TextInput
value={newPassword}
className="bg-base-200"
onChange={(e) => setNewPassword1(e.target.value)}
placeholder="••••••••••••••"
type="password"
/>
<p className="text-black dark:text-white">Confirm New Password</p>
<p>Confirm New Password</p>
<TextInput
value={newPassword2}
className="bg-base-200"
onChange={(e) => setNewPassword2(e.target.value)}
placeholder="••••••••••••••"
type="password"
+9 -11
View File
@@ -30,12 +30,12 @@ export default function Subscribe() {
process.env.NEXT_PUBLIC_TRIAL_PERIOD_DAYS || 14
}-day free trial, cancel anytime!`}
>
<div className="p-4 mx-auto flex flex-col gap-3 justify-between dark:border-neutral-700 max-w-[30rem] min-w-80 w-full bg-slate-50 dark:bg-neutral-800 rounded-2xl shadow-md border border-sky-100">
<div className="p-4 mx-auto flex flex-col gap-3 justify-between max-w-[30rem] min-w-80 w-full bg-base-200 rounded-2xl shadow-md border border-neutral-content">
<p className="sm:text-3xl text-2xl text-center font-extralight">
Subscribe to Linkwarden!
</p>
<hr className="border-1 border-sky-100 dark:border-neutral-700" />
<div className="divider my-0"></div>
<div>
<p>
@@ -47,10 +47,10 @@ export default function Subscribe() {
</p>
</div>
<div className="flex text-white dark:text-black gap-3 border border-solid border-sky-100 dark:border-neutral-700 w-4/5 mx-auto p-1 rounded-xl relative">
<div className="flex text-white dark:text-black gap-3 border border-solid border-neutral-content w-4/5 mx-auto p-1 rounded-xl relative">
<button
onClick={() => setPlan(Plan.monthly)}
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
className={`w-full duration-100 text-sm rounded-lg p-1 ${
plan === Plan.monthly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
@@ -61,7 +61,7 @@ export default function Subscribe() {
<button
onClick={() => setPlan(Plan.yearly)}
className={`w-full text-black dark:text-white duration-100 text-sm rounded-lg p-1 ${
className={`w-full duration-100 text-sm rounded-lg p-1 ${
plan === Plan.yearly
? "text-white bg-sky-700 dark:bg-sky-700"
: "hover:opacity-80"
@@ -77,15 +77,13 @@ export default function Subscribe() {
<div className="flex flex-col gap-2 justify-center items-center">
<p className="text-3xl">
${plan === Plan.monthly ? "4" : "3"}
<span className="text-base text-gray-500 dark:text-gray-400">
/mo
</span>
<span className="text-base text-neutral">/mo</span>
</p>
<p className="font-semibold">
Billed {plan === Plan.monthly ? "Monthly" : "Yearly"}
</p>
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-sky-100 dark:border-neutral-700">
<legend className="w-fit font-extralight px-2 border border-sky-100 dark:border-neutral-700 rounded-md text-xl">
<fieldset className="w-full flex-col flex justify-evenly px-4 pb-4 pt-1 rounded-md border border-neutral-content">
<legend className="w-fit font-extralight px-2 border border-neutral rounded-md text-xl">
Total
</legend>
@@ -108,7 +106,7 @@ export default function Subscribe() {
<div
onClick={() => signOut()}
className="w-fit mx-auto cursor-pointer text-gray-500 dark:text-gray-400 font-semibold "
className="w-fit mx-auto cursor-pointer text-neutral font-semibold "
>
Sign Out
</div>
+46 -38
View File
@@ -4,7 +4,6 @@ import {
faCheck,
faEllipsis,
faHashtag,
faSort,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -24,7 +23,6 @@ export default function Index() {
const { links } = useLinkStore();
const { tags, updateTag, removeTag } = useTagStore();
const [sortDropdown, setSortDropdown] = useState(false);
const [sortBy, setSortBy] = useState<Sort>(Sort.DateNewestFirst);
const [expandDropdown, setExpandDropdown] = useState(false);
@@ -107,7 +105,7 @@ export default function Index() {
<div className="flex gap-2 items-end font-thin">
<FontAwesomeIcon
icon={faHashtag}
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-sky-500 dark:text-sky-500"
className="sm:w-8 sm:h-8 w-6 h-6 mt-2 text-primary"
/>
{renameTag ? (
<>
@@ -115,50 +113,78 @@ export default function Index() {
<input
type="text"
autoFocus
className="sm:text-4xl text-3xl capitalize text-black dark:text-white bg-transparent h-10 w-3/4 outline-none border-b border-b-sky-100 dark:border-b-neutral-700"
className="sm:text-4xl text-3xl capitalize bg-transparent h-10 w-3/4 outline-none border-b border-b-neutral-content"
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
/>
<div
onClick={() => submit()}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
className="btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon
icon={faCheck}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
className="w-5 h-5 text-neutral"
/>
</div>
<div
onClick={() => cancelUpdateTag()}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
className="btn btn-ghost btn-square btn-sm"
>
<FontAwesomeIcon
icon={faXmark}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
className="w-5 h-5 text-neutral"
/>
</div>
</form>
</>
) : (
<>
<p className="sm:text-4xl text-3xl capitalize text-black dark:text-white">
<p className="sm:text-4xl text-3xl capitalize">
{activeTag?.name}
</p>
<div className="relative">
<div
onClick={() => setExpandDropdown(!expandDropdown)}
id="expand-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faEllipsis}
id="expand-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
<div className="dropdown dropdown-bottom font-normal">
<div
tabIndex={0}
role="button"
className="btn btn-ghost btn-sm btn-square text-neutral"
>
<FontAwesomeIcon
icon={faEllipsis}
title="More"
className="w-5 h-5"
/>
</div>
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box w-36 mt-1">
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
setRenameTag(true);
}}
>
Rename Tag
</div>
</li>
<li>
<div
role="button"
tabIndex={0}
onClick={() => {
(document?.activeElement as HTMLElement)?.blur();
remove();
}}
>
Remove Tag
</div>
</li>
</ul>
</div>
{expandDropdown ? (
@@ -194,25 +220,7 @@ export default function Index() {
</div>
<div className="relative">
<div
onClick={() => setSortDropdown(!sortDropdown)}
id="sort-dropdown"
className="inline-flex rounded-md cursor-pointer hover:bg-slate-200 hover:dark:bg-neutral-700 duration-100 p-1"
>
<FontAwesomeIcon
icon={faSort}
id="sort-dropdown"
className="w-5 h-5 text-gray-500 dark:text-gray-300"
/>
</div>
{sortDropdown ? (
<SortDropdown
sortBy={sortBy}
setSort={setSortBy}
toggleSortDropdown={() => setSortDropdown(!sortDropdown)}
/>
) : null}
<SortDropdown sortBy={sortBy} setSort={setSortBy} />
</div>
</div>
<div className="grid grid-cols-1 2xl:grid-cols-3 xl:grid-cols-2 gap-5">
+27 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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"]');
}),
],
};
-5
View File
@@ -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"