Skip to content

Commit 41fbfc4

Browse files
authored
Trade with history (#844)
* wip * wip * Show active trades on TradeWith by default
1 parent dac3c13 commit 41fbfc4

File tree

14 files changed

+285
-201
lines changed

14 files changed

+285
-201
lines changed

frontend/src/components/Card.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ export function Card({ card, onImageClick, className, editable = true }: CardPro
2626
startTransition(async () => {
2727
setAmountOwned(x)
2828
try {
29-
await updateCardsMutation.mutateAsync({
30-
updates: [{ card_id: card.card_id, internal_id: card.internal_id, amount_owned: x }],
31-
})
29+
await updateCardsMutation.mutateAsync([{ card_id: card.card_id, internal_id: card.internal_id, amount_owned: x }])
3230
} catch (error) {
3331
console.log('Failed updating card count:', error)
3432
}

frontend/src/components/InstallPrompt.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ const InstallPrompt = () => {
7878
>
7979
<p className="pb-2">{t('install')}</p>
8080
<div className="justify-between flex">
81-
<Button onClick={handleInstallClick} type="button" variant="default">
81+
<Button onClick={handleInstallClick} variant="default">
8282
{t('accept')}
8383
</Button>
8484
<Button

frontend/src/components/ui/button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
3333
isPending?: boolean
3434
}
3535

36-
export function Button({ className, variant, size, isPending, children, ...props }: ButtonProps) {
36+
export function Button({ className, variant, size, isPending, children, type, ...props }: ButtonProps) {
3737
return (
38-
<button className={cn(buttonVariants({ variant, size }), className)} {...props}>
38+
<button type={type ?? 'button'} className={cn(buttonVariants({ variant, size }), className)} {...props}>
3939
{children}
4040
{isPending && <Spinner size="inline" className="inline-block ml-2" />}
4141
</button>

frontend/src/pages/import/components/ImportReader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const ImportReader = () => {
3939
setNumberProcessed((n) => n + 1)
4040
}
4141

42-
updateCardsMutation.mutate({ updates: cardArray })
42+
updateCardsMutation.mutate(cardArray)
4343
}
4444

4545
const onDrop = (acceptedFiles: File[]) => {

frontend/src/pages/scan/Scan.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,13 @@ const Scan = () => {
168168
updates.push({ card_id, previous_amount, increment })
169169
}
170170

171-
try {
172-
updateCardsMutation.mutate({
173-
updates: updates.map((x) => ({
174-
card_id: x.card_id,
175-
internal_id: getInteralIdByCardId(x.card_id),
176-
amount_owned: Math.max(0, x.previous_amount + x.increment),
177-
})),
178-
})
179-
} catch (error) {
180-
setError(`Error incrementing card quantities: ${error}`)
181-
return
182-
}
171+
updateCardsMutation.mutate(
172+
updates.map((x) => ({
173+
card_id: x.card_id,
174+
internal_id: getInteralIdByCardId(x.card_id),
175+
amount_owned: Math.max(0, x.previous_amount + x.increment),
176+
})),
177+
)
183178

184179
setIncrementedCards(updates)
185180
setState(State.ProcessUpdates + 1)

frontend/src/pages/trade/TradeWith.tsx

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { ChevronRight } from 'lucide-react'
1+
import { ChevronFirst, ChevronLeft, ChevronRight } from 'lucide-react'
22
import { useEffect, useState } from 'react'
33
import { useTranslation } from 'react-i18next'
44
import { Link, useParams, useSearchParams } from 'react-router'
55
import { Spinner } from '@/components/Spinner'
66
import { Button } from '@/components/ui/button'
77
import { FriendIdDisplay } from '@/components/ui/friend-id-display'
8+
import { Switch } from '@/components/ui/switch'
89
import { getCardByInternalId, tradeableExpansions } from '@/lib/CardsDB.ts'
910
import { getExtraCards, getNeededCards } from '@/lib/utils'
1011
import { CardList } from '@/pages/trade/components/CardList.tsx'
1112
import { TradeOffer } from '@/pages/trade/components/TradeOffer.tsx'
1213
import { useAccount, usePublicAccount } from '@/services/account/useAccount'
1314
import { useCollection, usePublicCollection } from '@/services/collection/useCollection'
15+
import { useAllTrades } from '@/services/trade/useTrade'
1416
import { type Card, type CollectionRow, type Rarity, type TradableRarity, tradableRarities } from '@/types'
17+
import TradeList from './components/TradeList'
1518

1619
function getTradeCards(extraCards: number[], neededCards: number[]) {
1720
const neededCardsSet = new Set(neededCards)
@@ -33,6 +36,10 @@ function TradeWith() {
3336
const { data: account } = useAccount()
3437
const { data: ownedCards = new Map<number, CollectionRow>() } = useCollection()
3538

39+
const [viewHistory, setViewHistory] = useState(false)
40+
const [pageHistory, setPageHistory] = useState(0)
41+
const allTrades = useAllTrades(friendAccount?.friend_id, viewHistory, pageHistory, true)
42+
3643
const [yourCard, setYourCard] = useState<Card | null>(null)
3744
const [friendCard, setFriendCard] = useState<Card | null>(() => {
3845
const id = searchParams.get('friend_card')
@@ -91,23 +98,60 @@ function TradeWith() {
9198
const hasPossibleTrades = tradableRarities.some((r) => (userTrades[r] ?? []).length > 0 && (friendTrades[r] ?? []).length > 0)
9299

93100
return (
94-
<div className="kap-4 justify-center w-full m-auto px-1 sm:px-2">
101+
<div className="flex flex-col gap-4 w-full px-1 sm:px-2">
95102
<title>{`Trade with ${friendAccount.username} – TCG Pocket Collection Tracker`}</title>
96-
<div className="mb-4 mx-1 flex justify-between">
103+
<div className="mx-1 flex justify-between">
97104
<h1>
98105
<span className="text-2xl font-light">{t('tradingWith')}</span>
99106
<span className="text-2xl font-bold"> {friendAccount.username} </span>
100107
<span className="block sm:inline text-sm">
101108
<FriendIdDisplay friendId={friendAccount.friend_id} />
102109
</span>
103110
</h1>
104-
<Link to={`/collection/${friendId}`}>
105-
<Button>
106-
Collection
107-
<ChevronRight />
108-
</Button>
109-
</Link>
111+
<div className="flex flex-col-reverse sm:flex-row gap-2">
112+
<label htmlFor={`history-${friendId}`} className="my-auto flex items-center">
113+
{t('viewHistory')}
114+
<Switch
115+
id={`history-${friendId}`}
116+
className="ml-2 my-auto"
117+
checked={viewHistory}
118+
onCheckedChange={(x) => {
119+
setViewHistory(x)
120+
setPageHistory(0)
121+
}}
122+
/>
123+
</label>
124+
<Link to={`/collection/${friendId}`}>
125+
<Button>
126+
Collection
127+
<ChevronRight />
128+
</Button>
129+
</Link>
130+
</div>
110131
</div>
132+
{allTrades.isLoading ? (
133+
<Spinner size="md" />
134+
) : (
135+
allTrades.data && (
136+
<TradeList trades={allTrades.data.trades}>
137+
<div className="flex justify-between mt-2">
138+
<span className="text-neutral-400">{allTrades.data.count} total offers</span>
139+
<div className="flex items-center gap-2">
140+
<span>Page {pageHistory + 1}</span>
141+
<Button variant="outline" onClick={() => setPageHistory(() => 0)} disabled={pageHistory <= 0}>
142+
<ChevronFirst />
143+
</Button>
144+
<Button variant="outline" onClick={() => setPageHistory((prev) => prev - 1)} disabled={pageHistory <= 0}>
145+
<ChevronLeft />
146+
</Button>
147+
<Button variant="outline" onClick={() => setPageHistory((prev) => prev + 1)} disabled={!allTrades.data.hasNext}>
148+
<ChevronRight />
149+
</Button>
150+
</div>
151+
</div>
152+
</TradeList>
153+
)
154+
)}
111155

112156
<TradeOffer
113157
yourId={account.friend_id}
@@ -126,21 +170,21 @@ function TradeWith() {
126170
)}
127171

128172
{tradableRarities.map((rarity) => (
129-
<div key={rarity} className="mt-4">
173+
<div key={rarity}>
130174
<h3 id={rarity} className="text-xl font-semibold mb-2 text-center">
131175
[ {rarity} ]
132176
</h3>
133-
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4">
177+
<div className="flex flex-col-reverse sm:flex-row gap-2 sm:gap-4">
134178
<div className="w-full sm:w-1/2">
135-
<h4 className="text-md font-medium mb-1 ml-2">{t('youHave')}</h4>
179+
<h4 className="text-md font-medium mb-1 ml-2 text-neutral-400">{t('youHave')}</h4>
136180
{userTrades[rarity] ? (
137181
<CardList cards={userTrades[rarity]} selected={yourCard} setSelected={setYourCard} />
138182
) : (
139183
<div className="text-center text-neutral-500 rounded-lg border-1 border-neutral-700 border-solid p-2">No cards to trade</div>
140184
)}
141185
</div>
142186
<div className="w-full sm:w-1/2">
143-
<h4 className="text-md font-medium mb-1 ml-2">{t('friendHas')}</h4>
187+
<h4 className="text-md font-medium mb-1 ml-2 text-neutral-400">{t('friendHas')}</h4>
144188
{friendTrades[rarity] ? (
145189
<CardList cards={friendTrades[rarity]} selected={friendCard} setSelected={setFriendCard} />
146190
) : (
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type { Dispatch, SetStateAction } from 'react'
2+
import { useTranslation } from 'react-i18next'
3+
import { Button } from '@/components/ui/button'
4+
import { useToast } from '@/hooks/use-toast'
5+
import { getInteralIdByCardId } from '@/lib/CardsDB'
6+
import { umami } from '@/lib/utils'
7+
import { useAccount } from '@/services/account/useAccount'
8+
import { useCollection, useUpdateCards } from '@/services/collection/useCollection'
9+
import { useUpdateTrade } from '@/services/trade/useTrade'
10+
import type { CardAmountUpdate, TradeRow, TradeStatus } from '@/types'
11+
12+
interface Props {
13+
trade: TradeRow
14+
setSelected: Dispatch<SetStateAction<number | undefined>>
15+
}
16+
17+
export default function Actions({ trade, setSelected }: Props) {
18+
const { t } = useTranslation('trade-matches')
19+
const { toast } = useToast()
20+
21+
const { data: account, isLoading: isLoadingAccount } = useAccount()
22+
const { data: ownedCards, isLoading: isLoadingCollection } = useCollection()
23+
const updateCardsMutation = useUpdateCards()
24+
const updateTradeMutation = useUpdateTrade()
25+
26+
if (isLoadingAccount || isLoadingCollection) {
27+
return null
28+
}
29+
30+
if (!account || !ownedCards) {
31+
throw new Error('Cannot deduce trade actions: not logged in')
32+
}
33+
34+
const getAndIncrement = (card_id: string, increment: number): CardAmountUpdate => {
35+
const internal_id = getInteralIdByCardId(card_id)
36+
return { card_id, internal_id, amount_owned: (ownedCards.get(internal_id)?.amount_owned ?? 0) + increment }
37+
}
38+
39+
const end = async () => {
40+
const update =
41+
trade.offering_friend_id === account.friend_id
42+
? { offerer_ended: true }
43+
: trade.receiving_friend_id === account.friend_id
44+
? { receiver_ended: true }
45+
: null
46+
if (update === null) {
47+
throw new Error(`Error hiding trade: cannot match your friend id`)
48+
}
49+
await updateTradeMutation.mutateAsync({ id: trade.id, trade: update })
50+
51+
setSelected(undefined)
52+
umami('Updated trade: ended')
53+
}
54+
55+
const increment = async () => {
56+
if (trade.offer_card_id === trade.receiver_card_id) {
57+
return
58+
}
59+
60+
const updates =
61+
trade.offering_friend_id === account.friend_id
62+
? [getAndIncrement(trade.offer_card_id, -1), getAndIncrement(trade.receiver_card_id, 1)]
63+
: trade.receiving_friend_id === account.friend_id
64+
? [getAndIncrement(trade.offer_card_id, 1), getAndIncrement(trade.receiver_card_id, -1)]
65+
: null
66+
if (updates === null) {
67+
throw new Error(`Error updating collection: cannot match your friend id`)
68+
}
69+
70+
await updateCardsMutation.mutateAsync(updates)
71+
await end()
72+
toast({ title: t('collectionUpdated'), variant: 'default' })
73+
}
74+
75+
const updateStatus = async (status: TradeStatus) => {
76+
updateTradeMutation.mutate({ id: trade.id, trade: { status: status } })
77+
setSelected(trade.id)
78+
umami(`Updated trade: ${status}`)
79+
}
80+
81+
const i_ended =
82+
(trade.offering_friend_id === account.friend_id && trade.offerer_ended) || (trade.receiving_friend_id === account.friend_id && trade.receiver_ended)
83+
84+
switch (trade.status) {
85+
case 'offered':
86+
return (
87+
<>
88+
{trade.receiving_friend_id === account.friend_id && <Button onClick={() => updateStatus('accepted')}>{t('actionAccept')}</Button>}
89+
<Button onClick={() => updateStatus('declined')}>{trade.receiving_friend_id === account.friend_id ? t('actionDecline') : t('actionCancel')}</Button>
90+
</>
91+
)
92+
case 'accepted':
93+
return (
94+
<>
95+
<Button onClick={() => updateStatus('finished')}>{t('actionComplete')}</Button>
96+
<Button onClick={() => updateStatus('declined')}>{t('actionCancel')}</Button>
97+
</>
98+
)
99+
case 'declined':
100+
if (i_ended) {
101+
return null
102+
}
103+
return <Button onClick={end}>{t('actionHide')}</Button>
104+
case 'finished':
105+
if (i_ended) {
106+
return null
107+
}
108+
return (
109+
<>
110+
<Button onClick={() => increment()}>{t('actionUpdate')}</Button>
111+
<Button onClick={end}>{t('actionHide')}</Button>
112+
</>
113+
)
114+
default:
115+
console.log(`Unknown trade status ${trade.status}`)
116+
return null
117+
}
118+
}

0 commit comments

Comments
 (0)