feat: add collection functionality

This commit is contained in:
Daniel 2023-04-25 01:00:40 +03:30
parent b02766f6c8
commit 4bfb08a52e
12 changed files with 86 additions and 134 deletions

View File

@ -3,64 +3,69 @@
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
// You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. // You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
// import React, { useState } from "react"; import React, { useState } from "react";
// import CollectionSelection from "@/components/InputSelect/CollectionSelection"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
// import { faPlus } from "@fortawesome/free-solid-svg-icons"; import useCollectionStore from "@/store/collections";
// import useCollectionStore from "@/store/collections"; import { NewCollection } from "@/types/global";
// type Props = { type Props = {
// toggleCollectionModal: Function; toggleCollectionModal: Function;
// }; };
// export default function AddCollection({ toggleCollectionModal }: Props) { export default function AddCollection({ toggleCollectionModal }: Props) {
// const [newCollection, setNewCollection] = useState({ const [newCollection, setNewCollection] = useState<NewCollection>({
// name: "", name: "",
// ownerId: undefined, description: "",
// }); });
// const { addCollection } = useCollectionStore(); const { addCollection } = useCollectionStore();
// const setCollectionOwner = (e: any) => { const submitCollection = async () => {
// if (e?.__isNew__) e.value = null; console.log(newCollection);
// setNewCollection({ const response = await addCollection(newCollection as NewCollection);
// ...newCollection,
// ownerId: e?.value,
// });
// };
// return ( if (response) toggleCollectionModal();
// <div className="flex flex-col gap-3 sm:w-[35rem] w-80"> };
// <p className="font-bold text-sky-300 mb-2 text-center">New Collection</p>
// <div className="flex gap-5 items-center justify-between"> return (
// <p className="text-sm font-bold text-sky-300">Name</p> <div className="flex flex-col gap-3 sm:w-[35rem] w-80">
// <input <p className="font-bold text-sky-300 mb-2 text-center">New Collection</p>
// value={newCollection.name}
// onChange={(e) =>
// setNewCollection({ ...newCollection, name: e.target.value })
// }
// type="text"
// placeholder="e.g. Example Collection"
// className="w-full rounded-md p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100"
// />
// </div>
// <div className="flex gap-5 items-center justify-between"> <div className="flex gap-5 items-center justify-between">
// <p className="text-sm font-bold text-sky-300">Owner</p> <p className="text-sm font-bold text-sky-300">Name</p>
// <CollectionSelection onChange={setCollectionOwner} /> <input
// </div> value={newCollection.name}
onChange={(e) =>
setNewCollection({ ...newCollection, name: e.target.value })
}
type="text"
placeholder="e.g. Example Collection"
className="w-96 rounded-md p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100"
/>
</div>
// <div className="flex justify-end"> <div className="flex gap-5 items-center justify-between">
// <button <p className="text-sm font-bold text-sky-300">Description</p>
// onClick={() => submitCollection()} <input
// className="py-2 px-4 bg-sky-500 hover:bg-sky-600 text-white rounded-md" value={newCollection.description}
// > onChange={(e) =>
// <FontAwesomeIcon icon={faPlus} /> setNewCollection({ ...newCollection, description: e.target.value })
// <span className="ml-2">Add</span> }
// </button> type="text"
// </div> placeholder="Collection description (Optional)"
// </div> className="w-96 rounded-md p-3 border-sky-100 border-solid border text-sm outline-none focus:border-sky-500 duration-100"
// ); />
// } </div>
<div
className="mx-auto mt-2 bg-sky-500 text-white flex items-center gap-2 py-2 px-5 rounded-md select-none font-bold cursor-pointer duration-100 hover:bg-sky-400"
onClick={submitCollection}
>
<FontAwesomeIcon icon={faPlus} className="h-5" />
Add Collection
</div>
</div>
);
}

View File

