diff --git a/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.module.css b/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.module.css index b09bd08e..c73836eb 100644 --- a/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.module.css +++ b/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.module.css @@ -37,4 +37,11 @@ .chartWrapper :global(.priceGraphTooltip) { z-index: 18 !important -} \ No newline at end of file +} + +.dateRangePanel { + margin-top: 20px; + padding: 15px; + background-color: var(--bs-secondary); + border-radius: 5px; +} diff --git a/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.tsx b/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.tsx index 050f3b4a..1b545ebe 100644 --- a/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.tsx +++ b/components/PriceGraph/AuctionHousePriceGraph/AuctionHousePriceGraph.tsx @@ -8,7 +8,7 @@ import { getLoadingElement } from '../../../utils/LoadingUtils' import { AUCTION_GRAPH_LEGEND_SELECTION } from '../../../utils/SettingsUtils' import ActiveAuctions from '../../ActiveAuctions/ActiveAuctions' import ItemFilter, { getPrefillFilter } from '../../ItemFilter/ItemFilter' -import { DateRange, DEFAULT_DATE_RANGE, ItemPriceRange } from '../../ItemPriceRange/ItemPriceRange' +import { DateRange, ItemPriceRange } from '../../ItemPriceRange/ItemPriceRange' import { useValidRange } from '../../../hooks/useValidRange' import Number from '../../Number/Number' import GoogleSignIn from '../../GoogleSignIn/GoogleSignIn' @@ -17,11 +17,12 @@ import RelatedItems from '../../RelatedItems/RelatedItems' import ShareButton from '../../ShareButton/ShareButton' import SubscribeButton from '../../SubscribeButton/SubscribeButton' import QuickDateSelect from './QuickDateSelect' +import { buildYearHistoryFilter, type CustomDateRange } from './YearDateRangeUtils' import styles from './AuctionHousePriceGraph.module.css' import graphConfig from './PriceGraphConfig' import { applyMayorDataToChart } from '../../../utils/GraphUtils' import EChartsReact from 'echarts-for-react' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useGetApiItemPriceItemTagHistoryYear } from '../../../api/_generated/skyApi' import Link from 'next/link' import { v4 as generateUUID } from 'uuid' @@ -31,6 +32,16 @@ import { hasHighEnoughPremium, PREMIUM_RANK } from '../../../utils/PremiumTypeUt import NitroAdSlot from '../../Ads/NitroAdSlot' const HOUR_IN_MS = 60 * 60 * 1000 +const YEAR_LOADING_MESSAGES = [ + 'Loading year history data... This might take a while! 📊', + 'Crunching numbers from the past year... Stay tuned! 🔢', + "Fetching auction data... It's a lot of information! 📈", + 'Almost there! Processing historical market trends... 📋', + 'Loading complete market analysis... Worth the wait! 🎯', + 'Gathering seller and buyer statistics... Hang tight! 👥', + "Fun fact: We're processing millions of auction records! 🎪", + 'Did you know? Year data includes every single transaction! 💰' +] const normaliseMayorRange = (start: Date, end: Date) => { let from = new Date(start.getTime()) @@ -57,6 +68,12 @@ type YearStatistics = PriceStatistics & { recentAuctions?: AuctionPreview[] } +interface YearRequest { + id: number + params?: ItemFilter + fetchOptions: RequestInit +} + // Base empty statistics object used for premium/preview placeholders. // Created once so identical objects are not duplicated inline. const EMPTY_YEAR_STATISTICS_BASE: YearStatistics = { @@ -81,7 +98,6 @@ let currentLoadingString let mounted = true function AuctionHousePriceGraph(props: Props) { - let searchParams = useSearchParams() let [fetchspan, setFetchspan] = useValidRange() let [isLoading, setIsLoading] = useState(false) let [noDataFound, setNoDataFound] = useState(false) @@ -95,28 +111,11 @@ function AuctionHousePriceGraph(props: Props) { let [hasPremium, setHasPremium] = useState(false) let [yearStatistics, setYearStatistics] = useState(null) let [yearServerError, setYearServerError] = useState(null) - let [customStartDate, setCustomStartDate] = useState('') - let [customEndDate, setCustomEndDate] = useState('') + let [yearDateRange, setYearDateRange] = useState(null) let [mayorPeriods, setMayorPeriods] = useState([]) - let [showCustomDatePicker, setShowCustomDatePicker] = useState(false) - let [isYearLoading, setIsYearLoading] = useState(false) - let [yearParams, setYearParams] = useState(undefined) - let [yearFetchOptions, setYearFetchOptions] = useState(undefined) - let loadingMessage = useRotatingMessages( - isYearLoading, - [ - 'Loading year history data... This might take a while! 📊', - 'Crunching numbers from the past year... Stay tuned! 🔢', - "Fetching auction data... It's a lot of information! 📈", - 'Almost there! Processing historical market trends... 📋', - 'Loading complete market analysis... Worth the wait! 🎯', - 'Gathering seller and buyer statistics... Hang tight! 👥', - "Fun fact: We're processing millions of auction records! 🎪", - 'Did you know? Year data includes every single transaction! 💰' - ], - 10000 - ) + let [yearRequest, setYearRequest] = useState(null) let graphRef = useRef(null) + let yearRequestIdRef = useRef(0) let router = useRouter() let pathname = usePathname() @@ -137,11 +136,13 @@ function AuctionHousePriceGraph(props: Props) { useEffect(() => { loadFilters().then(filters => { - fetchspan = DEFAULT_DATE_RANGE - setFetchspan(DEFAULT_DATE_RANGE) setFilters(filters) if (props.item) { - updateChart(fetchspan, getPrefillFilter(filters)) + const prefillFilter = getPrefillFilter(filters) + updateChart( + fetchspanRef.current, + fetchspanRef.current === DateRange.YEAR ? buildYearHistoryFilter(prefillFilter, yearDateRange) : prefillFilter + ) } }) }, [props.item.tag]) @@ -152,10 +153,10 @@ function AuctionHousePriceGraph(props: Props) { const ok = hasHighEnoughPremium(products, PREMIUM_RANK.PREMIUM) setHasPremium(ok) if (ok) { - updateChart(DateRange.YEAR, itemFilter) + updateChart(DateRange.YEAR, buildYearHistoryFilter(itemFilter, yearDateRange)) } }) - .catch(() => { }) + .catch(() => {}) } const now = new Date() @@ -165,9 +166,9 @@ function AuctionHousePriceGraph(props: Props) { const mayorParams = fetchspan === DateRange.YEAR ? { - from: currentMayorFrom.toISOString(), - to: currentMayorTo.toISOString() - } + from: currentMayorFrom.toISOString(), + to: currentMayorTo.toISOString() + } : undefined const mayorQuery = useGetApiMayor(mayorParams, { query: { enabled: fetchspan === DateRange.YEAR } }) @@ -175,34 +176,29 @@ function AuctionHousePriceGraph(props: Props) { useEffect(() => { if (mayorQuery.data) { const periods = mayorQuery.data.data || [] - const sortedPeriods = periods.sort((a: any, b: any) => new Date(b.start).getTime() - new Date(a.start).getTime()) + const sortedPeriods = [...periods].sort((a, b) => new Date(b.start ?? 0).getTime() - new Date(a.start ?? 0).getTime()) setMayorPeriods(sortedPeriods) } else if (mayorQuery.error) { console.error(mayorQuery.error) } }, [mayorQuery.data, mayorQuery.error]) - const authKey = yearFetchOptions?.headers ? yearFetchOptions.headers : null - const yearQuery = useGetApiItemPriceItemTagHistoryYear(props.item.tag, yearParams, { + const yearQuery = useGetApiItemPriceItemTagHistoryYear(props.item.tag, yearRequest?.params as any, { query: { - enabled: isYearLoading, - queryKey: ['yearHistory', props.item.tag, yearParams ?? null, authKey] + enabled: fetchspan === DateRange.YEAR && yearRequest !== null, + queryKey: ['yearHistory', props.item.tag, yearRequest?.params ?? null, yearRequest?.id ?? null], + retry: false }, - fetch: yearFetchOptions + fetch: yearRequest?.fetchOptions }) + const isYearLoading = fetchspan === DateRange.YEAR && yearRequest !== null && yearQuery.isFetching + const loadingMessage = useRotatingMessages(isYearLoading, YEAR_LOADING_MESSAGES, 10000) useEffect(() => { - if (!isYearLoading) return + if (fetchspan !== DateRange.YEAR || !yearRequest || yearQuery.isFetching) return if (yearQuery.data) { const response = yearQuery.data as any - - if (!mounted || currentLoadingString !== JSON.stringify({ tag: props.item.tag, fetchspan, itemFilter })) { - setIsYearLoading(false) - return - } - - setIsYearLoading(false) setYearServerError(null) const data = response.data const status = response.status as number | undefined @@ -224,8 +220,11 @@ function AuctionHousePriceGraph(props: Props) { // Premium required: only when backend explicitly says so if ( - (status === 401 || status === 403) || - (data && typeof data === 'object' && (data.slug === 'premium_required' || data.isPremiumRequired || data.premiumRequired || data.error === 'premium_required')) + status === 401 || + status === 403 || + (data && + typeof data === 'object' && + (data.slug === 'premium_required' || data.isPremiumRequired || data.premiumRequired || data.error === 'premium_required')) ) { setIsLoading(false) setYearStatistics({ ...EMPTY_YEAR_STATISTICS_BASE, isPremiumRequired: true }) @@ -235,36 +234,27 @@ function AuctionHousePriceGraph(props: Props) { } if (data && typeof data === 'object' && 'prices' in data) { - setYearStatistics(data as any) - - let filteredPrices = data.prices || [] - - if (filteredPrices.length > 0) { - if (customStartDate || customEndDate) { - filteredPrices = filteredPrices.filter(item => { - const itemDate = new Date(item.time) - const startDate = customStartDate ? new Date(customStartDate) : new Date(0) - const endDate = customEndDate ? new Date(customEndDate) : new Date() - - return itemDate >= startDate && itemDate <= endDate - }) - } - - chartOptions.xAxis[0].data = filteredPrices.map(item => new Date(item.time).getTime()) - chartOptions.series[0].data = filteredPrices.map(item => item.avg.toFixed(2)) - chartOptions.series[1].data = filteredPrices.map(item => item.min.toFixed(2)) - chartOptions.series[2].data = filteredPrices.map(item => item.max.toFixed(2)) - chartOptions.series[3].data = filteredPrices.map(item => item.volume.toFixed(2)) - - let priceSum = filteredPrices.reduce((sum, item) => sum + item.avg, 0) - setAvgPrice(Math.round(priceSum / filteredPrices.length)) - } else { - setAvgPrice(0) - } + const statistics = data as YearStatistics + const prices = statistics.prices || [] + const dates = prices.map(item => new Date(item.time).getTime()) + + setYearStatistics(statistics) + setChartOptions(current => ({ + ...current, + dataZoom: current.dataZoom.map(zoom => ({ ...zoom, start: 0, end: 100 })), + xAxis: current.xAxis.map((axis, index) => (index === 0 ? { ...axis, data: dates } : axis)), + series: current.series.map((series, index) => { + if (index === 0) return { ...series, data: prices.map(item => item.avg.toFixed(2)) } + if (index === 1) return { ...series, data: prices.map(item => item.min.toFixed(2)) } + if (index === 2) return { ...series, data: prices.map(item => item.max.toFixed(2)) } + if (index === 3) return { ...series, data: prices.map(item => item.volume.toFixed(2)) } + return series + }) + })) + setAvgPrice(prices.length ? Math.round(prices.reduce((sum, item) => sum + item.avg, 0) / prices.length) : 0) setIsLoading(false) - setNoDataFound(filteredPrices?.length === 0) - setChartOptions(chartOptions) + setNoDataFound(prices.length === 0) } else { setIsLoading(false) setNoDataFound(true) @@ -274,7 +264,6 @@ function AuctionHousePriceGraph(props: Props) { } else if (yearQuery.error) { const e: any = yearQuery.error console.error(e) - setIsYearLoading(false) setIsLoading(false) setYearServerError(null) @@ -300,9 +289,13 @@ function AuctionHousePriceGraph(props: Props) { } setAvgPrice(0) } - }, [yearQuery.data, yearQuery.error]) + }, [fetchspan, yearQuery.data, yearQuery.error, yearQuery.isFetching, yearRequest]) let updateChart = (fetchspan: DateRange, itemFilter?: ItemFilter) => { + if (fetchspan !== DateRange.YEAR) { + setYearRequest(null) + } + if (fetchspan === DateRange.ACTIVE) { setIsLoading(false) return @@ -327,17 +320,16 @@ function AuctionHousePriceGraph(props: Props) { }) if (fetchspan === DateRange.YEAR) { - setIsYearLoading(true) setYearServerError(null) - let params: any = {} + let params: ItemFilter = {} if (itemFilter && Object.keys(itemFilter).length > 0) { params = { ...itemFilter } } const requestOptions: RequestInit = {} if (typeof window !== 'undefined') { - const googleId = isSignedIn ? sessionStorage.getItem('googleId') : null + const googleId = sessionStorage.getItem('googleId') if (googleId) { requestOptions.headers = { GoogleToken: googleId, @@ -346,8 +338,12 @@ function AuctionHousePriceGraph(props: Props) { } } - setYearParams(Object.keys(params).length > 0 ? params : undefined) - setYearFetchOptions(requestOptions) + yearRequestIdRef.current += 1 + setYearRequest({ + id: yearRequestIdRef.current, + params: Object.keys(params).length > 0 ? params : undefined, + fetchOptions: requestOptions + }) return } @@ -356,11 +352,11 @@ function AuctionHousePriceGraph(props: Props) { if ( !mounted || currentLoadingString !== - JSON.stringify({ - tag: props.item.tag, - fetchspan, - itemFilter - }) + JSON.stringify({ + tag: props.item.tag, + fetchspan, + itemFilter + }) ) { return } @@ -392,7 +388,7 @@ function AuctionHousePriceGraph(props: Props) { let mayorData = await api.getMayorData(minDate, maxDate) setMayorData(mayorData) applyMayorDataToChart(chartOptions, mayorData, 4) - } catch (e) { } + } catch (e) {} setAvgPrice(Math.round(priceSum / prices.length)) setNoDataFound(prices.length === 0) @@ -410,7 +406,7 @@ function AuctionHousePriceGraph(props: Props) { let onRangeChange = (timespan: DateRange) => { setFetchspan(timespan) if (timespan !== DateRange.ACTIVE) { - updateChart(timespan, itemFilter) + updateChart(timespan, timespan === DateRange.YEAR ? buildYearHistoryFilter(itemFilter, yearDateRange) : itemFilter) } } @@ -418,10 +414,15 @@ function AuctionHousePriceGraph(props: Props) { setItemFilter({ ...filter }) setDefaultRangeSwitch(!defaultRangeSwitch) if (fetchspanRef.current !== DateRange.ACTIVE) { - updateChart(fetchspanRef.current, filter) + updateChart(fetchspanRef.current, fetchspanRef.current === DateRange.YEAR ? buildYearHistoryFilter(filter, yearDateRange) : filter) } } + function onYearDateRangeChange(range: CustomDateRange | null) { + setYearDateRange(range) + updateChart(DateRange.YEAR, buildYearHistoryFilter(itemFilter, range)) + } + function loadFilters() { return api.getFilters(props.item.tag) } @@ -476,9 +477,7 @@ function AuctionHousePriceGraph(props: Props) {
⚠️
Server Error
-

