Skip to content

Commit

Permalink
Merge pull request #1279 from Coflnet/archived-auctions
Browse files Browse the repository at this point in the history
Archived auctions
  • Loading branch information
matthias-luger authored Jul 8, 2024
2 parents 2bc45e2 + 80ce33e commit e93e018
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 12 deletions.
38 changes: 37 additions & 1 deletion api/ApiHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getFlipCustomizeSettings } from '../utils/FlipUtils'
import { enchantmentAndReforgeCompare } from '../utils/Formatter'
import {
parseAccountInfo,
parseArchivedAuctions,
parseAuction,
parseAuctionDetails,
parseBazaarPrice,
Expand Down Expand Up @@ -2468,6 +2469,40 @@ export function initAPI(returnSSRResponse: boolean = false): API {
})
}

let requestArchivedAuctions = (itemTag: string, itemFilter?: ItemFilter): Promise<ArchivedAuctionResponse> => {
return new Promise((resolve, reject) => {
let googleId = sessionStorage.getItem('googleId')
if (!googleId) {
toast.error('You need to be logged in to request archived auctions.')
reject()
return
}

let params = new URLSearchParams()
if (itemFilter && Object.keys(itemFilter).length > 0) {
params = new URLSearchParams(itemFilter)
}

httpApi.sendApiRequest({
type: RequestType.ARCHIVED_AUCTIONS,
customRequestURL: `${getApiEndpoint()}/auctions/tag/${itemTag}/archive/overview?${params.toString()}`,
requestMethod: 'GET',
data: '',
requestHeader: {
GoogleToken: googleId,
'Content-Type': 'application/json'
},
resolve: result => {
resolve(parseArchivedAuctions(result))
},
reject: (error: any) => {
apiErrorHandler(RequestType.ARCHIVED_AUCTIONS, error, { itemTag, itemFilter })
reject(error)
}
})
})
}

return {
search,
trackSearch,
Expand Down Expand Up @@ -2557,7 +2592,8 @@ export function initAPI(returnSSRResponse: boolean = false): API {
deleteNotificationSubscription,
getNotificationSubscriptions,
getPublishedConfigs,
updateConfig
updateConfig,
requestArchivedAuctions
}
}

Expand Down
3 changes: 2 additions & 1 deletion api/ApiTypes.d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export enum RequestType {
ADD_NOTIFICATION_SUBSCRIPTION = 'addNotificationSubscription',
DELETE_NOTIFICATION_SUBSCRIPTION = 'deleteNotificationSubscription',
GET_PUBLISHED_CONFIGS = 'publishedConfigs',
UPDATE_CONFIG = 'updateConfig'
UPDATE_CONFIG = 'updateConfig',
ARCHIVED_AUCTIONS = 'archivedAuctions'
}

export enum SubscriptionType {
Expand Down
54 changes: 54 additions & 0 deletions app/item/[tag]/archive/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Container } from 'react-bootstrap'
import { parseItem } from '../../../../utils/Parser/APIResponseParser'
import Search from '../../../../components/Search/Search'
import { convertTagToName } from '../../../../utils/Formatter'
import api, { initAPI } from '../../../../api/ApiHelper'
import ArchivedAuctionsList from '../../../../components/ArchivedAuctions.tsx/ArchivedAuctions'
import { getHeadMetadata } from '../../../../utils/SSRUtils'
import { atobUnicode } from '../../../../utils/Base64Utils'

export default async function Page({ searchParams, params }) {
let tag = params.tag as string

let item = parseItem({
tag: tag,
name: convertTagToName(tag),
iconUrl: api.getItemImageUrl({ tag })
})

return (
<>
<Container>
<Search selected={item} type="item" />
<div style={{ paddingTop: '20px' }}>
<ArchivedAuctionsList item={item} />
</div>
</Container>
</>
)
}

export async function generateMetadata({ params, searchParams }) {
function getFiltersText(filter) {
if (!filter) {
return ' '
}
return `${Object.keys(filter)
.map(key => `➡️ ${key}: ${filter[key]}`)
.join('\n')}`
}

let tag = params?.tag as string
let api = initAPI(true)
let itemFilter = searchParams.filter ? JSON.parse(atobUnicode(searchParams.filter)) : null

let item = await api.getItemDetails(tag)

return getHeadMetadata(
`${item.name || convertTagToName(tag)} archived auctions`,
`${itemFilter ? `Filters: \n${getFiltersText(itemFilter)}` : ''}`,
item.iconUrl,
[item.name || convertTagToName(tag)],
`${item.name || convertTagToName(tag)} price | Hypixel SkyBlock AH history tracker`
)
}
37 changes: 37 additions & 0 deletions components/ArchivedAuctions.tsx/ArchivedAuctions.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.cardWrapper {
padding: 15px;
display: inline-block;
}

