From 174e7d34bef8c0f90e1cb80859bdb5a9ffb1b108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84kwav?= Date: Mon, 9 Mar 2026 14:31:45 +0100 Subject: [PATCH 1/5] Add bazaar export dialog --- api/_generated/skyApi.schemas.ts | 11 + api/_generated/skyApi.ts | 104 ++++-- .../BazaarExportModal.module.css | 71 ++++ .../BazaarExportModal/BazaarExportModal.tsx | 352 ++++++++++++++++++ components/OptionsMenu/OptionsMenu.tsx | 16 +- .../PremiumFeatures/PremiumFeatures.tsx | 18 + components/Search/Search.tsx | 2 +- 7 files changed, 538 insertions(+), 36 deletions(-) create mode 100644 components/BazaarExportModal/BazaarExportModal.module.css create mode 100644 components/BazaarExportModal/BazaarExportModal.tsx diff --git a/api/_generated/skyApi.schemas.ts b/api/_generated/skyApi.schemas.ts index b7aabada..fbcc3b47 100644 --- a/api/_generated/skyApi.schemas.ts +++ b/api/_generated/skyApi.schemas.ts @@ -578,6 +578,7 @@ export interface DescriptionSetting { replaceWhiteWith: string | null; noCookie: boolean; buyOrderPrices: boolean; + disableAuctionStartedTime: boolean; /** @nullable */ fields?: DescriptionField[][] | null; highlightInfo?: HighlightInfo; @@ -2844,6 +2845,16 @@ export type GetApiBazaarItemTagSnapshotParams = { timestamp?: string; }; +export type GetApiBazaarItemTagExportParams = { +start?: string; +end?: string; +fullOrderBook?: boolean; +}; + +export type GetApiBazaarItemTagExport401One = { + message?: string; +}; + export type GetApiCraftProfitParams = { player?: string; profile?: string; diff --git a/api/_generated/skyApi.ts b/api/_generated/skyApi.ts index d3b08a38..3abbc682 100644 --- a/api/_generated/skyApi.ts +++ b/api/_generated/skyApi.ts @@ -67,6 +67,8 @@ import type { GetApiAuctionsTagItemTagArchiveOverviewParams, GetApiAuctionsTagItemTagRecentOverviewParams, GetApiAuctionsTagItemTagSoldParams, + GetApiBazaarItemTagExport401One, + GetApiBazaarItemTagExportParams, GetApiBazaarItemTagHistoryParams, GetApiBazaarItemTagSnapshotParams, GetApiCraftProfitParams, @@ -2443,30 +2445,56 @@ export function useGetApiBazaarItemTagSnapshot { +export const getGetApiBazaarItemTagExportUrl = (itemTag: string, + params?: GetApiBazaarItemTagExportParams,) => { + const normalizedParams = new URLSearchParams(); + Object.entries(params || {}).forEach(([key, value]) => { + + if (value !== undefined) { + normalizedParams.append(key, value === null ? 'null' : value.toString()) + } + }); - + const stringifiedParams = normalizedParams.toString(); - return `https://sky.coflnet.com/api/bazaar/player/${playerId}/orders` + return stringifiedParams.length > 0 ? `https://sky.coflnet.com/api/bazaar/${itemTag}/export?${stringifiedParams}` : `https://sky.coflnet.com/api/bazaar/${itemTag}/export` } -export const getApiBazaarPlayerPlayerIdOrders = async (playerId: string, options?: RequestInit): Promise => { +export const getApiBazaarItemTagExport = async (itemTag: string, + params?: GetApiBazaarItemTagExportParams, options?: RequestInit): Promise => { - const res = await fetch(getGetApiBazaarPlayerPlayerIdOrdersUrl(playerId), + const res = await fetch(getGetApiBazaarItemTagExportUrl(itemTag,params), { ...options, method: 'GET' @@ -2476,74 +2504,82 @@ export const getApiBazaarPlayerPlayerIdOrders = async (playerId: string, options ) const body = [204, 205, 304].includes(res.status) ? null : await res.text() - const data: getApiBazaarPlayerPlayerIdOrdersResponse['data'] = body ? JSON.parse(body) : {} + const data: getApiBazaarItemTagExportResponse['data'] = body ? JSON.parse(body) : {} - return { data, status: res.status, headers: res.headers } as getApiBazaarPlayerPlayerIdOrdersResponse + return { data, status: res.status, headers: res.headers } as getApiBazaarItemTagExportResponse } -export const getGetApiBazaarPlayerPlayerIdOrdersQueryKey = (playerId?: string,) => { - return [`https://sky.coflnet.com/api/bazaar/player/${playerId}/orders`] as const; +export const getGetApiBazaarItemTagExportQueryKey = (itemTag?: string, + params?: GetApiBazaarItemTagExportParams,) => { + return [`https://sky.coflnet.com/api/bazaar/${itemTag}/export`, ...(params ? [params]: [])] as const; } -export const getGetApiBazaarPlayerPlayerIdOrdersQueryOptions = >, TError = unknown>(playerId: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +export const getGetApiBazaarItemTagExportQueryOptions = >, TError = GetApiBazaarItemTagExport401One | string | null>(itemTag: string, + params?: GetApiBazaarItemTagExportParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} ) => { const {query: queryOptions, fetch: fetchOptions} = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getGetApiBazaarPlayerPlayerIdOrdersQueryKey(playerId); + const queryKey = queryOptions?.queryKey ?? getGetApiBazaarItemTagExportQueryKey(itemTag,params); - const queryFn: QueryFunction>> = ({ signal }) => getApiBazaarPlayerPlayerIdOrders(playerId, { signal, ...fetchOptions }); + const queryFn: QueryFunction>> = ({ signal }) => getApiBazaarItemTagExport(itemTag,params, { signal, ...fetchOptions }); - return { queryKey, queryFn, enabled: !!(playerId), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } + return { queryKey, queryFn, enabled: !!(itemTag), ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } } -export type GetApiBazaarPlayerPlayerIdOrdersQueryResult = NonNullable>> -export type GetApiBazaarPlayerPlayerIdOrdersQueryError = unknown +export type GetApiBazaarItemTagExportQueryResult = NonNullable>> +export type GetApiBazaarItemTagExportQueryError = GetApiBazaarItemTagExport401One | string | null -export function useGetApiBazaarPlayerPlayerIdOrders>, TError = unknown>( - playerId: string, options: { query:Partial>, TError, TData>> & Pick< +export function useGetApiBazaarItemTagExport>, TError = GetApiBazaarItemTagExport401One | string | null>( + itemTag: string, + params: undefined | GetApiBazaarItemTagExportParams, options: { query:Partial>, TError, TData>> & Pick< DefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> > , 'initialData' >, fetch?: RequestInit} , queryClient?: QueryClient ): DefinedUseQueryResult & { queryKey: DataTag } -export function useGetApiBazaarPlayerPlayerIdOrders>, TError = unknown>( - playerId: string, options?: { query?:Partial>, TError, TData>> & Pick< +export function useGetApiBazaarItemTagExport>, TError = GetApiBazaarItemTagExport401One | string | null>( + itemTag: string, + params?: GetApiBazaarItemTagExportParams, options?: { query?:Partial>, TError, TData>> & Pick< UndefinedInitialDataOptions< - Awaited>, + Awaited>, TError, - Awaited> + Awaited> > , 'initialData' >, fetch?: RequestInit} , queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } -export function useGetApiBazaarPlayerPlayerIdOrders>, TError = unknown>( - playerId: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +export function useGetApiBazaarItemTagExport>, TError = GetApiBazaarItemTagExport401One | string | null>( + itemTag: string, + params?: GetApiBazaarItemTagExportParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} , queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } /** - * @deprecated + * @summary Exports detailed item data for a specific item, if there is no start/end specified it will return a compressed file with 20sec increments of the last 2 weeks +For longer timeframes we only keep 5min increments and return those, optionally with full orderbook for each point. +Note that this endpoint requires a google id token of an account with prem+ and is subject to strict non distribute and non profit license terms */ -export function useGetApiBazaarPlayerPlayerIdOrders>, TError = unknown>( - playerId: string, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} +export function useGetApiBazaarItemTagExport>, TError = GetApiBazaarItemTagExport401One | string | null>( + itemTag: string, + params?: GetApiBazaarItemTagExportParams, options?: { query?:Partial>, TError, TData>>, fetch?: RequestInit} , queryClient?: QueryClient ): UseQueryResult & { queryKey: DataTag } { - const queryOptions = getGetApiBazaarPlayerPlayerIdOrdersQueryOptions(playerId,options) + const queryOptions = getGetApiBazaarItemTagExportQueryOptions(itemTag,params,options) const query = useQuery(queryOptions , queryClient) as UseQueryResult & { queryKey: DataTag }; diff --git a/components/BazaarExportModal/BazaarExportModal.module.css b/components/BazaarExportModal/BazaarExportModal.module.css new file mode 100644 index 00000000..7c9c82ca --- /dev/null +++ b/components/BazaarExportModal/BazaarExportModal.module.css @@ -0,0 +1,71 @@ +.licenseHeader { + display: flex; + align-items: center; + cursor: pointer; + gap: 8px; + font-weight: bold; + margin-bottom: 5px; + user-select: none; +} + +.licenseHeader:hover { + color: #007bff; +} + +.licenseContent { + padding: 10px; + border: 1px solid #444; + border-radius: 4px; + background-color: rgba(0, 0, 0, 0.2); + font-size: 0.85rem; + margin-bottom: 15px; +} + +.tierInfo { + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; +} + +.premiumBadge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.85rem; + font-weight: bold; +} + +.formGroup { + margin-bottom: 15px; +} + +.label { + font-weight: bold; + min-width: 200px; + display: inline-block; +} + +.noteText { + font-size: 0.85rem; + color: #aaa; + margin-top: 5px; +} + +.checkboxContainer { + margin-bottom: 15px; +} + +.checkboxContainer :global(.form-check) { + pointer-events: auto; +} + +.checkboxContainer :global(.form-check-input) { + cursor: pointer; + pointer-events: auto; +} + +.checkboxContainer :global(.form-check-label) { + cursor: pointer; + pointer-events: auto; + user-select: none; +} diff --git a/components/BazaarExportModal/BazaarExportModal.tsx b/components/BazaarExportModal/BazaarExportModal.tsx new file mode 100644 index 00000000..beec11fc --- /dev/null +++ b/components/BazaarExportModal/BazaarExportModal.tsx @@ -0,0 +1,352 @@ +'use client' +import { useState } from 'react' +import { Alert, Button, Form, Modal } from 'react-bootstrap' +import api from '../../api/ApiHelper' +import { hasHighEnoughPremium, PREMIUM_RANK } from '../../utils/PremiumTypeUtils' +import styles from './BazaarExportModal.module.css' +import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' + +interface Props { + show: boolean + onHide: () => void + itemTag: string +} + +function BazaarExportModal(props: Props) { + let [fullOrderBook, setFullOrderBook] = useState(false) + let [startDate, setStartDate] = useState('') + let [endDate, setEndDate] = useState('') + let [hasPremium, setHasPremium] = useState(false) + let [hasPremiumPlus, setHasPremiumPlus] = useState(false) + let [isExporting, setIsExporting] = useState(false) + + let [isLoggedIn, setIsLoggedIn] = useState(false) + let [hasLoadedPremium, setHasLoadedPremium] = useState(false) + let [premiumLoadError, setPremiumLoadError] = useState(false) + + function loadPremiumProducts() { + setPremiumLoadError(false) + setHasLoadedPremium(false) + + api.refreshLoadPremiumProducts( + products => { + const hasPrem = hasHighEnoughPremium(products, PREMIUM_RANK.PREMIUM) + const hasPremPlus = hasHighEnoughPremium(products, PREMIUM_RANK.PREMIUM_PLUS) + setHasPremium(hasPrem) + setHasPremiumPlus(hasPremPlus) + setHasLoadedPremium(true) + }, + () => { + setPremiumLoadError(true) + setHasPremium(false) + setHasPremiumPlus(false) + setHasLoadedPremium(true) + } + ) + } + + function handleEnter() { + let loggedIn = !!(typeof window !== 'undefined' && (sessionStorage.getItem('googleId') || localStorage.getItem('googleId'))) + setIsLoggedIn(loggedIn) + + if (!loggedIn) { + setHasLoadedPremium(true) + return + } + + loadPremiumProducts() + } + + function onLogin() { + setIsLoggedIn(true) + handleEnter() + } + + function handleModalHide() { + // Reset form state when modal closes + setFullOrderBook(false) + setStartDate('') + setEndDate('') + setIsExporting(false) + props.onHide() + } + + function getMaxHistoryLabel(): string { + if (hasPremiumPlus) return '~6 years' + if (hasPremium) return '180 days' + return 'None' + } + + function getTierLabel(): string { + if (hasPremiumPlus) return 'Premium+' + if (hasPremium) return 'Premium' + return 'Free' + } + + function getTierVariant(): string { + if (hasPremiumPlus) return 'warning' + if (hasPremium) return 'success' + return 'secondary' + } + + function getTodayDate(): string { + const today = new Date() + return today.toISOString().split('T')[0] + } + + function isDateRangeExceedsYear(): boolean { + if (!startDate || !endDate) return false + const start = new Date(startDate) + const end = new Date(endDate) + const diffTime = Math.abs(end.getTime() - start.getTime()) + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + return diffDays > 400 + } + + function isStartDateExceeds180Days(): boolean { + if (!startDate) return false + const start = new Date(startDate) + const today = new Date() + const diffTime = Math.abs(today.getTime() - start.getTime()) + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + return diffDays > 180 + } + + function startExport() { + if (!isLoggedIn || (!hasPremium && !hasPremiumPlus)) { + // Need premium + return + } + + setIsExporting(true) + + const googleToken = sessionStorage.getItem('googleId') ?? localStorage.getItem('googleId') ?? '' + + const params = new URLSearchParams() + params.append('fullOrderBook', fullOrderBook.toString()) + if (startDate) { + params.append('start', new Date(startDate).toISOString()) + } + if (endDate) { + params.append('end', new Date(endDate).toISOString()) + } + + const url = `https://sky.coflnet.com/api/bazaar/${props.itemTag}/export?${params.toString()}` + + fetch(url, { + method: 'GET', + headers: { + GoogleToken: googleToken + } + }) + .then(async response => { + if (!response.ok) { + const text = await response.text() + throw new Error(text || `Export failed with status ${response.status}`) + } + return response.blob() + }) + .then(blob => { + const downloadUrl = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = downloadUrl + a.download = `bazaar-${props.itemTag}-export.zip` + document.body.appendChild(a) + a.click() + a.remove() + window.URL.revokeObjectURL(downloadUrl) + props.onHide() + }) + .catch(err => { + alert('Export failed: ' + err.message) + }) + .finally(() => { + setIsExporting(false) + }) + } + + return ( + + + Export Bazaar Data + + + {!isLoggedIn ? ( +
+

You need to be logged in to export bazaar data.

+ +
+ ) : !hasLoadedPremium ? ( +
Loading...
+ ) : ( + <> + {premiumLoadError && ( + +

Failed to load your premium status. Please try again.

+ +
+ )} + + {/* Tier info */} + + Your current tier: {getTierLabel()} — Max export history: {getMaxHistoryLabel()} + + + {!hasPremium && !hasPremiumPlus && ( + + Bazaar data export requires at least Premium. Exports of the last 180 days are available with + Premium and the last ~6 years are available with Premium+. +
+ +
+
+ )} + + {hasPremium && !hasPremiumPlus && ( + + With Premium you can export up to the last 180 days of data. Upgrade to Premium+ for up to{' '} + ~6 years of history. +
+ +
+
+ )} + + {hasPremiumPlus && ( + + With Premium+ you can export up to the last ~6 years of bazaar data. + + )} + +

+ Note: If no date range is specified, the export will contain the last 2 weeks of data in 20-second increments. The + export will download as a zip file containing JSON. +

+ +
+ + {/* Options */} +
+ + + Include full order book + + setFullOrderBook(event.target.checked)} + defaultChecked={fullOrderBook} + id="fullOrderBook" + style={{ display: 'inline' }} + type="checkbox" + /> + +
+ + {(hasPremium || hasPremiumPlus) && ( + <> +
+ + Start Date (optional) + setStartDate(e.target.value)} + min="2020-03-10" + /> + +
+ +
+ + End Date (optional) + setEndDate(e.target.value)} + max={getTodayDate()} + /> + +
+ + {isDateRangeExceedsYear() && ( + + Note: Exporting more than a year at once can cause issues. We recommend keeping your time frame to about a year or less for best results. + + )} + + {isStartDateExceeds180Days() && hasPremium && !hasPremiumPlus && ( + + Note: The start date you selected is more than 180 days in the past. Premium+ is required to export data older than 180 days. Please upgrade to Premium+ or select a more recent start date. +
+ +
+
+ )} + +
+ + {/* License Disclaimer */} +
+

License & Data Usage Agreement

+

By downloading this data you agree to the following terms:

+
    +
  • + Personal use only: The exported data is for personal use only and may not be redistributed, published, or + shared with third parties. +
  • +
  • + Data deletion: All exported data must be deleted when your premium tier expires or your subscription ends. +
  • +
  • + Non-compete: You agree not to offer any competing services based on the exported data. +
  • +
  • + Share findings: You agree to share any findings, analyses, or insights derived from the data with Coflnet. +
  • +
+
+ + )} + + )} +
+ + {!isLoggedIn || (!hasPremium && !hasPremiumPlus) ? ( +
+ {!isLoggedIn && 'Please log in to export bazaar data.'} + {isLoggedIn && !hasPremium && !hasPremiumPlus && premiumLoadError && ( + 'Unable to verify premium status. Please retry.' + )} + {isLoggedIn && !hasPremium && !hasPremiumPlus && !premiumLoadError && ( + 'Premium subscription required to export bazaar data. Click "Get Premium" above to upgrade.' + )} +
+ ) : ( +
+ By clicking Download, you accept the license agreement above. +
+ )} + + +
+
+ ) +} + +export default BazaarExportModal diff --git a/components/OptionsMenu/OptionsMenu.tsx b/components/OptionsMenu/OptionsMenu.tsx index 0b4be8fc..cc4cc5fa 100644 --- a/components/OptionsMenu/OptionsMenu.tsx +++ b/components/OptionsMenu/OptionsMenu.tsx @@ -1,8 +1,9 @@ 'use client' -import React from 'react' +import React, { useState } from 'react' import MoreVertIcon from '@mui/icons-material/MoreVert' import styles from './OptionsMenu.module.css' import { Button, Dropdown, DropdownButton } from 'react-bootstrap' +import BazaarExportModal from '../BazaarExportModal/BazaarExportModal' interface Props { selected?: Player | Item @@ -31,6 +32,7 @@ CustomToggle.displayName = 'CustomToggle' function OptionsMenu(props: Props) { let available: AvailableLinks[] = [] + let [showExportModal, setShowExportModal] = useState(false) const isItemPage = (props.selected as Item)?.tag !== undefined const isPlayerPage = !isItemPage if (isItemPage) { @@ -84,6 +86,11 @@ function OptionsMenu(props: Props) { ))} + {isItemPage && (props.selected as Item).bazaar && ( + + )} @@ -99,8 +106,15 @@ function OptionsMenu(props: Props) { {result.title} ))} + {isItemPage && (props.selected as Item).bazaar && ( + setShowExportModal(true)}>Export + )} + + {isItemPage && (props.selected as Item).bazaar && showExportModal && ( + setShowExportModal(false)} itemTag={(props.selected as Item).tag} /> + )} ) } diff --git a/components/Premium/PremiumFeatures/PremiumFeatures.tsx b/components/Premium/PremiumFeatures/PremiumFeatures.tsx index f0bb1e74..0957437f 100644 --- a/components/Premium/PremiumFeatures/PremiumFeatures.tsx +++ b/components/Premium/PremiumFeatures/PremiumFeatures.tsx @@ -325,6 +325,24 @@ function PremiumFeatures() { {xIconElement} {checkIconElement} + + + Bazaar data export (JSON) + + + + } + type="hover" + tooltipContent={

Export bazaar price history as a zip file containing JSON. Optionally includes full order book.

} + /> + + {xIconElement} + {xIconElement} + 180 days + ~6 years + Access to BazaarPro (pro.skyblock.bz) diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index 4f0e02af..c3428679 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -56,7 +56,7 @@ function Search(props: Props) { let [results, setResults] = useState([]) let [isSearching, setIsSearching] = useState(false) let [noResultsFound, setNoResultsFound] = useState(false) - let [isSmall, setIsSmall] = useState(true) + let [isSmall, setIsSmall] = useState(false) let [selectedIndex, setSelectedIndex] = useState(0) const { show: showPlayerContextMenu } = useContextMenu({ id: PLAYER_SEARCH_CONEXT_MENU_ID From f52776dae4649b650634351018f86febe83e16d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84kwav?= Date: Tue, 10 Mar 2026 13:08:23 +0100 Subject: [PATCH 2/5] move bazaar item check --- components/OptionsMenu/OptionsMenu.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/OptionsMenu/OptionsMenu.tsx b/components/OptionsMenu/OptionsMenu.tsx index cc4cc5fa..32afa6e7 100644 --- a/components/OptionsMenu/OptionsMenu.tsx +++ b/components/OptionsMenu/OptionsMenu.tsx @@ -35,6 +35,7 @@ function OptionsMenu(props: Props) { let [showExportModal, setShowExportModal] = useState(false) const isItemPage = (props.selected as Item)?.tag !== undefined const isPlayerPage = !isItemPage + const isBazaarItem = isItemPage && (props.selected as Item).bazaar if (isItemPage) { let tag = (props.selected as Item).tag let fandomName = (props.selected as Item).name ?? tag @@ -86,7 +87,7 @@ function OptionsMenu(props: Props) { ))} - {isItemPage && (props.selected as Item).bazaar && ( + {isBazaarItem && ( @@ -106,13 +107,13 @@ function OptionsMenu(props: Props) { {result.title} ))} - {isItemPage && (props.selected as Item).bazaar && ( + {isBazaarItem && ( setShowExportModal(true)}>Export )} - {isItemPage && (props.selected as Item).bazaar && showExportModal && ( + {isBazaarItem && showExportModal && ( setShowExportModal(false)} itemTag={(props.selected as Item).tag} /> )} From df6419384a96f8b8a65a8d9b26aa39c35ff9c8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84kwav?= Date: Tue, 10 Mar 2026 13:18:59 +0100 Subject: [PATCH 3/5] implement review points --- .../BazaarExportModal.module.css | 25 +++++++++ .../BazaarExportModal/BazaarExportModal.tsx | 56 +++++++++---------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/components/BazaarExportModal/BazaarExportModal.module.css b/components/BazaarExportModal/BazaarExportModal.module.css index 7c9c82ca..1fe62756 100644 --- a/components/BazaarExportModal/BazaarExportModal.module.css +++ b/components/BazaarExportModal/BazaarExportModal.module.css @@ -69,3 +69,28 @@ pointer-events: auto; user-select: none; } + +.centerPadded { + text-align: center; + padding: 20px; +} + +.actionButtonContainer { + margin-top: 10px; +} + +.inlineElement { + display: inline; +} + +.modalFooter { + flex-wrap: wrap; + gap: 10px; +} + +.footerNote { + width: 100%; + margin-bottom: 10px; + font-size: 0.85rem; + color: #aaa; +} diff --git a/components/BazaarExportModal/BazaarExportModal.tsx b/components/BazaarExportModal/BazaarExportModal.tsx index beec11fc..ebcdce44 100644 --- a/components/BazaarExportModal/BazaarExportModal.tsx +++ b/components/BazaarExportModal/BazaarExportModal.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { Alert, Button, Form, Modal } from 'react-bootstrap' import api from '../../api/ApiHelper' import { hasHighEnoughPremium, PREMIUM_RANK } from '../../utils/PremiumTypeUtils' +import { getGetApiBazaarItemTagExportUrl } from '../../api/_generated/skyApi' import styles from './BazaarExportModal.module.css' import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' @@ -45,7 +46,7 @@ function BazaarExportModal(props: Props) { ) } - function handleEnter() { + function onModalEntered() { let loggedIn = !!(typeof window !== 'undefined' && (sessionStorage.getItem('googleId') || localStorage.getItem('googleId'))) setIsLoggedIn(loggedIn) @@ -59,15 +60,10 @@ function BazaarExportModal(props: Props) { function onLogin() { setIsLoggedIn(true) - handleEnter() + onModalEntered() } function handleModalHide() { - // Reset form state when modal closes - setFullOrderBook(false) - setStartDate('') - setEndDate('') - setIsExporting(false) props.onHide() } @@ -100,6 +96,7 @@ function BazaarExportModal(props: Props) { const end = new Date(endDate) const diffTime = Math.abs(end.getTime() - start.getTime()) const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + // 400 days is just a nice approximate for "more than a year" to warn about slow exports return diffDays > 400 } @@ -122,16 +119,13 @@ function BazaarExportModal(props: Props) { const googleToken = sessionStorage.getItem('googleId') ?? localStorage.getItem('googleId') ?? '' - const params = new URLSearchParams() - params.append('fullOrderBook', fullOrderBook.toString()) - if (startDate) { - params.append('start', new Date(startDate).toISOString()) - } - if (endDate) { - params.append('end', new Date(endDate).toISOString()) - } - - const url = `https://sky.coflnet.com/api/bazaar/${props.itemTag}/export?${params.toString()}` + // Using manual fetch instead of generated Tanstack query (e.g. useGetApiBazaarItemTagExport) + // because the generated API attempts to parse the response as JSON, but this endpoint returns a binary ZIP blob. + const url = getGetApiBazaarItemTagExportUrl(props.itemTag, { + fullOrderBook: fullOrderBook, + start: startDate ? new Date(startDate).toISOString() : undefined, + end: endDate ? new Date(endDate).toISOString() : undefined + }) fetch(url, { method: 'GET', @@ -166,18 +160,18 @@ function BazaarExportModal(props: Props) { } return ( - + Export Bazaar Data {!isLoggedIn ? ( -
+

You need to be logged in to export bazaar data.

) : !hasLoadedPremium ? ( -
Loading...
+
Loading...
) : ( <> {premiumLoadError && ( @@ -198,8 +192,8 @@ function BazaarExportModal(props: Props) { Bazaar data export requires at least Premium. Exports of the last 180 days are available with Premium and the last ~6 years are available with Premium+. -
-
@@ -210,8 +204,8 @@ function BazaarExportModal(props: Props) { With Premium you can export up to the last 180 days of data. Upgrade to Premium+ for up to{' '} ~6 years of history. -
-
@@ -241,7 +235,7 @@ function BazaarExportModal(props: Props) { onChange={event => setFullOrderBook(event.target.checked)} defaultChecked={fullOrderBook} id="fullOrderBook" - style={{ display: 'inline' }} + className={styles.inlineElement} type="checkbox" /> @@ -282,8 +276,8 @@ function BazaarExportModal(props: Props) { {isStartDateExceeds180Days() && hasPremium && !hasPremiumPlus && ( Note: The start date you selected is more than 180 days in the past. Premium+ is required to export data older than 180 days. Please upgrade to Premium+ or select a more recent start date. -
-
@@ -317,9 +311,9 @@ function BazaarExportModal(props: Props) { )} - + {!isLoggedIn || (!hasPremium && !hasPremiumPlus) ? ( -
+
{!isLoggedIn && 'Please log in to export bazaar data.'} {isLoggedIn && !hasPremium && !hasPremiumPlus && premiumLoadError && ( 'Unable to verify premium status. Please retry.' @@ -329,11 +323,11 @@ function BazaarExportModal(props: Props) { )}
) : ( -
+
By clicking Download, you accept the license agreement above.
)} -