diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index 1533c3b..0bc2c9d 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -273,13 +273,13 @@ const renderItem = ( icon={collection.icon} size={30} weight={(collection.iconWeight || "regular") as IconWeight} - color={collection.color || "#0ea5e9"} + color={collection.color} className="-mr-[0.15rem]" /> ) : ( )} diff --git a/components/EditButton.tsx b/components/EditButton.tsx new file mode 100644 index 0000000..8286061 --- /dev/null +++ b/components/EditButton.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { useTranslation } from "next-i18next"; +import clsx from "clsx"; + +type Props = { + onClick: Function; + className?: string; +}; + +function EditButton({ onClick, className }: Props) { + const { t } = useTranslation(); + + return ( + onClick()} + className={clsx( + "group-hover:opacity-100 opacity-0 duration-100 btn-square btn-xs btn btn-ghost absolute bi-pencil-fill text-neutral cursor-pointer -right-7 text-xs", + className + )} + title={t("edit")} + > + ); +} + +export default EditButton; diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx index f561137..82643a8 100644 --- a/components/IconPicker.tsx +++ b/components/IconPicker.tsx @@ -6,6 +6,8 @@ import { useTranslation } from "next-i18next"; import Icon from "./Icon"; import { IconWeight } from "@phosphor-icons/react"; import IconGrid from "./IconGrid"; +import IconPopover from "./IconPopover"; +import clsx from "clsx"; type Props = { alignment?: string; @@ -33,7 +35,6 @@ const IconPicker = ({ reset, }: Props) => { const { t } = useTranslation(); - const [query, setQuery] = useState(""); const [iconPicker, setIconPicker] = useState(false); return ( @@ -59,57 +60,21 @@ const IconPicker = ({ )} {iconPicker && ( - setIconPicker(false)} - className={ - className + - " fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg backdrop-blur-sm bg-opacity-90 " + - (alignment || " lg:-translate-x-1/3 top-20 left-0 ") - } - > -
-
-
} - > - {t("reset")} -
- - setColor(e)} /> -
- -
- setQuery(e.target.value)} - /> - -
- -
-
-
-
+ className={clsx( + className, + alignment || "lg:-translate-x-1/3 top-20 left-0" + )} + /> )} ); diff --git a/components/IconPopover.tsx b/components/IconPopover.tsx new file mode 100644 index 0000000..f0cbb3c --- /dev/null +++ b/components/IconPopover.tsx @@ -0,0 +1,91 @@ +import React, { useState } from "react"; +import TextInput from "./TextInput"; +import Popover from "./Popover"; +import { HexColorPicker } from "react-colorful"; +import { useTranslation } from "next-i18next"; +import IconGrid from "./IconGrid"; +import clsx from "clsx"; + +type Props = { + alignment?: string; + color: string; + setColor: Function; + iconName?: string; + setIconName: Function; + weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; + setWeight: Function; + reset: Function; + className?: string; + onClose: Function; +}; + +const IconPopover = ({ + alignment, + color, + setColor, + iconName, + setIconName, + weight, + setWeight, + reset, + className, + onClose, +}: Props) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(""); + + return ( + onClose()} + className={clsx( + className, + "fade-in bg-base-200 border border-neutral-content p-2 h-44 w-[22.5rem] rounded-lg" + )} + > +
+
+
} + > + {t("reset")} +
+ + setColor(e)} /> +
+ +
+ setQuery(e.target.value)} + /> + +
+ +
+
+
+
+ ); +}; + +export default IconPopover; diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 81bef39..f53df37 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -16,6 +16,8 @@ type Props = { } | undefined; creatable?: boolean; + autoFocus?: boolean; + onBlur?: any; }; export default function CollectionSelection({ @@ -23,6 +25,8 @@ export default function CollectionSelection({ defaultValue, showDefaultValue = true, creatable = true, + autoFocus, + onBlur, }: Props) { const { data: collections = [] } = useCollections(); @@ -76,7 +80,7 @@ export default function CollectionSelection({ return (
{data.label} @@ -104,6 +108,8 @@ export default function CollectionSelection({ onChange={onChange} options={options} styles={styles} + autoFocus={autoFocus} + onBlur={onBlur} defaultValue={showDefaultValue ? defaultValue : null} components={{ Option: customOption, @@ -120,7 +126,9 @@ export default function CollectionSelection({ onChange={onChange} options={options} styles={styles} + autoFocus={autoFocus} defaultValue={showDefaultValue ? defaultValue : null} + onBlur={onBlur} components={{ Option: customOption, }} diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index 65e7499..08460ea 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -10,9 +10,16 @@ type Props = { value: number; label: string; }[]; + autoFocus?: boolean; + onBlur?: any; }; -export default function TagSelection({ onChange, defaultValue }: Props) { +export default function TagSelection({ + onChange, + defaultValue, + autoFocus, + onBlur, +}: Props) { const { data: tags = [] } = useTags(); const [options, setOptions] = useState([]); @@ -35,6 +42,8 @@ export default function TagSelection({ onChange, defaultValue }: Props) { styles={styles} defaultValue={defaultValue} isMulti + autoFocus={autoFocus} + onBlur={onBlur} /> ); } diff --git a/components/InputSelect/styles.ts b/components/InputSelect/styles.ts index 96aad6d..1d6336a 100644 --- a/components/InputSelect/styles.ts +++ b/components/InputSelect/styles.ts @@ -50,19 +50,28 @@ export const styles: StylesConfig = { multiValue: (styles) => { return { ...styles, - backgroundColor: "#0ea5e9", - color: "white", + backgroundColor: "oklch(var(--b2))", + color: "oklch(var(--bc))", + display: "flex", + alignItems: "center", + gap: "0.1rem", + marginRight: "0.4rem", }; }, multiValueLabel: (styles) => ({ ...styles, - color: "white", + color: "oklch(var(--bc))", }), multiValueRemove: (styles) => ({ ...styles, + height: "1.2rem", + width: "1.2rem", + borderRadius: "100px", + transition: "all 100ms", + color: "oklch(var(--w))", ":hover": { - color: "white", - backgroundColor: "#38bdf8", + color: "red", + backgroundColor: "oklch(var(--nc))", }, }), menuPortal: (base) => ({ ...base, zIndex: 9999 }), diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx index 7c978be..0afe613 100644 --- a/components/LinkDetails.tsx +++ b/components/LinkDetails.tsx @@ -17,7 +17,7 @@ import getPublicUserData from "@/lib/client/getPublicUserData"; import { useTranslation } from "next-i18next"; import { BeatLoader } from "react-spinners"; import { useUser } from "@/hooks/store/user"; -import { useGetLink } from "@/hooks/store/links"; +import { useGetLink, useUpdateLink } from "@/hooks/store/links"; import LinkIcon from "./LinkViews/LinkComponents/LinkIcon"; import CopyButton from "./CopyButton"; import { useRouter } from "next/router"; @@ -25,14 +25,27 @@ import Icon from "./Icon"; import { IconWeight } from "@phosphor-icons/react"; import Image from "next/image"; import clsx from "clsx"; +import toast from "react-hot-toast"; +import EditButton from "./EditButton"; +import CollectionSelection from "./InputSelect/CollectionSelection"; +import TagSelection from "./InputSelect/TagSelection"; +import unescapeString from "@/lib/client/unescapeString"; +import IconPopover from "./IconPopover"; type Props = { className?: string; - link: LinkIncludingShortenedCollectionAndTags; + activeLink: LinkIncludingShortenedCollectionAndTags; standalone?: boolean; }; -export default function LinkDetails({ className, link, standalone }: Props) { +export default function LinkDetails({ + className, + activeLink, + standalone, +}: Props) { + const [link, setLink] = + useState(activeLink); + const { t } = useTranslation(); const session = useSession(); const getLink = useGetLink(); @@ -129,6 +142,59 @@ export default function LinkDetails({ className, link, standalone }: Props) { const isPublicRoute = router.pathname.startsWith("/public") ? true : false; + const updateLink = useUpdateLink(); + + const [fieldToEdit, setFieldToEdit] = useState< + "name" | "collection" | "tags" | "description" | null + >(null); + + const submit = async (e?: any) => { + e?.preventDefault(); + + const { updatedAt: b, ...oldLink } = activeLink; + const { updatedAt: a, ...newLink } = link; + + console.log(oldLink); + console.log(newLink); + + if (JSON.stringify(oldLink) === JSON.stringify(newLink)) { + setFieldToEdit(null); + return; + } + + const load = toast.loading(t("updating")); + + await updateLink.mutateAsync(link, { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success(t("updated")); + setFieldToEdit(null); + console.log(data); + setLink(data); + } + }, + }); + }; + + 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) => ({ name: e.label })); + setLink({ ...link, tags: tagNames }); + }; + + const [iconPopover, setIconPopover] = useState(false); + return (
)} -
- + +
+
+
+
+ setIconPopover(true)} + /> +
+ {/* #006796 */} + {iconPopover && ( + setLink({ ...link, color })} + weight={(link.iconWeight || "regular") as IconWeight} + setWeight={(iconWeight: string) => + setLink({ ...link, iconWeight }) + } + iconName={link.icon as string} + setIconName={(icon: string) => setLink({ ...link, icon })} + reset={() => + setLink({ + ...link, + color: "", + icon: "", + iconWeight: "", + }) + } + className="top-12" + onClose={() => { + setIconPopover(false); + submit(); + }} + /> + )} +
+
- {link.name &&

{link.name}

} + {fieldToEdit !== "name" ? ( +
+

+ {link.name || t("untitled")} + setFieldToEdit("name")} + className="top-0" + /> +

+
+ ) : fieldToEdit === "name" ? ( +
+ setLink({ ...link, name: e.target.value })} + /> +
+ ) : undefined} {link.url && ( <> @@ -179,7 +348,7 @@ export default function LinkDetails({ className, link, standalone }: Props) {

{t("link")}

-
+
{link.url} @@ -193,82 +362,148 @@ export default function LinkDetails({ className, link, standalone }: Props) {
-

{t("collection")}

+
+

+ {t("collection")} + {fieldToEdit !== "collection" && ( + setFieldToEdit("collection")} + className="bottom-0" + /> + )} +

-
- -

{link.collection.name}

-
- {link.collection.icon ? ( - - ) : ( - - )} + {fieldToEdit !== "collection" ? ( +
+ +

{link.collection.name}

+
+ {link.collection.icon ? ( + + ) : ( + + )} +
+
- + ) : fieldToEdit === "collection" ? ( + + ) : undefined}
- {link.tags[0] && ( - <> -
+
-
-

{t("tags")}

-
+
+

+ {t("tags")} + {fieldToEdit !== "tags" && ( + setFieldToEdit("tags")} + className="bottom-0" + /> + )} +

-
- {link.tags.map((tag) => - isPublicRoute ? ( -
- {tag.name} -
- ) : ( - - {tag.name} - + {fieldToEdit !== "tags" ? ( +
+ {link.tags[0] ? ( + link.tags.map((tag) => + isPublicRoute ? ( +
+ {tag.name} +
+ ) : ( + + {tag.name} + + ) ) + ) : ( +
{t("no_tags")}
)}
- - )} + ) : ( + ({ + label: e.name, + value: e.id, + }))} + autoFocus + onBlur={submit} + /> + )} +
- {link.description && ( - <> -
+
-
-

{t("notes")}

+
+

+ {t("description")} + {fieldToEdit !== "description" && ( + setFieldToEdit("description")} + className="bottom-0" + /> + )} +

-
+ {fieldToEdit !== "description" ? ( +
+ {link.description ? (

{link.description}

-
+ ) : ( +

{t("no_description_provided")}

+ )}
- - )} + ) : ( +