From e2acec0adc82263787b5d62563179c736bb6d3dc Mon Sep 17 00:00:00 2001 From: Mat Morris Date: Tue, 24 Dec 2024 04:05:59 -0600 Subject: [PATCH] [19] Feature: I can create a collection that will represent a physical collection (#31) * can create collection * displays cards on collection page * scripts + index updates * can create collections * fix lint errors add ButtonProps type void router replacement when creating collection add alt text to images when viewing collection * fixing routes using singular noun for singular operations using plural noun for plural operations --- package.json | 3 +- .../migration.sql | 2 + prisma/schema.prisma | 19 ++++----- src/components/AuthButton.tsx | 6 +-- src/components/Button.tsx | 26 +++++++++++++ src/components/SearchBar.tsx | 26 ++++++++++++- src/pages/collection/[id].tsx | 21 ++++++++-- src/pages/collection/create.tsx | 38 ++++++++++++++++++ src/pages/collections/index.tsx | 33 ++++++++++++++++ src/pages/index.tsx | 25 +++++------- src/server/api/routers/card.ts | 9 +++++ src/server/api/routers/collection.ts | 39 +++++++++++++------ 12 files changed, 201 insertions(+), 46 deletions(-) create mode 100644 prisma/migrations/20241122063426_add_cards_to_collection/migration.sql create mode 100644 src/components/Button.tsx create mode 100644 src/pages/collection/create.tsx create mode 100644 src/pages/collections/index.tsx diff --git a/package.json b/package.json index 2d63754..944fedf 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "db:gen": "prisma generate", "db:seed": "prisma db seed", "db:migrate": "prisma migrate dev", - "db:push": "prisma db push" + "db:push": "prisma db push", + "studio": "prisma studio" }, "dependencies": { "@next-auth/prisma-adapter": "^1.0.7", diff --git a/prisma/migrations/20241122063426_add_cards_to_collection/migration.sql b/prisma/migrations/20241122063426_add_cards_to_collection/migration.sql new file mode 100644 index 0000000..12477c5 --- /dev/null +++ b/prisma/migrations/20241122063426_add_cards_to_collection/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Collection" ADD COLUMN "cards" TEXT[]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 04deddd..4a37b25 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,9 +10,9 @@ datasource db { model Card { id String @id - name String - scryfall_uri String - image_status String + name String + scryfall_uri String + image_status String image_uris Json? @db.JsonB card_faces Json? @db.JsonB all_parts Json? @db.JsonB @@ -20,12 +20,13 @@ model Card { } model Collection { - id String @id @default(cuid()) - name String? @default("My Collection") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId String + id String @id @default(cuid()) + name String? @default("My Collection") + cards String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId String } // Necessary for Next auth diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx index 5c979e8..0a4b1be 100644 --- a/src/components/AuthButton.tsx +++ b/src/components/AuthButton.tsx @@ -1,15 +1,15 @@ import { signIn, signOut, useSession } from 'next-auth/react'; +import Button from './Button'; function AuthButton() { const { data: sessionData } = useSession(); return ( - + ); } diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..d7b19ff --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,26 @@ +type ButtonProps = Omit, 'className'> + +export default function Button({ children, ...props }: ButtonProps) { + return ( + + + ) +} + diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 5ca18e1..57d755b 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,9 +1,15 @@ import Image from "next/image"; import { useState } from "react"; import { api } from "~/utils/api"; +import Button from "./Button"; -export default function SearchBar() { +interface SearchBarProps { + onSelectedChange: (cardIds: string[]) => void; +} + +export default function SearchBar({ onSelectedChange }: SearchBarProps) { const [input, setInput] = useState(''); + const [selectedCards, setSelectedCards] = useState([]); const { data: searchResults, @@ -23,6 +29,18 @@ export default function SearchBar() { } ) + function addCard(cardId: string) { + const updated = [...selectedCards, cardId]; + onSelectedChange(updated); + setSelectedCards(updated); + } + + function removeCard(cardId: string) { + const updated = selectedCards.filter(card => card !== cardId); + onSelectedChange(updated); + setSelectedCards(updated); + } + return ( <> {images?.small && {card.name}} + {selectedCards.includes(card.id) ? + : + + } ) }) })} - {hasNextPage && ()} + {hasNextPage && ()} {isFetchingNextPage &&
Fetching next page...
} diff --git a/src/pages/collection/[id].tsx b/src/pages/collection/[id].tsx index 05f4b2a..69f5911 100644 --- a/src/pages/collection/[id].tsx +++ b/src/pages/collection/[id].tsx @@ -1,12 +1,25 @@ +import Image from 'next/image'; import { useRouter } from 'next/router'; +import { api } from '~/utils/api'; export default function Page() { const router = useRouter() + const collection = api.collection.byId.useQuery({ id: router.query.id as string }) + const cards = api.card.findMany.useQuery({ cardIds: collection.data?.cards ?? [] }) return ( -
-

My Page

-

Slug: {router.query.id}

-
+
+

{collection.data?.name}

+ {cards.data?.map(card => { + const images = card.image_uris as { small: string } | null; + + return ( +
+

{card.name}

+ {card.name} +
+ ) + })} +
); } diff --git a/src/pages/collection/create.tsx b/src/pages/collection/create.tsx new file mode 100644 index 0000000..d6b0de2 --- /dev/null +++ b/src/pages/collection/create.tsx @@ -0,0 +1,38 @@ +import { useRouter } from "next/router" +import { useState } from "react" +import Button from "~/components/Button" +import SearchBar from "~/components/SearchBar" +import { api } from "~/utils/api" + +export default function CreateCollectionPage() { + const router = useRouter(); + const [collectionName, setCollectionName] = useState('') + const [selectedCards, setSelectedCards] = useState([]) + + const { mutate: collectionMutation } = api.collection.create.useMutation({ + onSuccess: newCollection => { + void router.replace(`/collection/${newCollection.id}`) + } + }); + + function createCollection(data: { name: string, cards: string[] }) { + collectionMutation(data); + } + + return ( +
+

Creating Collection

+
+
+ + setCollectionName(e.target.value)} value={collectionName} /> +
+ +
+ +
+
+
+ ) +} + diff --git a/src/pages/collections/index.tsx b/src/pages/collections/index.tsx new file mode 100644 index 0000000..d4bff1d --- /dev/null +++ b/src/pages/collections/index.tsx @@ -0,0 +1,33 @@ +import Link from "next/link" +import { type Collection } from "@prisma/client" +import { api } from "~/utils/api" + +const CollectionList = ({ collections }: { collections: Collection[] }) => { + if (collections.length < 1) { + return ( + <> +

No collections...

+ + ) + } + + return (collections.map(collection => ( +
+ {collection.name} + View Collection +
+ ))) +} + +export default function CollectionsIndex() { + const { data: collections } = api.collection.getAll.useQuery(); + + return ( +
+ Create Collection +

Your collections

+ +
+ ) +} + diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ae65dad..7007b04 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,25 +1,20 @@ import { useSession } from "next-auth/react"; -import SearchBar from "~/components/SearchBar"; +import Link from "next/link"; const App = () => { const { data: sessionData } = useSession(); - if (!sessionData) { - return ( -
-

Magic Vault

-
- ) - } - return ( -
-
-

Look for a card

- -
+
+

Magic Vault

+ {sessionData && ( +
+ Your Collections +
+ )}
- ); + ) + }; export default App; diff --git a/src/server/api/routers/card.ts b/src/server/api/routers/card.ts index 43634f2..2bcbc5c 100644 --- a/src/server/api/routers/card.ts +++ b/src/server/api/routers/card.ts @@ -6,6 +6,15 @@ import { } from "~/server/api/trpc"; export const cardRouter = createTRPCRouter({ + findMany: publicProcedure.input(z.object({ cardIds: z.string().array() })).query(async ({ ctx, input }) => { + const cards = await ctx.prisma.card.findMany({ + where: { + id: { in: input.cardIds }, + } + }) + + return cards; + }), search: publicProcedure .meta({ description: "Searching for card based on name alone.", }) .input( diff --git a/src/server/api/routers/collection.ts b/src/server/api/routers/collection.ts index f93cc95..1166770 100644 --- a/src/server/api/routers/collection.ts +++ b/src/server/api/routers/collection.ts @@ -17,21 +17,36 @@ export const collectionRouter = createTRPCRouter({ } }); }), - create: protectedProcedure.input(z.object({ name: z.string() })) - .mutation(async ({ ctx, input }) => { + create: protectedProcedure.input(z.object({ name: z.string(), cards: z.string().array() })) + .mutation(async ({ ctx, input }) => { - const collection = await ctx.prisma.collection.create({ - data: { - name: input.name, - user: { - connect: { - id: ctx.session.user.id + const collection = await ctx.prisma.collection.create({ + data: { + name: input.name, + cards: input.cards, + user: { + connect: { + id: ctx.session.user.id + } } } - } - }); + }); - return collection; - }), + return collection; + }), + update: protectedProcedure.input(z.object({ id: z.string(), name: z.string().optional(), cards: z.string().array().optional() })) + .mutation(async ({ ctx, input }) => { + const collection = await ctx.prisma.collection.update({ + where: { + id: input.id, + }, + data: { + name: input.name, + cards: input.cards, + }, + }) + + return collection; + }), });