Skip to content

Commit

Permalink
[19] Feature: I can create a collection that will represent a physica…
Browse files Browse the repository at this point in the history
…l 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
  • Loading branch information
mathewmorris authored Dec 24, 2024
1 parent 94c9b12 commit e2acec0
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 46 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Collection" ADD COLUMN "cards" TEXT[];
19 changes: 10 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,23 @@ 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
layout String
}

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
Expand Down
6 changes: 3 additions & 3 deletions src/components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { signIn, signOut, useSession } from 'next-auth/react';
import Button from './Button';

function AuthButton() {
const { data: sessionData } = useSession();

return (
<button
className="rounded-full dark:bg-purple-950 px-4 py-2 font-semibold no-underline transition dark:hover:bg-pink-800 dark:hover:drop-shadow-glow"
<Button
onClick={sessionData ? () => void signOut() : () => void signIn()}
>
{sessionData ? "Sign out" : "Sign in"}
</button>
</Button>
);
}

Expand Down
26 changes: 26 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type ButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className'>

export default function Button({ children, ...props }: ButtonProps) {
return (
<button
className="
rounded-full
dark:bg-purple-950
px-4
py-2
font-semibold
no-underline
transition
dark:hover:bg-pink-800
dark:focus:bg-pink-800
dark:hover:drop-shadow-glow
dark:focus:drop-shadow-glow
"
{...props}
>
{children}
</button>

)
}

26 changes: 24 additions & 2 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([]);

const {
data: searchResults,
Expand All @@ -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 (
<>
<input
Expand Down Expand Up @@ -50,11 +68,15 @@ export default function SearchBar() {
>
{images?.small && <Image src={images.small} width={146} height={204} alt={card.name} className="m-2 rounded-lg" />}
</div>
{selectedCards.includes(card.id) ?
<Button type="button" onClick={() => removeCard(card.id)}>Remove Card</Button> :
<Button type="button" onClick={() => addCard(card.id)}>Add Card</Button>
}
</div>
)
})
})}
{hasNextPage && (<button onClick={() => void fetchNextPage()}>Next Page</button>)}
{hasNextPage && (<Button onClick={() => void fetchNextPage()}>Next Page</Button>)}
{isFetchingNextPage && <div>Fetching next page...</div>}
</div>
</>
Expand Down
21 changes: 17 additions & 4 deletions src/pages/collection/[id].tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>My Page</h1>
<h2>Slug: {router.query.id}</h2>
</div>
<div>
<h1>{collection.data?.name}</h1>
{cards.data?.map(card => {
const images = card.image_uris as { small: string } | null;

return (
<div key={card.id}>
<p>{card.name}</p>
<Image src={images?.small ?? ""} width={100} height={200} alt={card.name} />
</div>
)
})}
</div>
);
}
38 changes: 38 additions & 0 deletions src/pages/collection/create.tsx
Original file line number Diff line number Diff line change
@@ -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<string[]>([])

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 (
<div>
<h1>Creating Collection</h1>
<form>
<div className="p-4">
<label htmlFor="collectionName">Collection Name</label>
<input className="text-black" type="text" id="collectionName" onChange={e => setCollectionName(e.target.value)} value={collectionName} />
</div>
<Button type="button" onClick={() => createCollection({ name: collectionName, cards: selectedCards })}>Create Collection</Button>
<div className="p-4">
<SearchBar onSelectedChange={setSelectedCards} />
</div>
</form>
</div>
)
}

33 changes: 33 additions & 0 deletions src/pages/collections/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<p>No collections...</p>
</>
)
}

return (collections.map(collection => (
<div key={collection.id}>
<span>{collection.name}</span>
<Link href={`/collection/${collection.id}`}>View Collection</Link>
</div>
)))
}

export default function CollectionsIndex() {
const { data: collections } = api.collection.getAll.useQuery();

return (
<div>
<Link href="collection/create">Create Collection</Link>
<p>Your collections</p>
<CollectionList collections={collections ?? []} />
</div>
)
}

25 changes: 10 additions & 15 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto grid justify-items-stretch">
<h1 className="text-5xl flex-row justify-self-center py-4">Magic Vault</h1>
</div>
)
}

return (
<div className="container mx-auto p-4">
<div>
<h2 className="text-xl">Look for a card</h2>
<SearchBar />
</div>
<div className="container mx-auto grid justify-items-stretch">
<h1 className="text-5xl flex-row justify-self-center py-4">Magic Vault</h1>
{sessionData && (
<div>
<Link href="/collections">Your Collections</Link>
</div>
)}
</div>
);
)

};

export default App;
Expand Down
9 changes: 9 additions & 0 deletions src/server/api/routers/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
39 changes: 27 additions & 12 deletions src/server/api/routers/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}),
});

0 comments on commit e2acec0

Please sign in to comment.