@media all and (max-width: 480px) {
.cardWrapper {
width: 100%;
}
.recentAuctionsFetchType {
width: 100px;
}
}

@media all and (min-width: 480px) and (max-width: 860px) {
.cardWrapper {
width: 50%;
}
}

@media all and (min-width: 860px) and (max-width: 1424px) {
.cardWrapper {
width: 25%;
}
}

@media all and (min-width: 1424px) {
.cardWrapper {
width: 16.66666666666666666666666666666%;
}
}

.datepickerContainer{
display: flex;
align-items: center;
justify-content: end;
}
233 changes: 233 additions & 0 deletions components/ArchivedAuctions.tsx/ArchivedAuctions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
'use client'
import React, { useRef, useState } from 'react'
import { Button, Card } from 'react-bootstrap'
import styles from './ArchivedAuctions.module.css'
import Link from 'next/link'
import Image from 'next/image'
import NumberElement from '../Number/Number'
import moment from 'moment'
import api from '../../api/ApiHelper'
import { getLoadingElement } from '../../utils/LoadingUtils'
import GoogleSignIn from '../GoogleSignIn/GoogleSignIn'
import { PREMIUM_RANK, hasHighEnoughPremium } from '../../utils/PremiumTypeUtils'
import DatePicker from 'react-datepicker'
import ItemFilter from '../ItemFilter/ItemFilter'
import Tooltip from '../Tooltip/Tooltip'
import { Help as HelpIcon } from '@mui/icons-material'
import InfiniteScroll from 'react-infinite-scroll-component'

interface Props {
item: Item
}