@ -70,7 +70,7 @@ export default function AddLink({ toggleLinkModal }: Props) {
const submitLink = async () => { const submitLink = async () => {
console.log(newLink); console.log(newLink);
const response = await addLink(newLink as ExtendedLink); const response = await addLink(newLink as NewLink);
if (response) toggleLinkModal(); if (response) toggleLinkModal();
}; };

View File

@ -3,12 +3,9 @@
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
// You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. // You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
import ClickAwayHandler from "@/components/ClickAwayHandler";
import { useState } from "react";
import useCollectionStore from "@/store/collections"; import useCollectionStore from "@/store/collections";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
faPlus,
faFolder, faFolder,
faBox, faBox,
faHashtag, faHashtag,
@ -19,27 +16,10 @@ import useTagStore from "@/store/tags";
import Link from "next/link"; import Link from "next/link";
export default function () { export default function () {
const [collectionInput, setCollectionInput] = useState(false); const { collections } = useCollectionStore();
const { collections, addCollection } = useCollectionStore();
const { tags } = useTagStore(); const { tags } = useTagStore();
const toggleCollectionInput = () => {
setCollectionInput(!collectionInput);
};
const submitCollection = async (
event: React.KeyboardEvent<HTMLInputElement>
) => {
const collectionName: string = (event.target as HTMLInputElement).value;
if (event.key === "Enter" && collectionName) {
addCollection(collectionName);
(event.target as HTMLInputElement).value = "";
}
};
return ( return (
<div className="fixed bg-gray-100 top-0 bottom-0 left-0 w-80 p-2 overflow-y-auto border-solid border-r-sky-100 border z-20"> <div className="fixed bg-gray-100 top-0 bottom-0 left-0 w-80 p-2 overflow-y-auto border-solid border-r-sky-100 border z-20">
<p className="p-2 text-sky-500 font-bold text-xl mb-5 leading-4"> <p className="p-2 text-sky-500 font-bold text-xl mb-5 leading-4">
@ -60,30 +40,8 @@ export default function () {
</div> </div>
</Link> </Link>
<div className="text-gray-500 flex items-center justify-between mt-5"> <div className="text-gray-500 mt-5">
<p className="text-sm p-2">Collections</p> <p className="text-sm p-2">Collections</p>
{collectionInput ? (
<ClickAwayHandler
onClickOutside={toggleCollectionInput}
className="w-fit"
>
<input
type="text"
placeholder="Enter Collection Name"
className="w-44 rounded-md p-1 border-sky-500 border-solid border text-sm outline-none"
onKeyDown={submitCollection}
autoFocus
/>
</ClickAwayHandler>
) : (
<div
title="Add Collection"
onClick={toggleCollectionInput}
className="select-none text-gray-500 rounded-md cursor-pointer hover:bg-white hover:outline outline-sky-100 outline-1 duration-100 p-1"
>
<FontAwesomeIcon icon={faPlus} className="h-3 w-3" />
</div>
)}
</div> </div>
<div> <div>
{collections.map((e, i) => { {collections.map((e, i) => {
@ -97,7 +55,7 @@ export default function () {
); );
})} })}
</div> </div>
<div className="text-gray-500 flex items-center justify-between mt-5"> <div className="text-gray-500 mt-5">
<p className="text-sm p-2">Tags</p> <p className="text-sm p-2">Tags</p>
</div> </div>
<div> <div>

View File

@ -4,12 +4,13 @@
// You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. // You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
import { prisma } from "@/lib/api/db"; import { prisma } from "@/lib/api/db";
import { Collection } from "@prisma/client";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
export default async function (collectionName: string, userId: number) { export default async function (collection: Collection, userId: number) {
if (!collectionName) if (!collection)
return { return {
response: "Please enter a valid name for the collection.", response: "Please enter a valid collection.",
status: 400, status: 400,
}; };
@ -20,7 +21,7 @@ export default async function (collectionName: string, userId: number) {
select: { select: {
collections: { collections: {
where: { where: {
name: collectionName, name: collection.name,
}, },
}, },
}, },
@ -38,7 +39,8 @@ export default async function (collectionName: string, userId: number) {
id: userId, id: userId,
}, },
}, },
name: collectionName, name: collection.name,
description: collection.description,
}, },
}); });

View File