- {yearServerError} -

+

{yearServerError}

@@ -487,7 +486,7 @@ function AuctionHousePriceGraph(props: Props) { className="btn btn-sm btn-outline-light" onClick={() => { setYearServerError(null) - updateChart(DateRange.YEAR, itemFilter) + updateChart(DateRange.YEAR, buildYearHistoryFilter(itemFilter, yearDateRange)) }} > Retry @@ -516,7 +515,7 @@ function AuctionHousePriceGraph(props: Props) {
- {/* Quick Select Options */} -
- 🎯 Quick Select: -
- {/* Previous Mayor */} - {mayorPeriods.length > 1 && ( - - )} - - {/* Around Current Mayor Start */} - {mayorPeriods.length > 0 && ( - - )} - - {/* 1 Year Ago */} - -
+ 🎯 Quick Select: +
+ + +
- {/* Custom Date Inputs */} - {showCustomDatePicker && ( - + {showDatePicker && ( + - - Start Date: - + Start Date setCustomStartDate(d ? d.toISOString().split('T')[0] : '')} - maxDate={new Date()} + selected={parseDateInput(startDate)} + onChange={(date: Date | null) => setStartDate(date ? formatDateInput(date) : '')} + maxDate={parseDateInput(endDate) ?? new Date()} className="form-control form-control-sm" + wrapperClassName="w-100" dateFormat="yyyy-MM-dd" + placeholderText="Select a start date" /> - - End Date: - + End Date setCustomEndDate(d ? d.toISOString().split('T')[0] : '')} + selected={parseDateInput(endDate)} + onChange={(date: Date | null) => setEndDate(date ? formatDateInput(date) : '')} + minDate={parseDateInput(startDate) ?? undefined} maxDate={new Date()} className="form-control form-control-sm" + wrapperClassName="w-100" dateFormat="yyyy-MM-dd" + placeholderText="Select an end date" /> + {invalidRange && ( + + The end date must be on or after the start date. + + )} )} -
+ ) } diff --git a/components/PriceGraph/AuctionHousePriceGraph/YearDateRangeUtils.ts b/components/PriceGraph/AuctionHousePriceGraph/YearDateRangeUtils.ts new file mode 100644 index 00000000..56bbce79 --- /dev/null +++ b/components/PriceGraph/AuctionHousePriceGraph/YearDateRangeUtils.ts @@ -0,0 +1,105 @@ +import type { CoflnetSkyMayorModelsModelElectionPeriod } from '../../../api/_generated/skyApi.schemas' + +const DAY_IN_MS = 24 * 60 * 60 * 1000 + +export interface CustomDateRange { + startDate?: string + endDate?: string +} + +function formatUtcDate(date: Date) { + return date.toISOString().slice(0, 10) +} + +function formatDate(year: number, month: number, day: number) { + return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` +} + +function parsePeriodTime(value: string) { + const utcParts = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4}) (\d{1,2}):(\d{2}):(\d{2})(?: \+00:00)?$/) + if (utcParts) { + const [, month, day, year, hour, minute, second] = utcParts.map(Number) + return Date.UTC(year, month - 1, day, hour, minute, second) + } + + return new Date(value).getTime() +} + +function getPeriodTime(period: CoflnetSkyMayorModelsModelElectionPeriod, field: 'start' | 'end') { + const value = period[field] + const time = value ? parsePeriodTime(value) : Number.NaN + return Number.isNaN(time) ? null : time +} + +export function buildYearHistoryFilter(itemFilter?: ItemFilter, dateRange?: CustomDateRange | null): ItemFilter { + const filter: ItemFilter = {} + + Object.entries(itemFilter ?? {}).forEach(([key, value]) => { + if (key !== '_hide' && typeof value === 'string') { + filter[key] = value + } + }) + + if (dateRange?.startDate) { + filter.EndAfter = Math.floor(new Date(`${dateRange.startDate}T00:00:00Z`).getTime() / 1000).toString() + } + if (dateRange?.endDate) { + filter.EndBefore = Math.floor(new Date(`${dateRange.endDate}T23:59:59Z`).getTime() / 1000).toString() + } + + return filter +} + +export function getMonthAYearAgo(now = new Date()): CustomDateRange { + const year = now.getFullYear() - 1 + const month = now.getMonth() + const lastDay = new Date(year, month + 1, 0).getDate() + + return { + startDate: formatDate(year, month, 1), + endDate: formatDate(year, month, lastDay) + } +} + +export function getLastCompletedMayor( + mayorPeriods: CoflnetSkyMayorModelsModelElectionPeriod[], + now = new Date() +): CoflnetSkyMayorModelsModelElectionPeriod | undefined { + return [...mayorPeriods] + .filter(period => { + const end = getPeriodTime(period, 'end') + return end !== null && end <= now.getTime() + }) + .sort((a, b) => (getPeriodTime(b, 'end') ?? 0) - (getPeriodTime(a, 'end') ?? 0))[0] +} + +export function getLastElectionRange(mayorPeriods: CoflnetSkyMayorModelsModelElectionPeriod[], mayorName: string, now = new Date()): CustomDateRange | null { + const matchingPeriod = [...mayorPeriods] + .filter(period => { + const start = getPeriodTime(period, 'start') + return period.winner?.name === mayorName && start !== null && start <= now.getTime() + }) + .sort((a, b) => (getPeriodTime(b, 'start') ?? 0) - (getPeriodTime(a, 'start') ?? 0))[0] + + const electionTime = matchingPeriod ? getPeriodTime(matchingPeriod, 'start') : null + if (electionTime === null) { + return null + } + + return { + startDate: formatUtcDate(new Date(electionTime - 5 * DAY_IN_MS)), + endDate: formatUtcDate(new Date(Math.min(electionTime + 5 * DAY_IN_MS, now.getTime()))) + } +} + +export function getMayorTermRange(period?: CoflnetSkyMayorModelsModelElectionPeriod): CustomDateRange | null { + const start = period ? getPeriodTime(period, 'start') : null + const end = period ? getPeriodTime(period, 'end') : null + + return start === null || end === null + ? null + : { + startDate: formatUtcDate(new Date(start)), + endDate: formatUtcDate(new Date(end)) + } +} diff --git a/cypress/e2e/yearDateRange.cy.ts b/cypress/e2e/yearDateRange.cy.ts new file mode 100644 index 00000000..4871fc76 --- /dev/null +++ b/cypress/e2e/yearDateRange.cy.ts @@ -0,0 +1,125 @@ +import { + buildYearHistoryFilter, + getLastCompletedMayor, + getLastElectionRange, + getMonthAYearAgo +} from '../../components/PriceGraph/AuctionHousePriceGraph/YearDateRangeUtils' + +describe('Year history date ranges', () => { + const mayorPeriods = [ + { + start: '2026-07-19T15:15:00Z', + end: '2026-07-24T19:15:00Z', + winner: { name: 'Paul', votes: 10 }, + year: 502 + }, + { + start: '07/14/2026 11:15:00 +00:00', + end: '07/19/2026 15:15:00', + winner: { name: 'Cole', votes: 10 }, + year: 501 + }, + { + start: '2026-07-04T03:15:00Z', + end: '2026-07-09T07:15:00Z', + winner: { name: 'Diana', votes: 10 }, + year: 499 + } + ] + + it('builds inclusive API filters without leaking UI-only values', () => { + const itemFilter: ItemFilter = { Bin: 'true' } + itemFilter._hide = true + + expect( + buildYearHistoryFilter(itemFilter, { + startDate: '2025-07-01', + endDate: '2025-07-31' + }) + ).to.deep.equal({ + Bin: 'true', + EndAfter: String(Date.UTC(2025, 6, 1) / 1000), + EndBefore: String(Date.UTC(2025, 6, 31, 23, 59, 59) / 1000) + }) + }) + + it('selects the requested calendar month from the previous year', () => { + expect(getMonthAYearAgo(new Date('2026-07-21T12:00:00Z'))).to.deep.equal({ + startDate: '2025-07-01', + endDate: '2025-07-31' + }) + }) + + it('finds the last completed mayor and the previous next-mayor election', () => { + const now = new Date('2026-07-21T12:00:00Z') + + expect(getLastCompletedMayor(mayorPeriods, now)?.winner?.name).to.equal('Cole') + expect(getLastElectionRange(mayorPeriods, 'Diana', now)).to.deep.equal({ + startDate: '2026-06-29', + endDate: '2026-07-09' + }) + }) + + it('loads a premium custom range and sends its inclusive date bounds', () => { + cy.clock(new Date('2026-07-22T12:00:00Z').getTime(), ['Date']) + cy.intercept('GET', 'https://sky.coflnet.com/api/filter/options*', [{ name: 'Bin', options: ['true', 'false'], type: 256, description: null }]) + cy.intercept('GET', 'https://sky.coflnet.com/api/mayor*', mayorPeriods) + cy.intercept('GET', 'https://api.hypixel.net/v2/resources/skyblock/election', { + success: true, + current: { + candidates: [ + { name: 'Diana', votes: 200 }, + { name: 'Cole', votes: 100 } + ] + } + }) + cy.intercept('GET', 'https://sky.coflnet.com/api/item/price/SKELETON_THE_FISH/history/year*', request => { + const requestUrl = new URL(request.url) + const isCustomRange = requestUrl.searchParams.has('EndAfter') + const average = isCustomRange ? 300 : 150 + + if (isCustomRange) { + request.alias = 'customYearHistory' + } + + request.reply({ + averageSellTimeSeconds: 3600, + totalAuctionsSold: 2, + totalListed: 2, + totalSellers: 2, + totalBuyers: 2, + totalBids: 0, + totalCoinsTransferred: average * 2, + totalAuctions: 2, + totalItemsSold: 2, + binCount: 2, + prices: [ + { min: average - 10, max: average + 10, avg: average, volume: 2, time: '2025-07-10T12:00:00Z' }, + { min: average - 10, max: average + 10, avg: average, volume: 2, time: '2025-07-11T12:00:00Z' } + ], + recentSamples: [] + }) + }) + + cy.visit('/item/SKELETON_THE_FISH?range=year&Bin=true', { + onBeforeLoad(window) { + window.localStorage.clear() + window.sessionStorage.setItem('googleId', 'cypress-premium-token') + } + }) + + cy.contains('h6', 'Custom Date Range').should('be.visible') + cy.contains('button', 'Last Mayor (Cole)').should('be.visible') + cy.contains('button', 'Month a Year Ago').click() + cy.wait('@customYearHistory').then(({ request }) => { + const requestUrl = new URL(request.url) + expect(request.headers).to.have.property('googletoken', 'cypress-premium-token') + expect(requestUrl.searchParams.get('Bin')).to.equal('true') + expect(requestUrl.searchParams.get('EndAfter')).to.equal(String(Date.UTC(2025, 6, 1) / 1000)) + expect(requestUrl.searchParams.get('EndBefore')).to.equal(String(Date.UTC(2025, 6, 31, 23, 59, 59) / 1000)) + }) + cy.contains('div', 'Avg Price:').should('contain.text', '300').and('contain.text', 'Coins') + cy.contains('button', 'Last Time Diana Was Elected').should('be.visible') + cy.contains('Loading year history data').should('not.exist') + }) +})