const ArchivedAuctionsList = (props: Props) => {
let [archivedAuctions, setArchivedAuctions] = useState<ArchivedAuction[]>([])
let [isLoggedIn, setIsLoggedIn] = useState(false)
let [premiumProducts, setPremiumProducts] = useState<PremiumProduct[]>([])
let [from, setFrom] = useState(new Date(Date.now() - 1000 * 60 * 60 * 24 * 1000))
let [to, setTo] = useState(new Date())
let [filters, setFilters] = useState<FilterOptions[]>([])
let [selectedFilter, setSelectedFilter] = useState<ItemFilter>()
let [isLoading, setIsLoading] = useState(false)
let [currentPage, setCurrentPage] = useState(0)
let [allElementsLoaded, setAllElementsLoaded] = useState(false)

let currentPageRef = useRef(currentPage)
currentPageRef.current = currentPage
let allElementsLoadedRef = useRef(allElementsLoaded)
allElementsLoadedRef.current = allElementsLoaded
let archivedAuctionsRef = useRef(archivedAuctions)
archivedAuctionsRef.current = archivedAuctions

function handleDateChange(date: Date, type: 'from' | 'to') {
if (type === 'from') {
setFrom(date)
} else if (type === 'to') {
setTo(date)
}
setArchivedAuctions([])
setCurrentPage(0)
setAllElementsLoaded(false)
}

function loadFilters(): Promise<FilterOptions[]> {
return Promise.all([api.getFilters(props.item?.tag || '*'), api.flipFilters(props.item?.tag || '*')]).then(filters => {
let result = [...(filters[0] || []), ...(filters[1] || [])]
return result
})
}

async function onAfterLogin() {
setIsLoggedIn(true)
try {
let [products, filters] = await Promise.all([api.getPremiumProducts(), loadFilters()])
setIsLoggedIn(true)
setPremiumProducts(products)
setFilters(filters)
} catch (e) {
setIsLoggedIn(false)
}
}

async function search(reset: boolean = false) {
if (isLoading) return
if (reset) {
setArchivedAuctions([])
setCurrentPage(0)
setAllElementsLoaded(false)
}

setIsLoading(true)
try {
let filter = (selectedFilter as any) || {}
const data = await api.requestArchivedAuctions(props.item.tag, {
...filter,
EndAfter: Math.floor(from.getTime() / 1000).toString(),
EndBefore: Math.floor(to.getTime() / 1000).toString(),
page: reset ? 0 : currentPageRef.current.toString()
})

if (data.queryStatus === 'Pending') {
setTimeout(() => {
search()
}, 1000)
return
}

let newAuctions = [...archivedAuctionsRef.current, ...data.auctions]
archivedAuctionsRef.current = newAuctions
setArchivedAuctions(newAuctions)
let newPage = currentPageRef.current + 1
currentPageRef.current = newPage
setCurrentPage(newPage)

if (newAuctions.length < 12) {
setAllElementsLoaded(true)
setIsLoading(false)
return
}

if (reset) {
search()
} else {
setIsLoading(false)
}
} catch {
setIsLoading(false)
}
}

if (!isLoggedIn || !hasHighEnoughPremium(premiumProducts, PREMIUM_RANK.PREMIUM_PLUS)) {
return (
<div>
<div>
<p>To see archived auctions, you need to sign in with Google and be a Premium+ user.</p>
</div>
{premiumProducts.length > 0 && !hasHighEnoughPremium(premiumProducts, PREMIUM_RANK.PREMIUM_PLUS) ? <p>You do not have Premium+.</p> : null}
<GoogleSignIn key="googleSignin" onAfterLogin={onAfterLogin} />
</div>
)
}

let archivedAuctionsList = archivedAuctions.map(auction => {
return (
<div key={auction.uuid} className={styles.cardWrapper}>
<span className="disableLinkStyle">
<Link href={`/auction/${auction.uuid}`} className="disableLinkStyle">
<Card className="card">
<Card.Header style={{ padding: '10px' }}>
<div style={{ float: 'left' }}>
<Image
crossOrigin="anonymous"
className="playerHeadIcon"
src={props.item.iconUrl || ''}
height="32"
width="32"
alt=""
style={{ marginRight: '5px' }}
loading="lazy"
/>
</div>
<div>
<NumberElement number={auction.price} /> Coins
</div>
</Card.Header>
<Card.Body style={{ padding: '10px' }}>
<Image
style={{ marginRight: '15px' }}
crossOrigin="anonymous"
className="playerHeadIcon"
src={auction.seller.iconUrl || ''}
alt=""
height="24"
width="24"
loading="lazy"
/>
<span>{auction.seller.name}</span>
<hr />
<p>{'ended ' + moment(auction.end).fromNow()}</p>
</Card.Body>
</Card>
</Link>
</span>
</div>
)
})

return (
<>
<h3 style={{ marginBottom: '15px' }}>
Archived Auctions{' '}
<Tooltip
type="hover"
content={<HelpIcon style={{ color: '#007bff', cursor: 'pointer' }} />}
tooltipContent={<span>Showing the player name takes additional processing time and therefore may add a bit of a delay for the flips.</span>}
/>
</h3>
<div className={styles.datepickerContainer}>
<label style={{ marginRight: 15 }}>From: </label>
<div style={{ paddingRight: 15 }}>
<DatePicker selected={from} onChange={date => handleDateChange(date, 'from')} className={'form-control'} />
</div>
<label style={{ marginRight: 15 }}>To: </label>
<DatePicker selected={to} onChange={date => handleDateChange(date, 'to')} className={'form-control'} />
</div>
<ItemFilter filters={filters} onFilterChange={filter => setSelectedFilter(filter)} />
<Button
onClick={() => {
search(true)
}}
>
Search
</Button>
<hr />
{isLoading && !archivedAuctions ? getLoadingElement(<p>Loading archived auctions...</p>) : null}
{archivedAuctions.length > 0 ? (
<>
<InfiniteScroll
loader={<div>{getLoadingElement(<p>Loading archived auctions...</p>)}</div>}
dataLength={archivedAuctions.length}
next={search}
hasMore={!allElementsLoaded}
>
{archivedAuctionsList}
</InfiniteScroll>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div>
<Button
onClick={() => {
search()
}}
>
Click here to manually load new data...
</Button>
</div>
</div>
</>
) : null}
<GoogleSignIn key="googleSignin" onAfterLogin={onAfterLogin} />
</>
)
}

export default ArchivedAuctionsList
Loading

0 comments on commit e93e018

Please sign in to comment.