@ -22,10 +22,7 @@ export default async function (req: NextApiRequest, res: NextApiResponse) {
.status(collections.status) .status(collections.status)
.json({ response: collections.response }); .json({ response: collections.response });
} else if (req.method === "POST") { } else if (req.method === "POST") {
const newCollection = await postCollection( const newCollection = await postCollection(req.body, session.user.id);
req.body.collectionName.trim(),
session.user.id
);
return res return res
.status(newCollection.status) .status(newCollection.status)
.json({ response: newCollection.response }); .json({ response: newCollection.response });

View File

@ -1,2 +0,0 @@
-- DropIndex
DROP INDEX "Tag_name_collectionId_key";

View File

@ -1,11 +0,0 @@
/*
Warnings:
- You are about to drop the column `collectionId` on the `Tag` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "Tag" DROP CONSTRAINT "Tag_collectionId_fkey";
-- AlterTable
ALTER TABLE "Tag" DROP COLUMN "collectionId";

View File

@ -13,6 +13,7 @@ CREATE TABLE "User" (
CREATE TABLE "Collection" ( CREATE TABLE "Collection" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"ownerId" INTEGER NOT NULL, "ownerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -48,7 +49,6 @@ CREATE TABLE "Link" (
CREATE TABLE "Tag" ( CREATE TABLE "Tag" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"collectionId" INTEGER NOT NULL,
"ownerId" INTEGER NOT NULL, "ownerId" INTEGER NOT NULL,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
@ -69,9 +69,6 @@ CREATE UNIQUE INDEX "Collection_name_ownerId_key" ON "Collection"("name", "owner
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "Tag_name_ownerId_key" ON "Tag"("name", "ownerId"); CREATE UNIQUE INDEX "Tag_name_ownerId_key" ON "Tag"("name", "ownerId");
-- CreateIndex
CREATE UNIQUE INDEX "Tag_name_collectionId_key" ON "Tag"("name", "collectionId");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "_LinkToTag_AB_unique" ON "_LinkToTag"("A", "B"); CREATE UNIQUE INDEX "_LinkToTag_AB_unique" ON "_LinkToTag"("A", "B");
@ -90,9 +87,6 @@ ALTER TABLE "UsersAndCollections" ADD CONSTRAINT "UsersAndCollections_collection
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Link" ADD CONSTRAINT "Link_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Link" ADD CONSTRAINT "Link_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- AddForeignKey
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE "Tag" ADD CONSTRAINT "Tag_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -26,6 +26,7 @@ model User {
model Collection { model Collection {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
description String @default("")
owner User @relation(fields: [ownerId], references: [id]) owner User @relation(fields: [ownerId], references: [id])
ownerId Int ownerId Int
members UsersAndCollections[] members UsersAndCollections[]

View File

@ -5,11 +5,12 @@
import { create } from "zustand"; import { create } from "zustand";
import { Collection } from "@prisma/client"; import { Collection } from "@prisma/client";
import { NewCollection } from "@/types/global";
type CollectionStore = { type CollectionStore = {
collections: Collection[]; collections: Collection[];
setCollections: () => void; setCollections: () => void;
addCollection: (collectionName: string) => void; addCollection: (body: NewCollection) => Promise<boolean>;
updateCollection: (collection: Collection) => void; updateCollection: (collection: Collection) => void;
removeCollection: (collectionId: number) => void; removeCollection: (collectionId: number) => void;
}; };
@ -23,9 +24,9 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
if (response.ok) set({ collections: data.response }); if (response.ok) set({ collections: data.response });
}, },
addCollection: async (collectionName) => { addCollection: async (body) => {
const response = await fetch("/api/routes/collections", { const response = await fetch("/api/routes/collections", {
body: JSON.stringify({ collectionName }), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@ -38,6 +39,8 @@ const useCollectionStore = create<CollectionStore>()((set) => ({
set((state) => ({ set((state) => ({
collections: [...state.collections, data.response], collections: [...state.collections, data.response],
})); }));
return response.ok;
}, },
updateCollection: (collection) => updateCollection: (collection) =>
set((state) => ({ set((state) => ({

View File

@ -4,14 +4,14 @@
// You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. // You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
import { create } from "zustand"; import { create } from "zustand";
import { ExtendedLink } from "@/types/global"; import { ExtendedLink, NewLink } from "@/types/global";
import useTagStore from "./tags"; import useTagStore from "./tags";
import useCollectionStore from "./collections"; import useCollectionStore from "./collections";
type LinkStore = { type LinkStore = {
links: ExtendedLink[]; links: ExtendedLink[];
setLinks: () => void; setLinks: () => void;
addLink: (linkName: ExtendedLink) => Promise<boolean>; addLink: (body: NewLink) => Promise<boolean>;
updateLink: (link: ExtendedLink) => void; updateLink: (link: ExtendedLink) => void;
removeLink: (link: ExtendedLink) => void; removeLink: (link: ExtendedLink) => void;
}; };
@ -25,9 +25,9 @@ const useLinkStore = create<LinkStore>()((set) => ({
if (response.ok) set({ links: data.response }); if (response.ok) set({ links: data.response });
}, },
addLink: async (newLink) => { addLink: async (body) => {
const response = await fetch("/api/routes/links", { const response = await fetch("/api/routes/links", {
body: JSON.stringify(newLink), body: JSON.stringify(body),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },

View File

@ -20,3 +20,8 @@ export interface NewLink {
ownerId: number | undefined; ownerId: number | undefined;
}; };
} }
export interface NewCollection {
name: string;
description: string;
}