diff --git a/components/AddLinkModal.tsx b/components/AddLinkModal.tsx index 31cf1c22..5e74380e 100644 --- a/components/AddLinkModal.tsx +++ b/components/AddLinkModal.tsx @@ -3,26 +3,22 @@ import CollectionSelection from "./InputSelect/CollectionSelection"; import TagSelection from "./InputSelect/TagSelection"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; -interface NewLink { - name: string; - url: string; - tags: string[]; - collectionId: - | { - id: string | number; - isNew: boolean | undefined; - } - | object; -} +import { useRouter } from "next/router"; +import { NewLink } from "@/types/global"; +import useLinkSlice from "@/store/links"; export default function () { + const router = useRouter(); + const [newLink, setNewLink] = useState({ name: "", url: "", tags: [], - collectionId: {}, + collectionId: { id: Number(router.query.id) }, }); + const { addLink } = useLinkSlice(); + const setTags = (e: any) => { const tagNames = e.map((e: any) => { return e.label; @@ -54,7 +50,7 @@ export default function () { }; return ( -
+

New Link

@@ -64,8 +60,7 @@ export default function () { onChange={(e) => setNewLink({ ...newLink, name: e.target.value })} type="text" placeholder="e.g. Example Link" - className="w-60 rounded p-2 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" - autoFocus + className="w-60 rounded p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" />
@@ -76,7 +71,7 @@ export default function () { onChange={(e) => setNewLink({ ...newLink, url: e.target.value })} type="text" placeholder="e.g. http://example.com/" - className="w-60 rounded p-2 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" + className="w-60 rounded p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100" />
@@ -92,7 +87,7 @@ export default function () {
postLink()} + onClick={() => addLink(newLink)} > Add Link diff --git a/components/ClickAwayHandler.tsx b/components/ClickAwayHandler.tsx index 50537833..3c8567da 100644 --- a/components/ClickAwayHandler.tsx +++ b/components/ClickAwayHandler.tsx @@ -19,10 +19,8 @@ function useOutsideAlerter( onClickOutside(); } } - // Bind the event listener document.addEventListener("mouseup", handleClickOutside); return () => { - // Unbind the event listener on clean up document.removeEventListener("mouseup", handleClickOutside); }; }, [ref, onClickOutside]); diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index aa5ba541..6a682cd4 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -1,6 +1,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronRight } from "@fortawesome/free-solid-svg-icons"; import { Collection } from "@prisma/client"; +import Link from "next/link"; export default function ({ collection }: { collection: Collection }) { const formattedDate = new Date(collection.createdAt).toLocaleString("en-US", { @@ -10,12 +11,14 @@ export default function ({ collection }: { collection: Collection }) { }); return ( -
-
-

{collection.name}

- + +
+
+

{collection.name}

+ +
+

{formattedDate}

-

{formattedDate}

-
+ ); } diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index e603d3a6..1c3348a6 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -1,4 +1,4 @@ -import useCollectionSlice from "@/store/collection"; +import useCollectionSlice from "@/store/collections"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import CreatableSelect from "react-select/creatable"; diff --git a/components/LinkList.tsx b/components/LinkList.tsx new file mode 100644 index 00000000..8afd6587 --- /dev/null +++ b/components/LinkList.tsx @@ -0,0 +1,23 @@ +import { LinkAndTags } from "@/types/global"; + +export default function ({ + link, + count, +}: { + link: LinkAndTags; + count: number; +}) { + return ( +
+
+

{count + 1}.

+

{link.name}

+
+
+ {link.tags.map((e, i) => ( +

{e.name}

+ ))} +
+
+ ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 9b422f75..d439ed4e 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,22 +1,63 @@ import { signOut } from "next-auth/react"; import { useRouter } from "next/router"; -import useCollectionSlice from "@/store/collection"; -import { Collection } from "@prisma/client"; +import useCollectionSlice from "@/store/collections"; +import { Collection, Tag } from "@prisma/client"; import ClickAwayHandler from "./ClickAwayHandler"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons"; -import { useState } from "react"; +import { + faPlus, + faFolder, + faBox, + faTag, + faBookmark, + faMagnifyingGlass, + IconDefinition, +} from "@fortawesome/free-solid-svg-icons"; +import { useEffect, useState } from "react"; import AddLinkModal from "./AddLinkModal"; +import useTagSlice from "@/store/tags"; export default function () { const router = useRouter(); - const collectionId = router.query.id; + const [pageName, setPageName] = useState(""); + const [pageIcon, setPageIcon] = useState(null); const { collections } = useCollectionSlice(); + const { tags } = useTagSlice(); - const activeCollection: Collection | undefined = collections.find( - (e) => e.id.toString() == collectionId - ); + useEffect(() => { + if (router.route === "/collections/[id]") { + const collectionId = router.query.id; + + const activeCollection: Collection | undefined = collections.find( + (e) => e.id.toString() == collectionId + ); + + if (activeCollection) { + setPageName(activeCollection?.name); + } + + setPageIcon(faFolder); + } else if (router.route === "/tags/[id]") { + const tagId = router.query.id; + + const activeTag: Tag | undefined = tags.find( + (e) => e.id.toString() == tagId + ); + + if (activeTag) { + setPageName(activeTag?.name); + } + + setPageIcon(faTag); + } else if (router.route === "/collections") { + setPageName("All Collections"); + setPageIcon(faBox); + } else if (router.route === "/links") { + setPageName("All Links"); + setPageIcon(faBookmark); + } + }, [router, collections, tags]); const [collectionInput, setCollectionInput] = useState(false); @@ -26,7 +67,12 @@ export default function () { return (
-

{activeCollection?.name}

+
+ {pageIcon ? ( + + ) : null} +

{pageName}

+
{collectionInput ? ( -
+
- -

{item.name}

-
+ +
+ +

{text}

+
+ ); } diff --git a/components/Sidebar/index.tsx b/components/Sidebar/index.tsx index 9e338996..66d725ee 100644 --- a/components/Sidebar/index.tsx +++ b/components/Sidebar/index.tsx @@ -1,19 +1,20 @@ import { useSession } from "next-auth/react"; import ClickAwayHandler from "@/components/ClickAwayHandler"; import { useState } from "react"; -import useCollectionSlice from "@/store/collection"; +import useCollectionSlice from "@/store/collections"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUserCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faChevronDown, faFolder, - faBoxesStacked, - faHashtag, + faBox, + faTag, faBookmark, } from "@fortawesome/free-solid-svg-icons"; import SidebarItem from "./SidebarItem"; import useTagSlice from "@/store/tags"; +import Link from "next/link"; export default function () { const { data: session } = useSession(); @@ -51,9 +52,19 @@ export default function () {
- + +
+ +

All Links

+
+ - + +
+ +

All Collections

+
+

Collections

@@ -80,7 +91,14 @@ export default function () {
{collections.map((e, i) => { - return ; + return ( + + ); })}
@@ -88,7 +106,14 @@ export default function () {
{tags.map((e, i) => { - return ; + return ( + + ); })}
diff --git a/lib/api/controllers/links/getLinks.ts b/lib/api/controllers/links/getLinks.ts new file mode 100644 index 00000000..9884aac4 --- /dev/null +++ b/lib/api/controllers/links/getLinks.ts @@ -0,0 +1,33 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "@/lib/api/db"; +import { Session } from "next-auth"; + +export default async function ( + req: NextApiRequest, + res: NextApiResponse, + session: Session +) { + const tags = await prisma.link.findMany({ + where: { + collection: { + OR: [ + { + ownerId: session?.user.id, + }, + { + members: { + some: { + userId: session?.user.id, + }, + }, + }, + ], + }, + }, + include: { tags: true }, + }); + + return res.status(200).json({ + response: tags || [], + }); +} diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index c9491600..3cb87bee 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -1,18 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "@/lib/api/db"; import { Session } from "next-auth"; -import { Link } from "@prisma/client"; - -interface LinkObject { - id: number; - name: string; - url: string; - tags: string[]; - collectionId: { - id: string | number; - isNew: boolean | undefined; - }; -} +import { LinkAndTags, NewLink } from "@/types/global"; export default async function ( req: NextApiRequest, @@ -24,7 +13,7 @@ export default async function ( } const email: string = session.user.email; - const link: LinkObject = req?.body; + const link: NewLink = req?.body; if (!link.name) { return res @@ -70,10 +59,10 @@ export default async function ( const collectionId = link.collectionId.id as number; - const createLink: Link = await prisma.link.create({ + const createLink: LinkAndTags = await prisma.link.create({ data: { name: link.name, - url: "https://www.example.com", + url: link.url, collection: { connect: { id: collectionId, @@ -98,6 +87,7 @@ export default async function ( })), }, }, + include: { tags: true }, }); return res.status(200).json({ diff --git a/lib/client/getInitialData.ts b/lib/client/getInitialData.ts index 0cd8c523..b0afa40f 100644 --- a/lib/client/getInitialData.ts +++ b/lib/client/getInitialData.ts @@ -1,17 +1,20 @@ -import useCollectionSlice from "@/store/collection"; +import useCollectionSlice from "@/store/collections"; import { useEffect } from "react"; import { useSession } from "next-auth/react"; import useTagSlice from "@/store/tags"; +import useLinkSlice from "@/store/links"; export default function () { const { status } = useSession(); const { setCollections } = useCollectionSlice(); const { setTags } = useTagSlice(); + const { setLinks } = useLinkSlice(); useEffect(() => { if (status === "authenticated") { setCollections(); setTags(); + setLinks(); } }, [status]); } diff --git a/pages/api/routes/links/index.ts b/pages/api/routes/links/index.ts index 1ace1430..cd548857 100644 --- a/pages/api/routes/links/index.ts +++ b/pages/api/routes/links/index.ts @@ -1,6 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getServerSession } from "next-auth/next"; import { authOptions } from "pages/api/auth/[...nextauth]"; +import getLinks from "@/lib/api/controllers/links/getLinks"; import postLink from "@/lib/api/controllers/links/postLink"; type Data = { @@ -20,5 +21,6 @@ export default async function ( // Check if user is unauthorized to the collection (If isn't owner or doesn't has the required permission...) // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + if (req.method === "GET") return await getLinks(req, res, session); if (req.method === "POST") return await postLink(req, res, session); } diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx index 8d9e982b..08b76bbf 100644 --- a/pages/collections/[id].tsx +++ b/pages/collections/[id].tsx @@ -1,11 +1,24 @@ +import LinkList from "@/components/LinkList"; +import useLinkSlice from "@/store/links"; import { useRouter } from "next/router"; export default function () { const router = useRouter(); + const { links } = useLinkSlice(); - const collectionId = Number(router.query.id); + const linksByCollection = links.filter( + (e) => e.collectionId === Number(router.query.id) + ); - console.log(collectionId); - - return
{"HI"}
; + return ( + // ml-80 +
+

+ {linksByCollection.length || 0} Links Found +

+ {linksByCollection.map((e, i) => { + return ; + })} +
+ ); } diff --git a/pages/collections/index.tsx b/pages/collections/index.tsx index 3ce9f3e9..5b5312de 100644 --- a/pages/collections/index.tsx +++ b/pages/collections/index.tsx @@ -1,5 +1,5 @@ import { useSession } from "next-auth/react"; -import useCollectionSlice from "@/store/collection"; +import useCollectionSlice from "@/store/collections"; import CollectionCard from "@/components/CollectionCard"; diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx new file mode 100644 index 00000000..3d89f4d9 --- /dev/null +++ b/pages/tags/[id].tsx @@ -0,0 +1,9 @@ +import { useRouter } from "next/router"; + +export default function () { + const router = useRouter(); + + const tagId = Number(router.query.id); + + return
{"HI"}
; +} diff --git a/store/collection.ts b/store/collections.ts similarity index 100% rename from store/collection.ts rename to store/collections.ts diff --git a/store/links.ts b/store/links.ts new file mode 100644 index 00000000..63e2df8a --- /dev/null +++ b/store/links.ts @@ -0,0 +1,48 @@ +import { create } from "zustand"; +import { LinkAndTags, NewLink } from "@/types/global"; + +type LinkSlice = { + links: LinkAndTags[]; + setLinks: () => void; + addLink: (linkName: NewLink) => void; + updateLink: (link: LinkAndTags) => void; + removeLink: (linkId: number) => void; +}; + +const useLinkSlice = create()((set) => ({ + links: [], + setLinks: async () => { + const response = await fetch("/api/routes/links"); + + const data = await response.json(); + + if (response.ok) set({ links: data.response }); + }, + addLink: async (newLink) => { + const response = await fetch("/api/routes/links", { + body: JSON.stringify(newLink), + headers: { + "Content-Type": "application/json", + }, + method: "POST", + }); + + const data = await response.json(); + + if (response.ok) + set((state) => ({ + links: [...state.links, data.response], + })); + }, + updateLink: (link) => + set((state) => ({ + links: state.links.map((c) => (c.id === link.id ? link : c)), + })), + removeLink: (linkId) => { + set((state) => ({ + links: state.links.filter((c) => c.id !== linkId), + })); + }, +})); + +export default useLinkSlice; diff --git a/styles/globals.css b/styles/globals.css index edf4f200..790abe35 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -10,3 +10,40 @@ -ms-overflow-style: none; scrollbar-width: none; } + +::selection { + background-color: #0ea5e9; + color: white; +} + +.hyphens { + hyphens: auto; +} + +.slide-up { + animation: slide-up-animation 70ms; +} + +.fade-in { + animation: fade-in-animation 100ms; +} + +@keyframes fade-in-animation { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes slide-up-animation { + 0% { + transform: translateY(10%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +} diff --git a/types/global.ts b/types/global.ts new file mode 100644 index 00000000..c8d0a23d --- /dev/null +++ b/types/global.ts @@ -0,0 +1,15 @@ +import { Link, Tag } from "@prisma/client"; + +export interface LinkAndTags extends Link { + tags: Tag[]; +} + +export interface NewLink { + name: string; + url: string; + tags: string[]; + collectionId: { + id: string | number; + isNew?: boolean; + }; +}