feat: added dropdown component

This commit is contained in:
Daniel 2023-03-23 02:41:54 +03:30
parent e5e2a615fc
commit f80113c487
20 changed files with 152 additions and 54 deletions

View File

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { NewLink } from "@/types/global"; import { NewLink } from "@/types/global";
import useLinkSlice from "@/store/links"; import useLinkStore from "@/store/links";
export default function ({ toggleLinkModal }: { toggleLinkModal: Function }) { export default function ({ toggleLinkModal }: { toggleLinkModal: Function }) {
const router = useRouter(); const router = useRouter();
@ -17,7 +17,7 @@ export default function ({ toggleLinkModal }: { toggleLinkModal: Function }) {
collection: { id: Number(router.query.id) }, collection: { id: Number(router.query.id) },
}); });
const { addLink } = useLinkSlice(); const { addLink } = useLinkStore();
const setTags = (e: any) => { const setTags = (e: any) => {
const tagNames = e.map((e: any) => { const tagNames = e.map((e: any) => {

View File

@ -1,10 +1,10 @@
import React, { useRef, useEffect, ReactNode, RefObject } from "react"; import React, { useRef, useEffect, ReactNode, RefObject } from "react";
interface Props { type Props = {
children: ReactNode; children: ReactNode;
onClickOutside: Function; onClickOutside: Function;
className?: string; className?: string;
} };
function useOutsideAlerter( function useOutsideAlerter(
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,

47
components/Dropdown.tsx Normal file
View File

@ -0,0 +1,47 @@
import Link from "next/link";
import React, { MouseEventHandler, ReactElement } from "react";
import ClickAwayHandler from "./ClickAwayHandler";
type MenuItem = {
name: string;
icon: ReactElement;
onClick?: MouseEventHandler;
href?: string;
};
type Props = {
onClickOutside: Function;
className?: string;
items: MenuItem[];
};
export default function ({ onClickOutside, className, items }: Props) {
return (
<ClickAwayHandler
onClickOutside={onClickOutside}
className={`${className} border border-sky-100 shadow mb-5 bg-gray-50 p-4 rounded flex flex-col gap-4`}
>
{items.map((e, i) => {
return e.href ? (
<Link key={i} href={e.href}>
<div className="flex items-center gap-2 px-2 cursor-pointer">
{React.cloneElement(e.icon, {
className: "text-sky-500 w-5 h-5",
})}
<p className="text-sky-900">{e.name}</p>
</div>
</Link>
) : (
<div key={i} onClick={e.onClick}>
<div className="flex items-center gap-2 px-2 cursor-pointer">
{React.cloneElement(e.icon, {
className: "text-sky-500 w-5 h-5",
})}
<p className="text-sky-900">{e.name}</p>
</div>
</div>
);
})}
</ClickAwayHandler>
);
}

View File

@ -1,4 +1,4 @@
import useCollectionSlice from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import CreatableSelect from "react-select/creatable"; import CreatableSelect from "react-select/creatable";
@ -6,7 +6,7 @@ import { styles } from "./styles";
import { Options } from "./types"; import { Options } from "./types";
export default function ({ onChange }: any) { export default function ({ onChange }: any) {
const { collections } = useCollectionSlice(); const { collections } = useCollectionStore();
const router = useRouter(); const router = useRouter();
const [options, setOptions] = useState<Options[]>([]); const [options, setOptions] = useState<Options[]>([]);

View File

@ -1,11 +1,11 @@
import useTagSlice from "@/store/tags"; import useTagStore from "@/store/tags";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import CreatableSelect from "react-select/creatable"; import CreatableSelect from "react-select/creatable";
import { styles } from "./styles"; import { styles } from "./styles";
import { Options } from "./types"; import { Options } from "./types";
export default function ({ onChange }: any) { export default function ({ onChange }: any) {
const { tags } = useTagSlice(); const { tags } = useTagStore();
const [options, setOptions] = useState<Options[]>([]); const [options, setOptions] = useState<Options[]>([]);

View File

@ -3,13 +3,16 @@ import {
faFolder, faFolder,
faArrowUpRightFromSquare, faArrowUpRightFromSquare,
faEllipsis, faEllipsis,
faHeart, faStar,
faPenToSquare,
faTrashCan,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons"; import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import Dropdown from "./Dropdown";
export default function ({ export default function ({
link, link,
@ -18,6 +21,7 @@ export default function ({
link: ExtendedLink; link: ExtendedLink;
count: number; count: number;
}) { }) {
const [editDropdown, setEditDropdown] = useState(false);
const [archiveLabel, setArchiveLabel] = useState("Archived Formats"); const [archiveLabel, setArchiveLabel] = useState("Archived Formats");
const shortendURL = new URL(link.url).host.toLowerCase(); const shortendURL = new URL(link.url).host.toLowerCase();
@ -42,8 +46,8 @@ export default function ({
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<p className="text-sm text-sky-300 font-bold">{count + 1}.</p> <p className="text-sm text-sky-300 font-bold">{count + 1}.</p>
<p className="text-lg text-sky-600">{link.name}</p> <p className="text-lg text-sky-600">{link.name}</p>
{link.isFavorites ? ( {link.starred ? (
<FontAwesomeIcon icon={faHeart} className="w-3 text-red-600" /> <FontAwesomeIcon icon={faStar} className="w-3 text-amber-400" />
) : null} ) : null}
</div> </div>
<p className="text-sky-400 text-sm font-medium">{link.title}</p> <p className="text-sky-400 text-sm font-medium">{link.title}</p>
@ -76,10 +80,12 @@ export default function ({
</a> </a>
</div> </div>
</div> </div>
<div className="flex flex-col justify-between items-end">
<div className="flex flex-col justify-between items-end relative">
<FontAwesomeIcon <FontAwesomeIcon
icon={faEllipsis} icon={faEllipsis}
className="w-6 h-6 text-gray-500 cursor-pointer" className="w-6 h-6 text-gray-500 hover:text-gray-400 duration-100 cursor-pointer"
onClick={() => setEditDropdown(!editDropdown)}
/> />
<div> <div>
<p className="text-center text-sky-500 text-sm font-bold"> <p className="text-center text-sky-500 text-sm font-bold">
@ -121,6 +127,27 @@ export default function ({
/> />
</div> </div>
</div> </div>
{editDropdown ? (
<Dropdown
items={[
{
name: "Star",
icon: <FontAwesomeIcon icon={faStar} />,
},
{
name: "Edit",
icon: <FontAwesomeIcon icon={faPenToSquare} />,
},
{
name: "Delete",
icon: <FontAwesomeIcon icon={faTrashCan} />,
},
]}
onClickOutside={() => setEditDropdown(!editDropdown)}
className="absolute top-8 right-0"
/>
) : null}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,5 @@
import { signOut } from "next-auth/react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useCollectionSlice from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { Collection, Tag } from "@prisma/client"; import { Collection, Tag } from "@prisma/client";
import ClickAwayHandler from "./ClickAwayHandler"; import ClickAwayHandler from "./ClickAwayHandler";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -15,15 +14,15 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AddLinkModal from "./AddLinkModal"; import AddLinkModal from "./AddLinkModal";
import useTagSlice from "@/store/tags"; import useTagStore from "@/store/tags";
export default function () { export default function () {
const router = useRouter(); const router = useRouter();
const [pageName, setPageName] = useState<string | null>(""); const [pageName, setPageName] = useState<string | null>("");
const [pageIcon, setPageIcon] = useState<IconDefinition | null>(null); const [pageIcon, setPageIcon] = useState<IconDefinition | null>(null);
const { collections } = useCollectionSlice(); const { collections } = useCollectionStore();
const { tags } = useTagSlice(); const { tags } = useTagStore();
useEffect(() => { useEffect(() => {
if (router.route === "/collections/[id]") { if (router.route === "/collections/[id]") {
@ -83,12 +82,6 @@ export default function () {
icon={faMagnifyingGlass} icon={faMagnifyingGlass}
className="select-none cursor-pointer w-5 h-5 text-white bg-sky-500 p-2 rounded hover:bg-sky-400 duration-100" className="select-none cursor-pointer w-5 h-5 text-white bg-sky-500 p-2 rounded hover:bg-sky-400 duration-100"
/> />
<div
onClick={() => signOut()}
className="cursor-pointer w-max text-sky-900"
>
Sign Out
</div>
{linkModal ? ( {linkModal ? (
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in z-10"> <div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in z-10">

View File

@ -1,9 +1,9 @@
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import ClickAwayHandler from "@/components/ClickAwayHandler"; import ClickAwayHandler from "@/components/ClickAwayHandler";
import { useState } from "react"; import { useState } from "react";
import useCollectionSlice from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { signOut } from "next-auth/react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUserCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faPlus, faPlus,
faChevronDown, faChevronDown,
@ -11,19 +11,24 @@ import {
faBox, faBox,
faHashtag, faHashtag,
faBookmark, faBookmark,
faCircleUser,
faSliders,
faArrowRightFromBracket,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import SidebarItem from "./SidebarItem"; import SidebarItem from "./SidebarItem";
import useTagSlice from "@/store/tags"; import useTagStore from "@/store/tags";
import Link from "next/link"; import Link from "next/link";
import Dropdown from "@/components/Dropdown";
export default function () { export default function () {
const { data: session } = useSession(); const { data: session } = useSession();
const [collectionInput, setCollectionInput] = useState(false); const [collectionInput, setCollectionInput] = useState(false);
const [profileDropdown, setProfileDropdown] = useState(false);
const { collections, addCollection } = useCollectionSlice(); const { collections, addCollection } = useCollectionStore();
const { tags } = useTagSlice(); const { tags } = useTagStore();
const user = session?.user; const user = session?.user;
@ -44,12 +49,35 @@ export default function () {
return ( return (
<div className="fixed bg-gray-100 top-0 bottom-0 left-0 w-80 p-5 overflow-y-auto hide-scrollbar border-solid border-r-sky-100 border z-10"> <div className="fixed bg-gray-100 top-0 bottom-0 left-0 w-80 p-5 overflow-y-auto hide-scrollbar border-solid border-r-sky-100 border z-10">
<div className="flex gap-3 items-center mb-5 p-3 cursor-pointer w-fit text-gray-600"> <div className="flex gap-3 items-center mb-5 p-3 w-fit text-gray-600 relative">
<FontAwesomeIcon icon={faUserCircle} className="h-8" /> <FontAwesomeIcon icon={faCircleUser} className="h-8" />
<div className="flex items-center gap-1"> <div
className="flex items-center gap-1 cursor-pointer"
onClick={() => setProfileDropdown(!profileDropdown)}
>
<p>{user?.name}</p> <p>{user?.name}</p>
<FontAwesomeIcon icon={faChevronDown} className="h-3" /> <FontAwesomeIcon icon={faChevronDown} className="h-3" />
</div> </div>
{profileDropdown ? (
<Dropdown
items={[
{
name: "Settings",
icon: <FontAwesomeIcon icon={faSliders} />,
},
{
name: "Logout",
icon: <FontAwesomeIcon icon={faArrowRightFromBracket} />,
onClick: () => {
signOut();
setProfileDropdown(!profileDropdown);
},
},
]}
onClickOutside={() => setProfileDropdown(!profileDropdown)}
className="absolute top-14 left-0"
/>
) : null}
</div> </div>
<Link href="links"> <Link href="links">

View File

@ -110,7 +110,7 @@ export default async function (
})), })),
}, },
title, title,
isFavorites: false, starred: false,
screenshotPath: "", screenshotPath: "",
pdfPath: "", pdfPath: "",
}, },

View File

@ -1,14 +1,14 @@
import useCollectionSlice from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useTagSlice from "@/store/tags"; import useTagStore from "@/store/tags";
import useLinkSlice from "@/store/links"; import useLinkStore from "@/store/links";
export default function () { export default function () {
const { status } = useSession(); const { status } = useSession();
const { setCollections } = useCollectionSlice(); const { setCollections } = useCollectionStore();
const { setTags } = useTagSlice(); const { setTags } = useTagStore();
const { setLinks } = useLinkSlice(); const { setLinks } = useLinkStore();
useEffect(() => { useEffect(() => {
if (status === "authenticated") { if (status === "authenticated") {

View File

@ -4,6 +4,7 @@
"main": "index.js", "main": "index.js",
"repository": "https://github.com/Daniel31x13/link-warden.git", "repository": "https://github.com/Daniel31x13/link-warden.git",
"author": "Daniel31X13 <daniel31x13@gmail.com>", "author": "Daniel31X13 <daniel31x13@gmail.com>",
"license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@ -1,10 +1,10 @@
import LinkList from "@/components/LinkList"; import LinkList from "@/components/LinkList";
import useLinkSlice from "@/store/links"; import useLinkStore from "@/store/links";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
export default function () { export default function () {
const router = useRouter(); const router = useRouter();
const { links } = useLinkSlice(); const { links } = useLinkStore();
const linksByCollection = links.filter( const linksByCollection = links.filter(
(e) => e.collectionId === Number(router.query.id) (e) => e.collectionId === Number(router.query.id)

View File

@ -1,10 +1,10 @@
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import useCollectionSlice from "@/store/collections"; import useCollectionStore from "@/store/collections";
import CollectionCard from "@/components/CollectionCard"; import CollectionCard from "@/components/CollectionCard";
export default function () { export default function () {
const { collections } = useCollectionSlice(); const { collections } = useCollectionStore();
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const user = session?.user; const user = session?.user;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Link" RENAME COLUMN "isFavorites" TO "starred";

View File

@ -50,7 +50,7 @@ model Link {
collection Collection @relation(fields: [collectionId], references: [id]) collection Collection @relation(fields: [collectionId], references: [id])
collectionId Int collectionId Int
tags Tag[] tags Tag[]
isFavorites Boolean starred Boolean
screenshotPath String screenshotPath String
pdfPath String pdfPath String
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -1,7 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { Collection } from "@prisma/client"; import { Collection } from "@prisma/client";
type CollectionSlice = { type CollectionStore = {
collections: Collection[]; collections: Collection[];
setCollections: () => void; setCollections: () => void;
addCollection: (collectionName: string) => void; addCollection: (collectionName: string) => void;
@ -9,7 +9,7 @@ type CollectionSlice = {
removeCollection: (collectionId: number) => void; removeCollection: (collectionId: number) => void;
}; };
const useCollectionSlice = create<CollectionSlice>()((set) => ({ const useCollectionStore = create<CollectionStore>()((set) => ({
collections: [], collections: [],
setCollections: async () => { setCollections: async () => {
const response = await fetch("/api/routes/collections"); const response = await fetch("/api/routes/collections");
@ -47,4 +47,4 @@ const useCollectionSlice = create<CollectionSlice>()((set) => ({
}, },
})); }));
export default useCollectionSlice; export default useCollectionStore;

View File

@ -1,7 +1,7 @@
import { create } from "zustand"; import { create } from "zustand";
import { ExtendedLink, NewLink } from "@/types/global"; import { ExtendedLink, NewLink } from "@/types/global";
type LinkSlice = { type LinkStore = {
links: ExtendedLink[]; links: ExtendedLink[];
setLinks: () => void; setLinks: () => void;
addLink: (linkName: NewLink) => Promise<boolean>; addLink: (linkName: NewLink) => Promise<boolean>;
@ -9,7 +9,7 @@ type LinkSlice = {
removeLink: (linkId: number) => void; removeLink: (linkId: number) => void;
}; };
const useLinkSlice = create<LinkSlice>()((set) => ({ const useLinkStore = create<LinkStore>()((set) => ({
links: [], links: [],
setLinks: async () => { setLinks: async () => {
const response = await fetch("/api/routes/links"); const response = await fetch("/api/routes/links");
@ -47,4 +47,4 @@ const useLinkSlice = create<LinkSlice>()((set) => ({
}, },
})); }));
export default useLinkSlice; export default useLinkStore;

View File

@ -1,12 +1,12 @@
import { create } from "zustand"; import { create } from "zustand";
import { Tag } from "@prisma/client"; import { Tag } from "@prisma/client";
type TagSlice = { type TagStore = {
tags: Tag[]; tags: Tag[];
setTags: () => void; setTags: () => void;
}; };
const useTagSlice = create<TagSlice>()((set) => ({ const useTagStore = create<TagStore>()((set) => ({
tags: [], tags: [],
setTags: async () => { setTags: async () => {
const response = await fetch("/api/routes/tags"); const response = await fetch("/api/routes/tags");
@ -17,4 +17,4 @@ const useTagSlice = create<TagSlice>()((set) => ({
}, },
})); }));
export default useTagSlice; export default useTagStore;

View File

@ -39,7 +39,7 @@
@keyframes slide-up-animation { @keyframes slide-up-animation {
0% { 0% {
transform: translateY(10%); transform: translateY(15%);
opacity: 0; opacity: 0;
} }
100% { 100% {