Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wow Shopping List #288

Merged
merged 4 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion app/components/navigation/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
ChevronDownIcon,
PencilAltIcon,
SearchIcon,
ExclamationCircleIcon
ExclamationCircleIcon,
ShoppingCartIcon
} from '@heroicons/react/outline'
import {
Form,
Expand Down Expand Up @@ -238,6 +239,11 @@ const navGroups: Array<{
name: 'Best Deals',
href: '/wow/best-deals',
icon: ExclamationCircleIcon
},
{
name: 'Shopping List',
href: '/wow/shopping-list',
icon: ShoppingCartIcon
}
]
},
Expand Down
45 changes: 45 additions & 0 deletions app/requests/WoW/ShoppingList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { address, UserAgent } from '~/requests/client/config'
import type { WoWServerRegion } from '../WOWScan'

interface WoWShoppingListProps {
region: WoWServerRegion
itemID: number
maxPurchasePrice: number
}

export interface ListItem {
link: string
price: number
quantity: number
realmID: number
realmName: string
realmNames: string
}

export interface WoWListResponse {
name: string
data: Array<ListItem>
}

const WoWShoppingList = async ({
region,
itemID,
maxPurchasePrice
}: WoWShoppingListProps) => {
return fetch(`${address}/api/wow/shoppinglist`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': UserAgent
},
// send a JSON with salesPerDay as a float
body: JSON.stringify({
region,
itemID,
maxPurchasePrice,
connectedRealmIDs: {}
})
})
}

export default WoWShoppingList
6 changes: 6 additions & 0 deletions app/routes/wow._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ const recommendedQueries = [
'Find the best deals on your server and region wide with our Best Deals search!',
Icon: DocumentSearchIcon,
href: '/wow/best-deals'
},
{
name: 'Shopping List',
description: 'Search for the realms with the lowest price for an item.',
Icon: DocumentSearchIcon,
href: '/wow/shopping-list'
}
]

Expand Down
182 changes: 182 additions & 0 deletions app/routes/wow.shopping-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import type { ActionFunction } from '@remix-run/cloudflare'
import { json } from '@remix-run/cloudflare'
import { useEffect, useMemo, useState } from 'react'
import { PageWrapper } from '~/components/Common'
import SmallFormContainer from '~/components/form/SmallFormContainer'
import type { ListItem, WoWListResponse } from '~/requests/WoW/ShoppingList'
import WoWShoppingList from '~/requests/WoW/ShoppingList'
import { getUserSessionData } from '~/sessions'
import z from 'zod'
import { useActionData, useNavigation } from '@remix-run/react'
import { InputWithLabel } from '~/components/form/InputWithLabel'
import NoResults from '~/components/Common/NoResults'
import SmallTable from '~/components/WoWResults/FullScan/SmallTable'
import type { ColumnList } from '~/components/types'
import ExternalLink from '~/components/utilities/ExternalLink'
import DebouncedSelectInput from '~/components/Common/DebouncedSelectInput'
import { parseItemsForDataListSelect } from '~/utils/items/id_to_item'
import { useTypedSelector } from '~/redux/useTypedSelector'
import { getItemIDByName } from '~/utils/items'

const parseNumber = z.string().transform((value) => parseInt(value, 10))

const validateInput = z.object({
itemID: parseNumber,
maxPurchasePrice: parseNumber
})

export const action: ActionFunction = async ({ request }) => {
const session = await getUserSessionData(request)

const region = session.getWoWSessionData().region

const formData = Object.fromEntries(await request.formData())

const validatedFormData = validateInput.safeParse(formData)
if (!validatedFormData.success) {
return json({ exception: 'Invalid Input' })
}

const result = await WoWShoppingList({
region,
...validatedFormData.data
})

// await the result and then return the json

return json({
...(await result.json()),
sortby: 'discount'
})
}

type ActionResponseType =
| {}
| { exception: string }
| (WoWListResponse & { sortby: string })

const ShoppingList = () => {
const result = useActionData<ActionResponseType>()
const transistion = useNavigation()
const { wowItems } = useTypedSelector((state) => state.user)
const [itemName, setItemName] = useState<string>('')

const isSubmitting = transistion.state === 'submitting'

const handleSelect = (value: string) => {
setItemName(value)
}

const memoItems = useMemo(
() => wowItems.map(parseItemsForDataListSelect),
[wowItems]
)

const itemId = getItemIDByName(itemName.trim(), wowItems)
const error = result && 'exception' in result ? result.exception : undefined

if (result && !Object.keys(result).length) {
return <NoResults href="/wow/shopping-list" />
}

if (result && 'data' in result && !error) {
return <Results {...result} />
}

const handleSubmit = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
if (isSubmitting) {
event.preventDefault()
}
}

return (
<PageWrapper>
<SmallFormContainer
title="Shopping List"
description="Search for the realms with the lowest price for an item."
onClick={handleSubmit}
error={error}
loading={isSubmitting}>
<div className="pt-3 flex flex-col">
<DebouncedSelectInput
title={'Item to search for'}
label="Item"
id="export-item-select"
selectOptions={memoItems}
onSelect={handleSelect}
/>
<input hidden name="itemID" value={itemId} />
<InputWithLabel
labelTitle="Maximum Purchase Price"
name="maxPurchasePrice"
type="number"
defaultValue={10000000}
min={0}
/>
</div>
</SmallFormContainer>
</PageWrapper>
)
}

export default ShoppingList

const Results = ({
data,
sortby,
name
}: WoWListResponse & { sortby: string }) => {
useEffect(() => {
if (window && document) {
window.scroll({ top: 0, behavior: 'smooth' })
}
}, [])
return (
<PageWrapper>
<SmallTable
title={'Best Deals for ' + name}
sortingOrder={[{ desc: true, id: sortby }]}
columnList={columnList}
mobileColumnList={mobileColumnList}
columnSelectOptions={['price', 'quantity', 'realmNames', 'link']}
data={data}
/>
</PageWrapper>
)
}

const columnList: Array<ColumnList<ListItem>> = [
{ columnId: 'price', header: 'Price' },
{ columnId: 'quantity', header: 'Quantity' },
{
columnId: 'realmNames',
header: 'Realm Names',
accessor: ({ getValue }) => (
<p className="py-2 px-3 max-w-[400px] mx-auto overflow-x-scroll">
{getValue() as string}
</p>
)
},
{
columnId: 'link',
header: 'Item Link',
accessor: ({ getValue }) => (
<ExternalLink link={getValue() as string} text="Undermine" />
)
}
]

const mobileColumnList: Array<ColumnList<ListItem>> = [
{ columnId: 'price', header: 'Price' },
{
columnId: 'realmNames',
header: 'Realm Names',
accessor: ({ getValue }) => (
<p className="py-2 px-3 w-[200px] overflow-x-scroll">
{getValue() as string}
</p>
)
}
]
4 changes: 4 additions & 0 deletions app/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,10 @@ select {
max-width: 200px;
}

.max-w-\[400px\] {
max-width: 400px;
}

.max-w-fit {
max-width: -moz-fit-content;
max-width: fit-content;
Expand Down
Loading