diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 259105b04..203144ce2 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -7,7 +7,7 @@ on: jobs: cypress-run: timeout-minutes: 60 - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/app/about/page.tsx b/app/about/page.tsx index 4fa9bad62..b652cd099 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -115,4 +115,4 @@ export default async function Page() { ) } -export const metadata = getHeadMetadata('About') \ No newline at end of file +export const metadata = getHeadMetadata('About', 'Learn about Hypixel SkyBlock\'s premier auction house tracker and bazaar analyzer. Discover our features, credits, API access, and how we help millions of players optimize their trading strategies.') \ No newline at end of file diff --git a/app/authmod/page.tsx b/app/authmod/page.tsx index a18fce892..ef679d997 100644 --- a/app/authmod/page.tsx +++ b/app/authmod/page.tsx @@ -18,4 +18,4 @@ export default async function Page() { ) } -export const metadata = getHeadMetadata('Authenticate Mod') \ No newline at end of file +export const metadata = getHeadMetadata('Authenticate Mod', 'Connect your Minecraft account with our SkyCofl mod for Hypixel SkyBlock. Sync your data, enable premium features, and get real-time flipping notifications directly in-game.') \ No newline at end of file diff --git a/app/bazaar/page.tsx b/app/bazaar/page.tsx new file mode 100644 index 000000000..871b62faf --- /dev/null +++ b/app/bazaar/page.tsx @@ -0,0 +1,32 @@ +import { getHeadMetadata } from '../../utils/SSRUtils' +import NavBar from '../../components/NavBar/NavBar' +import AuthMod from '../../components/AuthMod/AuthMod' +import { Container } from 'react-bootstrap' +import { getApiFlipBazaarSpread, getGetApiFlipBazaarSpreadQueryKey, useGetApiFlipBazaarSpread } from '../../api/_generated/skyApi' +import { BazaarFlips } from '../../components/BazaarFlips/BazaarFlips' +import { getQueryClient } from '../../utils/QueryUtils' +import { dehydrate, HydrationBoundary } from '@tanstack/react-query' + +export default async function Page() { + const queryClient = getQueryClient() + queryClient.prefetchQuery({ + queryKey: [getGetApiFlipBazaarSpreadQueryKey()], + queryFn: () => getApiFlipBazaarSpread(), + }) + return ( + <> + +

+ + Bazaar Flips +

+
+ + + +
+ + ) +} + +export const metadata = getHeadMetadata('Bazaar Flips', 'Discover profitable Hypixel SkyBlock bazaar flipping opportunities. Real-time flip analysis, buy/sell spreads, and instant order data to maximize your coin profits.') \ No newline at end of file diff --git a/app/cancel.tsx/page.tsx b/app/cancel.tsx/page.tsx index d63f5c5b1..b1cafd93a 100644 --- a/app/cancel.tsx/page.tsx +++ b/app/cancel.tsx/page.tsx @@ -28,4 +28,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata('Payment canceled') +export const metadata = getHeadMetadata('Payment Canceled', 'Payment was canceled. No charges were made to your account. You can still access our free Hypixel SkyBlock tools or try upgrading to premium again anytime.') diff --git a/app/data/page.tsx b/app/data/page.tsx index d696c3169..6ce8825ca 100644 --- a/app/data/page.tsx +++ b/app/data/page.tsx @@ -74,4 +74,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata('API') +export const metadata = getHeadMetadata('API', 'Access comprehensive Hypixel SkyBlock auction and bazaar data through our free API. Get real-time prices, historical data, item information, and player statistics for your applications and tools.') diff --git a/app/error.tsx b/app/error.tsx index e247e878d..41aa52f58 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -1,21 +1,233 @@ 'use client' import Link from 'next/link' -import { Button } from 'react-bootstrap' -import { Container } from 'react-bootstrap' +import { Button, Container, Alert, Form, Card, Row, Col } from 'react-bootstrap' import { getHeadMetadata } from '../utils/SSRUtils' +import { useState, useEffect } from 'react' +import { toast } from 'react-toastify' +import api from '../api/ApiHelper' + +interface ErrorReportData { + userDescription: string + includeErrorDetails: boolean + contactMethod: 'discord' | 'email' | 'none' +} + +export default function Custom500({ error, reset }) { + const [showDetails, setShowDetails] = useState(false) + const [isReporting, setIsReporting] = useState(false) + const [hasReported, setHasReported] = useState(false) + const [autoReportAttempted, setAutoReportAttempted] = useState(false) + const [reportData, setReportData] = useState({ + userDescription: '', + includeErrorDetails: true, + contactMethod: 'none' + }) + + // Attempt automatic error reporting when component mounts + useEffect(() => { + if (!autoReportAttempted) { + setAutoReportAttempted(true) + attemptAutoReport() + } + }, []) + + const attemptAutoReport = async () => { + try { + const errorReport = { + type: 'server_error', + error: error?.message || 'Unknown server error', + stack: error?.stack || 'No stack trace available', + digest: error?.digest || 'No digest available', + timestamp: new Date().toISOString(), + url: window.location.href, + userAgent: navigator.userAgent, + automaticReport: true + } + + await api.sendFeedback('server_error_auto', errorReport) + console.log('Automatic error report sent successfully') + } catch (reportError) { + console.error('Failed to send automatic error report:', reportError) + // Don't show this error to the user, as it's secondary to the main error + } + } + + const handleManualReport = async () => { + setIsReporting(true) + try { + const errorReport = { + otherIssue : true, + additionalInformation: JSON.stringify({ + userDescription: reportData.userDescription, + type: 'server_error_manual', + error: error?.message || 'Unknown server error', + stack: reportData.includeErrorDetails ? (error?.stack || 'No stack trace available') : 'User opted not to include technical details', + digest: reportData.includeErrorDetails ? (error?.digest || 'No digest available') : 'Not included', + timestamp: new Date().toISOString(), + url: window.location.href, + userAgent: navigator.userAgent, + }) + } as ReloadFeedback + + await api.sendFeedback('server_error_manual', errorReport) + toast.success('Error report sent successfully! Thank you for helping us improve.') + setHasReported(true) + } catch (reportError) { + toast.error('Failed to send error report. Please try contacting us directly.') + console.error('Manual error report failed:', reportError) + } finally { + setIsReporting(false) + } + } + + const errorTitle = error?.message ? `Error: ${error.message}` : 'Something went wrong' + const errorDetails = { + message: error?.message || 'Unknown error', + digest: error?.digest || 'No digest available', + timestamp: new Date().toISOString() + } -export default function Custom500({ error }) { return ( <> - -

500 - Server-side error occurred

- - - -

{JSON.stringify(error)}

+ + + + + +

๐Ÿšจ Oops! Something went wrong

+
+ + +

+ We're sorry! An unexpected error occurred while processing your request. +

+

+ Don't worry - we've been automatically notified and will investigate this issue. +

+
+ +
+
What you can do:
+
    +
  • Try refreshing the page
  • +
  • Go back to the main page and try again
  • +
  • Report this error with additional details (optional)
  • +
+
+ +
+ + + + + {reset && ( + + )} +
+ + {!hasReported && ( + + +
๐Ÿ“ Help us fix this (Optional)
+
+ +
+ + What were you trying to do when this error occurred? + setReportData({...reportData, userDescription: e.target.value})} + placeholder="Describe what you were doing when the error happened..." + /> + + + + setReportData({...reportData, includeErrorDetails: e.target.checked})} + /> + + + +
+
+
+ )} + + {hasReported && ( + +
โœ… Thank you!
+

Your error report has been sent successfully. We appreciate your help in making our service better!

+
+ )} + + + +
๐Ÿ’ฌ Need immediate help?
+
+ +

If you need assistance right away, you can reach us directly:

+ +
+
+ +
+ +
+ + {showDetails && ( + +
Technical Details:
+
+                                            {JSON.stringify(errorDetails, null, 2)}
+                                        
+
+ )} +
+
+ +
) } -export const metadata = getHeadMetadata('Error') +export const metadata = getHeadMetadata('Error', 'An error occurred while loading the page. Please try again or contact support if the issue persists. Our Hypixel SkyBlock tools are usually available 24/7 for reliable auction and bazaar tracking.') diff --git a/app/feedback/page.tsx b/app/feedback/page.tsx index 749658d34..35154f0c6 100644 --- a/app/feedback/page.tsx +++ b/app/feedback/page.tsx @@ -44,4 +44,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata('Feedback') +export const metadata = getHeadMetadata('Feedback', 'Share your ideas and suggestions for improving our Hypixel SkyBlock tools. Contact our support team via email or Discord to help us build better features for the community.') diff --git a/app/not-found.tsx b/app/not-found.tsx index 7b04c7d15..d3dafd896 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -19,4 +19,4 @@ export default function NotFound() { ) } -export const metadata = getHeadMetadata('Not found') +export const metadata = getHeadMetadata('Page Not Found', 'The page you\'re looking for doesn\'t exist. Return to our Hypixel SkyBlock auction house tracker and bazaar analyzer to continue exploring price data and trading opportunities.') diff --git a/app/premium/page.tsx b/app/premium/page.tsx index de16b7fce..a09a145e4 100644 --- a/app/premium/page.tsx +++ b/app/premium/page.tsx @@ -13,4 +13,4 @@ export default async function Page() { ) } -export const metadata = getHeadMetadata('Premium', 'See available premium options to support this project') +export const metadata = getHeadMetadata('Premium', 'Upgrade to premium for advanced Hypixel SkyBlock features: priority flip notifications, enhanced bazaar analysis, exclusive tools, and priority support. Support our project while maximizing your profits.') diff --git a/app/refed/page.tsx b/app/refed/page.tsx index d9eccd4e9..c0c4366e2 100644 --- a/app/refed/page.tsx +++ b/app/refed/page.tsx @@ -14,4 +14,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata() +export const metadata = getHeadMetadata('Referral Success', 'Welcome! You\'ve been referred to the best Hypixel SkyBlock auction house and bazaar tracking tools. Start exploring profitable flips, price history, and trading opportunities.') diff --git a/app/subscriptions/page.tsx b/app/subscriptions/page.tsx index 7eb55a547..bddc1e540 100644 --- a/app/subscriptions/page.tsx +++ b/app/subscriptions/page.tsx @@ -19,4 +19,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata('Notifiers') +export const metadata = getHeadMetadata('Notifiers', 'Set up custom price alerts and notifications for Hypixel SkyBlock items. Get notified about auction house deals, bazaar price changes, and profitable flipping opportunities via Discord or in-game.') diff --git a/app/success/page.tsx b/app/success/page.tsx index a93be90a2..b726bd7f2 100644 --- a/app/success/page.tsx +++ b/app/success/page.tsx @@ -26,4 +26,4 @@ export default function Page() { ) } -export const metadata = getHeadMetadata('Payment successful') +export const metadata = getHeadMetadata('Payment Successful', 'Thank you for your purchase! Your premium subscription is now active. Enjoy advanced features, priority support, and enhanced Hypixel SkyBlock tools to maximize your trading profits.') diff --git a/app/trade/page.tsx b/app/trade/page.tsx index 93bcd7ec9..946749c73 100644 --- a/app/trade/page.tsx +++ b/app/trade/page.tsx @@ -17,4 +17,4 @@ export default async function Page() { ) } -export const metadata = getHeadMetadata('Trading') +export const metadata = getHeadMetadata('Trading', 'Create and manage Hypixel SkyBlock item trades. Connect with other players, set up wanted items, and find trading partners for efficient item exchanges in the SkyBlock community.') diff --git a/app/wiki/docs/filters.md b/app/wiki/docs/filters.md index c9665566d..f96f15706 100644 --- a/app/wiki/docs/filters.md +++ b/app/wiki/docs/filters.md @@ -37,7 +37,7 @@ Selects estimated profit percentage of a flip, so how much profit you make compa Selects the algorithm that found the flip. Allows you to blacklist certain items from one algorithm. For example you might want to have a different minprofit for the sniper finder so you could add: ``` WHITELIST -Minprofit: {{MIN_PROFIT}}*0.5 +Profit: >{{MIN_PROFIT}}*0.5 FlipFinder: sniper ``` To set the minprofit to just half for all flips found by the sniper finder (lbin based flips) diff --git a/app/wiki/page.tsx b/app/wiki/page.tsx index d5607f25d..bedc60faa 100644 --- a/app/wiki/page.tsx +++ b/app/wiki/page.tsx @@ -4,6 +4,7 @@ import { MDXRemote } from 'next-mdx-remote/rsc'; import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import matter from 'gray-matter'; +import { getHeadMetadata } from '../../utils/SSRUtils'; const docsDirectory = path.join(process.cwd(), 'app/wiki/docs'); @@ -40,3 +41,5 @@ export default async function Wiki() { ); } + +export const metadata = getHeadMetadata('Wiki', 'Comprehensive guides and documentation for Hypixel SkyBlock tools, trading strategies, mod features, and optimization tips. Learn how to maximize your profits and improve your gameplay.'); diff --git a/components/BazaarFlips/BazaarFlips.module.css b/components/BazaarFlips/BazaarFlips.module.css new file mode 100644 index 000000000..7301fc1bf --- /dev/null +++ b/components/BazaarFlips/BazaarFlips.module.css @@ -0,0 +1,61 @@ +.bazaarFlips :global(.list-group-item) { + width: -webkit-fill-available; + border-radius: 20px; +} + +.list { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + flex-direction: initial; +} + +#tooltip-container .tooltip-inner { + max-width: fit-content; +} + +.bazaarFlips .flipCard { + width: calc(100% - 10px); + height: auto; + padding-bottom: 15px; +} + +@media all and (min-width: 768px) { + .bazaarFlips .flipCard { + width: calc(50% - 10px); + } +} + +@media all and (min-width: 1424px) { + .bazaarFlips .flipCard { + width: calc(33% - 10px); + } +} + +.bazaarFlips .preventSelect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.bazaarFlips .label { + width: 150px; + float: left; +} + +.bazaarFlips :global(.list-group-item) :global(p) { + margin: 0; +} +.bazaarFlips :global(.list-group-item) .craftRequirement { + text-align: right; +} + +.bazaarFlips :global(.list-group-item) .craftRequirementLabel { + margin-right: 10px; +} +.filterInput { + width: 100%; + margin-right: 3vh; +} diff --git a/components/BazaarFlips/BazaarFlips.tsx b/components/BazaarFlips/BazaarFlips.tsx new file mode 100644 index 000000000..6d42f43e6 --- /dev/null +++ b/components/BazaarFlips/BazaarFlips.tsx @@ -0,0 +1,176 @@ +'use client' +import Image from 'next/image' +import React from 'react' +import { Badge } from 'react-bootstrap' +import api from '../../api/ApiHelper' +import Number from '../Number/Number' +import { SpreadFlip } from '../../api/_generated/skyApi.schemas' +import { useSuspenseQuery } from '@tanstack/react-query' +import { getApiFlipBazaarSpread, getGetApiFlipBazaarSpreadQueryKey } from '../../api/_generated/skyApi' +import { GenericFlipList, SortOption } from '../GenericFlipList' +import Link from 'next/link' + +const SORT_OPTIONS: SortOption[] = [ + { + label: 'Profit/Hour', + value: 'profitPerHour', + sortFunction: flips => flips.sort((a, b) => (b.flip?.profitPerHour || 0) - (a.flip?.profitPerHour || 0)) + }, + { + label: 'Profit โ‡ง', + value: 'profitAsc', + sortFunction: flips => flips.sort((a, b) => (b.flip?.profitPerHour || 0) - (a.flip?.profitPerHour || 0)) + }, + { + label: 'Profit โ‡ฉ', + value: 'profitDesc', + sortFunction: flips => flips.sort((a, b) => (a.flip?.profitPerHour || 0) - (b.flip?.profitPerHour || 0)) + }, + { + label: 'Volume โ‡ฉ', + value: 'volumeAsc', + sortFunction: flips => flips.sort((a, b) => (b.flip?.volume || 0) - (a.flip?.volume || 0)) + }, + { + label: 'Price โ‡ง', + value: 'priceDesc', + sortFunction: flips => flips.sort((a, b) => (a.flip?.sellPrice || 0) - (b.flip?.sellPrice || 0)) + }, + { + label: 'Price โ‡ฉ', + value: 'priceAsc', + sortFunction: flips => flips.sort((a, b) => (b.flip?.sellPrice || 0) - (a.flip?.sellPrice || 0)) + } +] + +export function BazaarFlips() { + const { data: { data: flips } = { data: [] } } = useSuspenseQuery({ + queryKey: [getGetApiFlipBazaarSpreadQueryKey()], + queryFn: () => getApiFlipBazaarSpread(), + }) + + function renderFlipContent(flip: SpreadFlip) { + return ( + <> +

{getFlipHeader(flip)}

+

+ Buy-order at: +

+

+ Sell-order at:{' '} + Coins +

+ {flip.flip?.medianBuyPrice && +

+ Median: Coins +

+ } +

+ Profit per Hour:{' '} + Coins +

+

+ Sells/hour: + +

+ {flip.isManipulated && +

+ Seems Manipulated +

+ } + + ) + } + + function onFlipClick(flip: SpreadFlip) { + const url = `https://sky.coflnet.com/item/${flip.flip?.itemTag}` + window.open(url, '_blank') + } + + function getFlipHeader(flip: SpreadFlip) { + return ( + + + {flip.itemName} + + ) + } + + function filterFunction(flip: SpreadFlip, nameFilter: string | null | undefined, minimumProfit: number): boolean { + const nameMatch = !nameFilter || (flip.itemName?.toLowerCase().includes(nameFilter.toLowerCase()) ?? false) + const profitMatch = (flip.flip?.profitPerHour || 0) >= minimumProfit + return nameMatch && profitMatch + } + + function censoredItemGenerator(flip: SpreadFlip): SpreadFlip { + return { + ...flip, + itemName: 'You cheated the blur โ˜บ', + flip: { + buyPrice: 12345, + sellPrice: 69420, + profitPerHour: -100000, + volume: -1, + itemTag: 'BARRIER', + estimatedFees: 0, + timestamp: '', + medianBuyPrice: 0 + } + } + } + + return ( + <> +
These are bazaar spread based flips +

the spread is the difference between buy and sell price.
+ They are calculated using {'({sell price}-{buy price})*{sales per week}/{hours per week}'} + that calculation is used by most other hypixel skyblock bazaar website/mods and doesn't need data storing.
+ It displays you the potential profit under perfect conditions. If you are impatient it's probably lower, but it can also be higher if you are lucky.
+ We enhanced this by marking items that are probably manipulated to protect you from losing coins. +

+
+
+ How do we detect manipulated items? +

Using our hypixel skyblock bazaar data archive we know what an item usually sells for. If it suddenly spikes its usually manipulated. + Sometimes its also because of a game update. + So these items are still displayed for you to make a decission on if you want to risk you coins for potentially more reward. +

+
+
+ Why are the top 3 hidden +

The manipulation detection and other features depend on our data archive. + To finance the archive storage, the top 3 flips are only available with starter premium. + You either buy any premium tier or watched an ad to get starter premium for free +

+
+
+ How to do these flips? +
    +
  1. Go to the skyblock bazaar and search up the item. (This is simpler if you use this list in game with /cofl bazaar)
  2. +
  3. Place a top buyorder (should be within one coin of whats displayed here)
  4. +
  5. If you don't know how much to order select fill your inventory if its an item below 10k coins, otherwise you buy a stack, make sure to not spend your whole purse at once
  6. +
  7. Wait for the order to fill, if you notice that taking long times you may want to try our premium demand based bazaar flips
  8. +
  9. Once its filled you claim the items and place the lowest sell order
  10. +
+ flip.itemName || ''} + censoredItemGenerator={censoredItemGenerator} + premiumMessage="The top 3 flips can only be seen with starter premium or better" + clickMessage="Click on a flip for further details" + /> + + ) +} diff --git a/components/CraftsList/CraftsList.tsx b/components/CraftsList/CraftsList.tsx index 66deea721..c46f46246 100644 --- a/components/CraftsList/CraftsList.tsx +++ b/components/CraftsList/CraftsList.tsx @@ -1,50 +1,39 @@ 'use client' import Image from 'next/image' -import React, { ChangeEvent, useEffect, useState, type JSX } from 'react' -import { Form, ListGroup } from 'react-bootstrap' +import React, { useMemo } from 'react' import api from '../../api/ApiHelper' import { convertTagToName, getMinecraftColorCodedElement } from '../../utils/Formatter' -import { getLoadingElement } from '../../utils/LoadingUtils' -import { hasHighEnoughPremium, PREMIUM_RANK } from '../../utils/PremiumTypeUtils' -import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' import Number from '../Number/Number' import Tooltip from '../Tooltip/Tooltip' import { CraftDetails } from './CraftDetails/CraftDetails' -import styles from './CraftsList.module.css' import { parseProfitableCrafts } from '../../utils/Parser/APIResponseParser' -import Link from 'next/link' +import { GenericFlipList, SortOption } from '../GenericFlipList' interface Props { crafts?: any[] bazaarTags?: string[] } -interface SortOption { - label: string - value: string - sortFunction(crafts: ProfitableCraft[], bazaarTags: string[]) -} - -const SORT_OPTIONS: SortOption[] = [ +const SORT_OPTIONS: SortOption[] = [ { label: 'Profit', value: 'profit', - sortFunction: crafts => crafts.sort((a, b) => b.sellPrice - b.craftCost - (a.sellPrice - a.craftCost)) + sortFunction: (crafts: ProfitableCraft[]) => crafts.sort((a, b) => b.sellPrice - b.craftCost - (a.sellPrice - a.craftCost)) }, { label: 'Sell Price', value: 'sellPrice', - sortFunction: crafts => crafts.sort((a, b) => b.sellPrice - a.sellPrice) + sortFunction: (crafts: ProfitableCraft[]) => crafts.sort((a, b) => b.sellPrice - a.sellPrice) }, { label: 'Craft Cost', value: 'craftCost', - sortFunction: crafts => crafts.sort((a, b) => b.craftCost - a.craftCost) + sortFunction: (crafts: ProfitableCraft[]) => crafts.sort((a, b) => b.craftCost - a.craftCost) }, { label: 'Sell Offer (Bazaar)', value: 'bazaarCrafts', - sortFunction: (crafts, bazaarTags) => + sortFunction: (crafts: ProfitableCraft[], bazaarTags: string[] = []) => crafts .sort((a, b) => b.sellPrice - b.craftCost - (a.sellPrice - a.craftCost)) .filter(craft => { @@ -64,182 +53,50 @@ const SORT_OPTIONS: SortOption[] = [ } ] -let observer: MutationObserver - export function CraftsList(props: Props) { - let [crafts, setCrafts] = useState(props.crafts ? parseProfitableCrafts(props.crafts) : []) - let [nameFilter, setNameFilter] = useState() - let [orderBy, setOrderBy] = useState(SORT_OPTIONS[0]) - let [accountInfo, setAccountInfo] = useState() - let [isLoadingProfileData, setIsLoadingProfileData] = useState(true) - let [hasPremium, setHasPremium] = useState(false) - let [isLoggedIn, setIsLoggedIn] = useState(false) - let [bazaarTags, setBazaarTags] = useState(props.bazaarTags || []) - let [showTechSavvyMessage, setShowTechSavvyMessage] = useState(false) - let [minimumProfit, setMinimumProfit] = useState(0) - let [columns, setColumns] = useState() - - useEffect(() => { - setIsLoadingProfileData(true) - loadCrafts() - }, []) - - useEffect(() => { - // reset the blur observer, when something changed - setTimeout(() => { - setBlurObserver() - }, 100) - setColumns(getDefaultColumns()) // Set columns based on screen width after mounting - }, []) - - function loadCrafts() { - api.getProfitableCrafts().then(crafts => { - setCrafts(crafts) - }) - } - - function setBlurObserver() { - if (observer) { - observer.disconnect() - } - observer = new MutationObserver(function () { - setShowTechSavvyMessage(true) - }) - - var targets = document.getElementsByClassName('blur') - for (var i = 0; i < targets.length; i++) { - var config = { - attributes: true, - childList: true, - characterData: true, - attributeFilter: ['style'] - } - observer.observe(targets[i], config) - } - } - - function getDefaultColumns() { - const screenWidth = window.innerWidth - - if (screenWidth >= 1424) { - return 3 - } else if (screenWidth >= 768) { - return 2 - } else { - return 1 - } - } - - function onAfterLogin() { - setIsLoggedIn(true) - api.refreshLoadPremiumProducts(products => { - setHasPremium(hasHighEnoughPremium(products, PREMIUM_RANK.STARTER)) - api.getAccountInfo().then(info => { - setAccountInfo(info) - setIsLoadingProfileData(false) - }) - }) - } - - function onNameFilterChange(e: any) { - setNameFilter(e.target.value) - } - - function updateOrderBy(event: ChangeEvent) { - let selectedIndex = event.target.options.selectedIndex - let value = event.target.options[selectedIndex].getAttribute('value')! - let sortOption = SORT_OPTIONS.find(option => option.value === value) - if (sortOption) { - setOrderBy(sortOption) - } - } - function onMinimumProfitChange(e: any) { - setMinimumProfit(e.target.value) - } - - function handleColumnChange(event: ChangeEvent) { - const value = parseInt(event.target.value, 10) - setColumns(value) - } + const crafts = useMemo(() => props.crafts ? parseProfitableCrafts(props.crafts) : [], [props.crafts]) - let blurStyle: React.CSSProperties = { - WebkitFilter: 'blur(5px)', - msFilter: 'blur(5px)', - filter: 'blur(5px)' - } - - function getListElement(craft: ProfitableCraft, blur: boolean) { - if ( - ((nameFilter && craft.item.name?.toLowerCase().indexOf(nameFilter.toLowerCase()) === -1) || craft.sellPrice - craft.craftCost < minimumProfit) && - !blur - ) { - return - } + function renderFlipContent(craft: ProfitableCraft) { return ( - - {blur ? ( -

- The top 3 crafts can only be seen with starter premium or better + <> +

{getCraftHeader(craft)}

+

+ Crafting Cost: Coins +

+

+ Sell Price: Coins +

+

+ Median:{' '} + {craft.median > 0 ? ( + + Coins + + ) : ( + 'unknown' + )} +

+

+ Volume: {craft.volume > 0 ? : 'unknown'} +

+ {craft.requiredCollection ? ( +

+ Req. Collection: + {convertTagToName(craft.requiredCollection.name) + ' ' + craft.requiredCollection.level}

) : null} - {showTechSavvyMessage && blur ? ( -

- You seem like a tech savvy person, contribute to the project to get premium for free. :) -

- ) : ( - '' - )} -
-

{getCraftHeader(craft)}

-

- Crafting Cost: Coins -

-

- Sell Price: Coins -

-

- Median:{' '} - {craft.median > 0 ? ( - - Coins - - ) : ( - 'unknown' - )} -

-

- Volume: {craft.volume > 0 ? : 'unknown'} + {craft.requiredSlayer ? ( +

+ Req. Slayer: + {convertTagToName(craft.requiredSlayer.name) + ' ' + craft.requiredSlayer.level}

- {craft.requiredCollection ? ( -

- Req. Collection: - {convertTagToName(craft.requiredCollection.name) + ' ' + craft.requiredCollection.level} -

- ) : null} - {craft.requiredSlayer ? ( -

- Req. Slayer: - {convertTagToName(craft.requiredSlayer.name) + ' ' + craft.requiredSlayer.level} -

- ) : null} - {!craft.requiredCollection && !craft.requiredSlayer ?

No Collection/Slayer required

: null} -
-
+ ) : null} + {!craft.requiredCollection && !craft.requiredSlayer ?

No Collection/Slayer required

: null} + ) } - function getCraftHeader(craft: ProfitableCraft): JSX.Element { + function getCraftHeader(craft: ProfitableCraft): React.JSX.Element { return ( option.value === orderBy.value) - orderedCrafts = sortOption?.sortFunction(crafts, bazaarTags) + function filterFunction(craft: ProfitableCraft, nameFilter: string | null | undefined, minimumProfit: number): boolean { + const nameMatch = !nameFilter || (craft.item.name?.toLowerCase().includes(nameFilter.toLowerCase()) ?? false) + const profitMatch = craft.sellPrice - craft.craftCost >= minimumProfit + return nameMatch && profitMatch } - let shown = 0 - let list = orderedCrafts - .filter( - craft => - !((nameFilter && craft.item.name?.toLowerCase().indexOf(nameFilter.toLowerCase()) === -1) || craft.sellPrice - craft.craftCost < minimumProfit) - ) - .map(craft => { - shown++ - - if (!hasPremium && shown <= 3) { - let censoredCraft = { ...craft } - censoredCraft.item = { - tag: '', - name: 'ยง6You cheated the blur โ˜บ', - iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' - } - censoredCraft.craftCost = 42 - censoredCraft.sellPrice = 69 - censoredCraft.ingredients = [ - { - cost: 119999545.7, - count: 80, - item: { - tag: 'ASPECT_OF_THE_DRAGONS', - name: 'Sword', - iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' - } + function censoredItemGenerator(craft: ProfitableCraft): ProfitableCraft { + return { + ...craft, + item: { + tag: '', + name: 'ยง6You cheated the blur โ˜บ', + iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' + }, + craftCost: 42, + sellPrice: 69, + ingredients: [ + { + cost: 119999545.7, + count: 80, + item: { + tag: 'ASPECT_OF_THE_DRAGONS', + name: 'Sword', + iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' } - ] - censoredCraft.median = -1 - censoredCraft.volume = 123123 - censoredCraft.requiredCollection = undefined - censoredCraft.requiredSlayer = undefined - - return ( -
- {getListElement(censoredCraft, true)} -
- ) - } else { - return ( - } - /> - ) - } - }) + } + ], + median: -1, + volume: 123123, + requiredCollection: undefined, + requiredSlayer: undefined + } + } - let connectMinecraftTooltip = ( - connect your Minecraft Account
} - tooltipContent={ -
-

- To connect your Minecraft Account, search your ingame name in the search bar. On the player page you should see a text "You? Claim - account." -

+ // Wrapper function for rendering with tooltip + function customItemWrapper(craft: ProfitableCraft, blur: boolean, key: string, content: React.ReactNode, flipCardClass: string) { + if (blur) { + return ( +
+ {content}
- } - /> - ) - - const craftsListClass = `${styles.craftsList} ${styles[`columns-${columns}`]}` + ) + } else { + return ( + {content}} + tooltipTitle={getCraftHeader(craft)} + tooltipContent={} + /> + ) + } + } return ( -
-
- {isLoadingProfileData && isLoggedIn ? ( - getLoadingElement() - ) : ( -
- {!isLoggedIn ? ( -

To use the the profile filter, please login with Google and {connectMinecraftTooltip}:

- ) : !accountInfo?.mcId ? ( -

To use the the profile filter, please {connectMinecraftTooltip}

- ) : ( - '' - )} -
- )} - - {!isLoggedIn || !accountInfo?.mcId ?
: ''} -
-
- - - {SORT_OPTIONS.map(option => ( - - ))} - - - - - - - - - -
-
-

- Look at some advertising to get Starter Premium for free and see the top crafts -

-

Click on a craft for further details

-
- {list} -
-
+ craft.item.tag} + censoredItemGenerator={censoredItemGenerator} + premiumMessage="The top 3 crafts can only be seen with starter premium or better" + clickMessage="Click on a craft for further details" + showColumns={true} + sortFunctionArgs={[props.bazaarTags]} + customItemWrapper={customItemWrapper} + /> ) } diff --git a/components/FlippingHub/FlippingHub.tsx b/components/FlippingHub/FlippingHub.tsx index 0ad439642..5001b1791 100644 --- a/components/FlippingHub/FlippingHub.tsx +++ b/components/FlippingHub/FlippingHub.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { Container, Card, Row, Col } from 'react-bootstrap' -import { Pets, Build, Storefront, Agriculture, ShowChart, AutoFixHigh, Help, JoinFull, Volcano, QuestionMark } from '@mui/icons-material' +import { Pets, Build, Storefront, Agriculture, ShowChart, Handyman, Help, JoinFull, Volcano, QuestionMark } from '@mui/icons-material' import Tooltip from '../Tooltip/Tooltip' const flipKinds = [ @@ -32,7 +32,7 @@ const flipKinds = [ }, { name: 'Bazaar Flips', - path: undefined, + path: '/bazaar', description: Bazaar Flips are all about trading items on the Hypixel Bazaar for profit. Monitor market trends, find arbitrage opportunities, and learn the best flipping tactics for every item. Run /cofl bazaar with our mod to access, icon: }, @@ -48,6 +48,18 @@ const flipKinds = [ description: Use the Attribute Fusion Machine on galatea to combine two shards that you got from a buy order and create a sell order for the resulting shard. This flip is currently only available via /cofl fusionflip using our mod, icon: }, + { + name: 'Item Upgrade Flips', + path: undefined, + description: Use the the hex or just manually apply enchantments, Hot potato books etc to an item to make it worth more than it costs. Available via /cofl attributeflip using our mod, + icon: + }, + { + name: 'Ananke Feather Flips', + path: undefined, + description: Find the best items to use Ananke Feather on in RNG-Meters. Shows you the most profitable item to target, how much it costs and how much profit is to be made. Available via /cofl ananke using our mod, + icon: + }, { name: 'suggest new', path: 'https://discord.gg/wvKXfTgCfb', diff --git a/components/GenericFlipList/GenericFlipList.module.css b/components/GenericFlipList/GenericFlipList.module.css new file mode 100644 index 000000000..859a71f36 --- /dev/null +++ b/components/GenericFlipList/GenericFlipList.module.css @@ -0,0 +1,85 @@ +.genericFlipList :global(.list-group-item) { + width: -webkit-fill-available; + border-radius: 20px; +} + +.list { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + flex-direction: initial; +} + +#tooltip-container .tooltip-inner { + max-width: fit-content; +} + +.genericFlipList .flipCard { + width: calc(100% - 10px); + height: auto; + padding-bottom: 15px; +} + +@media all and (min-width: 768px) { + .genericFlipList .flipCard { + width: calc(50% - 10px); + } +} + +@media all and (min-width: 1424px) { + .genericFlipList .flipCard { + width: calc(33% - 10px); + } +} + +.genericFlipList .preventSelect { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.genericFlipList .label { + width: 150px; + float: left; +} + +.genericFlipList :global(.list-group-item) :global(p) { + margin: 0; +} + +.genericFlipList :global(.list-group-item) .craftRequirement { + text-align: right; +} + +.genericFlipList :global(.list-group-item) .craftRequirementLabel { + margin-right: 10px; +} + +/* Column-based layouts for CraftsList compatibility */ +.flipList.columns-1 .flipCard { + width: calc(100% - 10px); +} + +.flipList.columns-2 .flipCard { + width: calc(50% - 10px); +} + +.flipList.columns-3 .flipCard { + width: calc(33% - 10px); +} + +.flipList.columns-4 .flipCard { + width: calc(25% - 10px); +} + +.flipList.columns-5 .flipCard { + width: calc(20% - 10px); +} + +.filterInput { + width: 100%; + margin-right: 3vh; +} diff --git a/components/GenericFlipList/GenericFlipList.tsx b/components/GenericFlipList/GenericFlipList.tsx new file mode 100644 index 000000000..18faf6c47 --- /dev/null +++ b/components/GenericFlipList/GenericFlipList.tsx @@ -0,0 +1,307 @@ +'use client' +import React, { ChangeEvent, useEffect, useState, useMemo, useCallback } from 'react' +import { Form, ListGroup, Spinner } from 'react-bootstrap' +import Link from 'next/link' +import { hasHighEnoughPremium, PREMIUM_RANK } from '../../utils/PremiumTypeUtils' +import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' +import api from '../../api/ApiHelper' +import styles from './GenericFlipList.module.css' +import { useSortedAndFilteredItems } from '../../hooks/useSortedAndFilteredItems' + +export interface FlipListProps { + items: T[] + sortOptions: SortOption[] + renderFlipContentAction: (item: T) => React.ReactNode + onFlipClick?: (item: T, event: React.MouseEvent) => void + filterFunction?: (item: T, nameFilter: string | null | undefined, minimumProfit: number) => boolean + getItemKeyAction: (item: T) => string + censoredItemGenerator?: (item: T) => T + premiumMessage?: string + clickMessage?: string + showColumns?: boolean + customFilters?: React.ReactNode + sortFunctionArgs?: any[] + customItemWrapper?: (item: T, blur: boolean, key: string, content: React.ReactNode, flipCardClass: string) => React.ReactNode + onAfterSignIn?: () => void + customHeader?: (isLoggedIn: boolean) => React.ReactNode +} + +export interface SortOption { + label: string + value: string + sortFunction(items: T[], ...args: any[]): T[] +} + + +let observer: MutationObserver + +export function GenericFlipList({ + items, + sortOptions, + renderFlipContentAction, + onFlipClick, + filterFunction, + getItemKeyAction, + censoredItemGenerator, + premiumMessage = "The top 3 flips can only be seen with starter premium or better", + clickMessage = "Click on a flip for further details", + showColumns = false, + customFilters, + sortFunctionArgs = [], + customItemWrapper, + onAfterSignIn, + customHeader +}: FlipListProps) { + const [nameFilter, setNameFilter] = useState() + const [minimumProfit, setMinimumProfit] = useState(0) + const [orderBy, setOrderBy] = useState>(sortOptions[0]) + const [hasPremium, setHasPremium] = useState(false) + const [isLoggedIn, setIsLoggedIn] = useState(false) + const [showTechSavvyMessage, setShowTechSavvyMessage] = useState(false) + const [columns, setColumns] = useState() + + const { processedItems, isProcessing } = useSortedAndFilteredItems( + items, + orderBy, + nameFilter, + minimumProfit, + filterFunction, + sortFunctionArgs + ) + + useEffect(() => { + // reset the blur observer, when something changed + setTimeout(setBlurObserver, 100) + if (showColumns) { + setColumns(getDefaultColumns()) + } + }, []) + + function setBlurObserver() { + if (observer) { + observer.disconnect() + } + observer = new MutationObserver(function () { + setShowTechSavvyMessage(true) + }) + + var targets = document.getElementsByClassName('blur') + for (var i = 0; i < targets.length; i++) { + var config = { + attributes: true, + childList: true, + characterData: true, + attributeFilter: ['style'] + } + observer.observe(targets[i], config) + } + } + + function getDefaultColumns() { + const screenWidth = window.innerWidth + + if (screenWidth >= 1424) { + return 3 + } else if (screenWidth >= 768) { + return 2 + } else { + return 1 + } + } + + function onAfterLogin() { + setIsLoggedIn(true) + api.refreshLoadPremiumProducts(products => { + setHasPremium(hasHighEnoughPremium(products, PREMIUM_RANK.STARTER)) + }) + + // Call the custom onAfterSignIn if provided + if (onAfterSignIn) { + onAfterSignIn() + } + } + + const onNameFilterChange = useCallback((e: any) => { + setNameFilter(e.target.value) + }, []) + + const onMinimumProfitChange = useCallback((e: any) => { + setMinimumProfit(e.target.value) + }, []) + + const updateOrderBy = useCallback((event: ChangeEvent) => { + if (!isProcessing) { + let selectedIndex = event.target.options.selectedIndex + let value = event.target.options[selectedIndex].getAttribute('value')! + let sortOption = sortOptions.find(option => option.value === value) + if (sortOption) { + setOrderBy(sortOption) + } + } + }, [isProcessing, sortOptions]) + + const handleColumnChange = useCallback((event: ChangeEvent) => { + if (!isProcessing) { + const value = parseInt(event.target.value, 10) + setColumns(value) + } + }, [isProcessing]) + + const blurStyle: React.CSSProperties = { + WebkitFilter: 'blur(5px)', + msFilter: 'blur(5px)', + filter: 'blur(5px)', + pointerEvents: 'none' + } + + function getListElement(item: T, blur: boolean) { + return ( + { + if (onFlipClick && !blur) { + onFlipClick(item, e) + } + }} + style={{ height: '100%', padding: '15px' }} + > + {blur ? ( +

+ {premiumMessage} +

+ ) : null} + {showTechSavvyMessage && blur ? ( +

+ You seem like a tech savvy person, contribute to the project to get premium for free. :) +

+ ) : null} +
+ {renderFlipContentAction(item)} +
+
+ ) + } + + // Memoized displayed items + const list = useMemo(() => { + if (isProcessing) { + return [] + } + + let shown = 0 + const list = processedItems.map(item => { + const defaultContent = getListElement(item, false) + + if (!hasPremium && ++shown <= 3) { + const censoredItem = censoredItemGenerator ? censoredItemGenerator(item) : item + const censoredContent = getListElement(censoredItem, true) + + if (customItemWrapper) { + return customItemWrapper(censoredItem, true, getItemKeyAction(item), censoredContent, styles.flipCard) + } + + return ( +
+ {censoredContent} +
+ ) + } else { + if (customItemWrapper) { + return customItemWrapper(item, false, getItemKeyAction(item), defaultContent, styles.flipCard) + } + + return ( +
+ {defaultContent} +
+ ) + } + }) + + return list + }, [processedItems, hasPremium, isProcessing, censoredItemGenerator, customItemWrapper]) + + const flipListClass = showColumns && columns ? `${styles.flipList} ${styles[`columns-${columns}`]}` : styles.flipList + return ( +
+ {customHeader && customHeader(isLoggedIn)} +
+ + {!isLoggedIn ?
: ''} +
+
+ + + {sortOptions.map(option => ( + + ))} + + + {showColumns && ( + + + + + + + + )} + {customFilters} +
+
+ {hasPremium ? null : ( +

+ Click here to get Starter Premium for free to see the top flips +

+ )} +

{clickMessage}

+
+ {isProcessing ? ( +
+ + Processing items... +
+ ) : ( + {list} + )} +
+
+ ) +} \ No newline at end of file diff --git a/components/GenericFlipList/index.ts b/components/GenericFlipList/index.ts new file mode 100644 index 000000000..32d97d4e2 --- /dev/null +++ b/components/GenericFlipList/index.ts @@ -0,0 +1,2 @@ +export { GenericFlipList } from './GenericFlipList' +export type { SortOption, FlipListProps } from './GenericFlipList' diff --git a/components/KatFlips/KatFlips.tsx b/components/KatFlips/KatFlips.tsx index e1a85f701..6dc47776c 100644 --- a/components/KatFlips/KatFlips.tsx +++ b/components/KatFlips/KatFlips.tsx @@ -1,119 +1,49 @@ 'use client' import Image from 'next/image' import Link from 'next/link' -import React, { ChangeEvent, useEffect, useMemo, useState } from 'react' -import { Form, ListGroup } from 'react-bootstrap' +import React, { useMemo } from 'react' import { toast } from 'react-toastify' import api from '../../api/ApiHelper' import { convertTagToName, getStyleForTier } from '../../utils/Formatter' -import { hasHighEnoughPremium, PREMIUM_RANK } from '../../utils/PremiumTypeUtils' -import GoogleSignIn from '../GoogleSignIn/GoogleSignIn' import Number from '../Number/Number' -import styles from './KatFlips.module.css' import { parseKatFlip } from '../../utils/Parser/APIResponseParser' import { writeToClipboard } from '../../utils/ClipboardUtils' +import { GenericFlipList, SortOption } from '../GenericFlipList' interface Props { flips: any[] } -interface SortOption { - label: string - value: string - sortFunction(flips: KatFlip[]) -} -const SORT_OPTIONS: SortOption[] = [ + +const SORT_OPTIONS: SortOption[] = [ { label: 'Profit', value: 'profit', - sortFunction: crafts => crafts.sort((a, b) => b.profit - a.profit) + sortFunction: flips => flips.sort((a, b) => b.profit - a.profit) }, { label: 'Time โ‡ง', value: 'timeAsc', - sortFunction: crafts => crafts.sort((a, b) => b.coreData.hours - a.coreData.hours) + sortFunction: flips => flips.sort((a, b) => b.coreData.hours - a.coreData.hours) }, { label: 'Time โ‡ฉ', value: 'timeDesc', - sortFunction: crafts => crafts.sort((a, b) => a.coreData.hours - b.coreData.hours) + sortFunction: flips => flips.sort((a, b) => a.coreData.hours - b.coreData.hours) }, { label: 'Profit/Time', value: 'profitPerTime', - sortFunction: crafts => crafts.sort((a, b) => b.profit / b.coreData.hours - a.profit / a.coreData.hours) + sortFunction: flips => flips.sort((a, b) => b.profit / b.coreData.hours - a.profit / a.coreData.hours) } ] -let observer: MutationObserver - export function KatFlips(props: Props) { - let [nameFilter, setNameFilter] = useState() - let [minimumProfit, setMinimumProfit] = useState(0) - let [orderBy, setOrderBy] = useState(SORT_OPTIONS[0]) - let [hasPremium, setHasPremium] = useState(false) - let [isLoggedIn, setIsLoggedIn] = useState(false) - let [showTechSavvyMessage, setShowTechSavvyMessage] = useState(false) - - let flips = useMemo(() => { + const flips = useMemo(() => { return (props.flips ? props.flips.map(parseKatFlip) : []) as KatFlip[] }, [props.flips]) - useEffect(() => { - // reset the blur observer, when something changed - setTimeout(setBlurObserver, 100) - }) - - function setBlurObserver() { - if (observer) { - observer.disconnect() - } - observer = new MutationObserver(function () { - setShowTechSavvyMessage(true) - }) - - var targets = document.getElementsByClassName('blur') - for (var i = 0; i < targets.length; i++) { - var config = { - attributes: true, - childList: true, - characterData: true, - attributeFilter: ['style'] - } - observer.observe(targets[i], config) - } - } - - function onAfterLogin() { - setIsLoggedIn(true) - return api.refreshLoadPremiumProducts(products => { - setHasPremium(hasHighEnoughPremium(products, PREMIUM_RANK.STARTER)) - }) - } - - function onNameFilterChange(e: any) { - setNameFilter(e.target.value) - } - function onMinimumProfitChange(e: any) { - setMinimumProfit(e.target.value) - } - function updateOrderBy(event: ChangeEvent) { - let selectedIndex = event.target.options.selectedIndex - let value = event.target.options[selectedIndex].getAttribute('value')! - let sortOption = SORT_OPTIONS.find(option => option.value === value) - if (sortOption) { - setOrderBy(sortOption) - } - } - - let blurStyle: React.CSSProperties = { - WebkitFilter: 'blur(5px)', - msFilter: 'blur(5px)', - filter: 'blur(5px)', - pointerEvents: 'none' - } - - function onFlipClick(e, flip: KatFlip) { - if (e.defaultPrevented || !flip.originAuctionUUID) { + function onFlipClick(flip: KatFlip, event: React.MouseEvent) { + if (event.defaultPrevented || !flip.originAuctionUUID) { return } writeToClipboard('/viewauction ' + flip.originAuctionUUID) @@ -130,91 +60,57 @@ export function KatFlips(props: Props) { ) } - function getListElement(flip: KatFlip, blur: boolean) { - if (nameFilter && flip.coreData.item.name?.toLowerCase().indexOf(nameFilter.toLowerCase()) === -1 && !blur) { - return - } + function renderFlipContent(flip: KatFlip) { return ( - { - onFlipClick(e, flip) - }} - style={{ height: '100%', padding: '15px' }} - > - {blur ? ( -

- The top 3 flips can only be seen with starter premium or better -

- ) : null} - {showTechSavvyMessage && blur ? ( -

- You seem like a tech savvy person, contribute to the project to get premium for free. :) -

- ) : ( - '' - )} -
-

{getFlipHeader(flip)}

-

- Purchase Cost:{' '} - - Coins - -

-

- Upgrade Cost: - Coins -

-

- Median: Coins -

-

- Profit:{' '} - - Coins - -

-
-

- Volume: -

-

- Target Rarity: {flip.targetRarity} -

-

- Time: {flip.coreData.hours} Hours -

-
-

- Material Cost: Coins -

- {flip.coreData?.materials && Object.keys(flip.coreData?.materials).length > 0 ? ( -
- Material{Object.keys(flip.coreData.materials).length > 1 ? 's' : ''}: -
- {Object.entries(flip.coreData.materials).map(([material, amount]) => { - return ( -

- {`${amount}x ${convertTagToName(material)}`} -

- ) - })} -
-
) : null} -
-
+ <> +

{getFlipHeader(flip)}

+

+ Purchase Cost:{' '} + + Coins + +

+

+ Upgrade Cost: + Coins +

+

+ Median: Coins +

+

+ Profit:{' '} + + Coins + +

+
+

+ Volume: +

+

+ Target Rarity: {flip.targetRarity} +

+

+ Time: {flip.coreData.hours} Hours +

+
+

+ Material Cost: Coins +

+ {flip.coreData?.materials && Object.keys(flip.coreData?.materials).length > 0 ? ( +
+ Material{Object.keys(flip.coreData.materials).length > 1 ? 's' : ''}: +
+ {Object.entries(flip.coreData.materials).map(([material, amount]) => { + return ( +

+ {`${amount}x ${convertTagToName(material)}`} +

+ ) + })} +
+
) : null} + ) } @@ -235,80 +131,50 @@ export function KatFlips(props: Props) { ) } - let orderedFlips = [...flips] - if (orderBy) { - let sortOption = SORT_OPTIONS.find(option => option.value === orderBy.value) - orderedFlips = sortOption?.sortFunction([...flips]) + function filterFunction(flip: KatFlip, nameFilter: string | null | undefined, minimumProfit: number): boolean { + const nameMatch = !nameFilter || (flip.coreData.item.name?.toLowerCase().includes(nameFilter.toLowerCase()) ?? false) + const profitMatch = flip.profit >= minimumProfit + return nameMatch && profitMatch } - let shown = 0 - let list = orderedFlips - .filter(flip => !((nameFilter && flip.coreData.item.name?.toLowerCase().indexOf(nameFilter.toLowerCase()) === -1) || flip.profit < minimumProfit)) - .map(flip => { - if (!hasPremium && ++shown <= 3) { - let censoredFlip: KatFlip = { ...flip } - censoredFlip.coreData = { - amount: -1, - hours: 69, - item: { - tag: '', - name: 'You cheated the blur โ˜บ', - tier: 'LEGENDARY', - iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' - }, - materials: { 'CoflCoin': 1 } - } - censoredFlip.cost = 12345 - censoredFlip.materialCost = 696969 - censoredFlip.median = 424242 - censoredFlip.originAuctionUUID = '' - censoredFlip.profit = -100000 - censoredFlip.purchaseCost = 1 - censoredFlip.referenceAuctionUUID = '' - censoredFlip.volume = -1 - censoredFlip.upgradeCost = 0 - censoredFlip.originAuctionName = 'You cheated the blur โ˜บ' - return ( -
- {getListElement(censoredFlip, true)} -
- ) - } else { - return ( -
- {getListElement(flip, false)} -
- ) - } - }) + function censoredItemGenerator(flip: KatFlip): KatFlip { + return { + ...flip, + coreData: { + amount: -1, + hours: 69, + item: { + tag: '', + name: 'You cheated the blur โ˜บ', + tier: 'LEGENDARY', + iconUrl: 'https://sky.coflnet.com/static/icon/BARRIER' + }, + materials: { 'CoflCoin': 1 } + }, + cost: 12345, + materialCost: 696969, + median: 424242, + originAuctionUUID: '', + profit: -100000, + purchaseCost: 1, + referenceAuctionUUID: '', + volume: -1, + upgradeCost: 0, + originAuctionName: 'You cheated the blur โ˜บ' + } + } return ( -
-
- - {!isLoggedIn ?
: ''} -
-
- - - {SORT_OPTIONS.map(option => ( - - ))} - - -
-
- {hasPremium ? null : ( -

- Click here to get Starter Premium for free to see the top kat flips -

- )} -

Click on a craft for further details

-
- {list} -
-
+ flip.originAuctionUUID || ''} + censoredItemGenerator={censoredItemGenerator} + premiumMessage="The top 3 flips can only be seen with starter premium or better" + clickMessage="Click on a flip for further details" + /> ) } diff --git a/components/PriceGraph/BazaarPriceGraph/BazaarPriceGraph.tsx b/components/PriceGraph/BazaarPriceGraph/BazaarPriceGraph.tsx index b9db85b76..336ea7c6a 100644 --- a/components/PriceGraph/BazaarPriceGraph/BazaarPriceGraph.tsx +++ b/components/PriceGraph/BazaarPriceGraph/BazaarPriceGraph.tsx @@ -461,7 +461,7 @@ function BazaarPriceGraph(props: Props) {

- + diff --git a/components/Providers/Providers.tsx b/components/Providers/Providers.tsx index ef1fe0703..8022e2d0d 100644 --- a/components/Providers/Providers.tsx +++ b/components/Providers/Providers.tsx @@ -1,35 +1,14 @@ 'use client' import { MatomoProvider, createInstance } from '@jonkoops/matomo-tracker-react' import { GoogleOAuthProvider } from '@react-oauth/google' -import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { QueryClientProvider } from '@tanstack/react-query' +import { getQueryClient } from '../../utils/QueryUtils' const matomoTrackingInstance = createInstance({ urlBase: 'https://track.coflnet.com', siteId: 1 }) -function makeQueryClient() { - return new QueryClient({ - defaultOptions: { - queries: { - staleTime: 60 * 1000, - }, - }, - }) -} - -let browserQueryClient: QueryClient | undefined = undefined - -// https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr -function getQueryClient() { - if (isServer) { - return makeQueryClient() - } else { - if (!browserQueryClient) browserQueryClient = makeQueryClient() - return browserQueryClient - } -} - export function Providers({ children }) { const queryClient = getQueryClient() return ( diff --git a/components/RelatedItems/RelatedItems.tsx b/components/RelatedItems/RelatedItems.tsx index 6f764d94f..cd65c1b8e 100644 --- a/components/RelatedItems/RelatedItems.tsx +++ b/components/RelatedItems/RelatedItems.tsx @@ -11,6 +11,7 @@ import Image from 'next/image' interface Props { tag: string + isBazaarItem?: boolean } function RelatedItems(props: Props) { @@ -33,6 +34,26 @@ function RelatedItems(props: Props) { return null } + // Show bazaar flip notice if it's a bazaar item and there are no related items + if (props.isBazaarItem && relatedItems.length === 0) { + return ( +
+

Similar items

+

+ No similar items found. Looking for flip opportunities?{' '} + + Check out Bazaar Flips โ†’ + +

+
+ ) + } + + // If no related items for non-bazaar items, don't show anything + if (relatedItems.length === 0) { + return null + } + return ( <> {!hide ? ( diff --git a/cypress/e2e/crafts.cy.ts b/cypress/e2e/crafts.cy.ts index 6e2976652..e3dfa78ff 100644 --- a/cypress/e2e/crafts.cy.ts +++ b/cypress/e2e/crafts.cy.ts @@ -6,8 +6,7 @@ describe('Profitable craft page', () => { it('works', () => { cy.visit('/crafts') cy.contains('The top 3 crafts can only be seen with starter premium or betterYou Cheated the ').should('be.visible') - cy.wait(1000) - cy.contains('button', 'Crafting Cost').should('be.visible').click() + cy.contains('button', 'Crafting Cost').scrollIntoView().children().first().should('be.visible').click() cy.contains('h3', 'Recipe').should('be.visible') cy.contains(/\)[\.,\d]* Coins.*/).should('be.visible') }) diff --git a/global.d.ts b/global.d.ts index fde168b16..70d364d1f 100644 --- a/global.d.ts +++ b/global.d.ts @@ -387,6 +387,11 @@ interface ReloadFeedback { additionalInformation: string } +interface ErrorReportData { + userDescription: string + includeErrorDetails: boolean +} + interface ProfitableCraft { item: Item sellPrice: number diff --git a/hooks/useSortedAndFilteredItems.tsx b/hooks/useSortedAndFilteredItems.tsx new file mode 100644 index 000000000..6aedbf7fd --- /dev/null +++ b/hooks/useSortedAndFilteredItems.tsx @@ -0,0 +1,170 @@ +import { useState, useEffect, useRef, useCallback } from 'react' + +export interface SortOption { + label: string + value: string + sortFunction(items: T[], ...args: any[]): T[] +} + +// Utility function to process items in chunks to avoid blocking the main thread +async function processInChunks( + items: T[], + processor: (item: T) => boolean, + chunkSize: number = 100 +): Promise { + const result: T[] = [] + + for (let i = 0; i < items.length; i += chunkSize) { + const chunk = items.slice(i, i + chunkSize) + const chunkResult = chunk.filter(processor) + result.push(...chunkResult) + + if (i + chunkSize < items.length) { + await new Promise(resolve => setTimeout(resolve, 0)) + } + } + + return result +} + +// Custom hook for sorting and filtering items +export function useSortedAndFilteredItems( + items: T[], + orderBy: SortOption | null, + nameFilter: string | null | undefined, + minimumProfit: number, + filterFunction?: (item: T, nameFilter: string | null | undefined, minimumProfit: number) => boolean, + sortFunctionArgs: any[] = [], + debounceMs: number = 300 +) { + const [processedItems, setProcessedItems] = useState(items) + const [isProcessing, setIsProcessing] = useState(false) + const [debouncedNameFilter, setDebouncedNameFilter] = useState(nameFilter) + const [debouncedMinimumProfit, setDebouncedMinimumProfit] = useState(minimumProfit) + + const processingIdRef = useRef(0) + const abortControllerRef = useRef(null) + + // Debounce the name filter + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedNameFilter(nameFilter) + }, debounceMs) + + return () => clearTimeout(timer) + }, [nameFilter, debounceMs]) + + // Debounce the minimum profit filter + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedMinimumProfit(minimumProfit) + }, debounceMs) + + return () => clearTimeout(timer) + }, [minimumProfit, debounceMs]) + + const processItems = useCallback(async ( + currentItems: T[], + currentOrderBy: SortOption | null, + currentNameFilter: string | null | undefined, + currentMinimumProfit: number, + currentFilterFunction?: (item: T, nameFilter: string | null | undefined, minimumProfit: number) => boolean, + currentSortFunctionArgs: any[] = [] + ) => { + const processingId = ++processingIdRef.current + + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + + const abortController = new AbortController() + abortControllerRef.current = abortController + + try { + setIsProcessing(true) + + if (currentItems.length === 0) { + if (processingId === processingIdRef.current && !abortController.signal.aborted) { + setProcessedItems([]) + } + return + } + + await new Promise(resolve => setTimeout(resolve, 0)) + + if (processingId !== processingIdRef.current || abortController.signal.aborted) { + return + } + + let sortedItems = [...currentItems] + if (currentOrderBy) { + if (currentItems.length > 1000) { + await new Promise(resolve => setTimeout(resolve, 0)) + } + + if (processingId !== processingIdRef.current || abortController.signal.aborted) { + return + } + + sortedItems = currentOrderBy.sortFunction([...currentItems], ...currentSortFunctionArgs) + } + + if (processingId !== processingIdRef.current || abortController.signal.aborted) { + return + } + + let filteredItems: T[] + + if (!currentFilterFunction) { + filteredItems = sortedItems + } else { + const optimizedFilter = (item: T) => { + return currentFilterFunction(item, currentNameFilter, currentMinimumProfit) + } + + if (sortedItems.length > 500) { + filteredItems = await processInChunks(sortedItems, optimizedFilter, 100) + } else { + filteredItems = sortedItems.filter(optimizedFilter) + } + } + + if (processingId === processingIdRef.current && !abortController.signal.aborted) { + setProcessedItems(filteredItems) + } + + } catch (error) { + if (!abortController.signal.aborted) { + console.error('Error during processing:', error) + if (processingId === processingIdRef.current) { + setProcessedItems([...currentItems]) + } + } + } finally { + if (processingId === processingIdRef.current) { + setIsProcessing(false) + } + } + }, []) + + useEffect(() => { + processItems( + items, + orderBy, + debouncedNameFilter, + debouncedMinimumProfit, + filterFunction, + sortFunctionArgs + ) + }, [items, orderBy, debouncedNameFilter, debouncedMinimumProfit, filterFunction, processItems]) + + useEffect(() => { + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + } + }, []) + + return { processedItems, isProcessing } +} diff --git a/package-lock.json b/package-lock.json index bc1a4dcb1..485f1a5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "devDependencies": { "@types/react": "^19.1.0", "cross-env": "^7.0.3 ", - "cypress": "^13.6.1", + "cypress": "^14.5.4", "eslint-config-next": "^15.2.4", "orval": "^7.11.2", "typescript": "^5.8.3" @@ -286,21 +286,12 @@ "node": ">=6.9.0" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/@cypress/request": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.8.tgz", - "integrity": "sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.9.tgz", + "integrity": "sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -308,7 +299,7 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~4.0.0", + "form-data": "~4.0.4", "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -330,6 +321,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -2434,6 +2426,19 @@ "lodash.uniq": "^4.5.0" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3238,13 +3243,15 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/sizzle": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/unist": { "version": "3.0.3", @@ -3268,6 +3275,7 @@ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -3737,6 +3745,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -3815,6 +3824,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -3867,7 +3877,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", @@ -4048,6 +4059,7 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } @@ -4057,6 +4069,7 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } @@ -4072,6 +4085,7 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4088,7 +4102,8 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/async-function": { "version": "1.0.0", @@ -4103,7 +4118,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -4134,6 +4150,7 @@ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } @@ -4142,7 +4159,8 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/axe-core": { "version": "4.10.3", @@ -4288,6 +4306,7 @@ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -4306,13 +4325,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.12", @@ -4364,6 +4385,7 @@ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -4393,6 +4415,7 @@ "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4481,7 +4504,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -4566,6 +4590,7 @@ "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -4591,9 +4616,9 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "node_modules/ci-info": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "dev": true, "funding": [ { @@ -4601,6 +4626,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -4632,10 +4658,11 @@ } }, "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.1.tgz", + "integrity": "sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -4643,7 +4670,7 @@ "node": "10.* || >= 12.*" }, "optionalDependencies": { - "@colors/colors": "1.5.0" + "colors": "1.4.0" } }, "node_modules/cli-truncate": { @@ -4662,6 +4689,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4742,11 +4784,23 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4769,6 +4823,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4778,6 +4833,7 @@ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -4808,7 +4864,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "7.1.0", @@ -4863,13 +4920,14 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/cypress": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", - "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", + "version": "14.5.4", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.4.tgz", + "integrity": "sha512-0Dhm4qc9VatOcI1GiFGVt8osgpPdqJLHzRwcAB5MSD/CAAts3oybvPUPawHyvJZUd8osADqZe/xzMsZ8sDTjXw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { - "@cypress/request": "^3.0.6", + "@cypress/request": "^3.0.9", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -4880,9 +4938,9 @@ "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", - "ci-info": "^4.0.0", + "ci-info": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-table3": "~0.6.1", + "cli-table3": "0.6.1", "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", @@ -4895,6 +4953,7 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", + "hasha": "5.2.2", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", @@ -4906,7 +4965,7 @@ "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.5.3", + "semver": "^7.7.1", "supports-color": "^8.1.1", "tmp": "~0.2.3", "tree-kill": "1.2.2", @@ -4917,7 +4976,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" } }, "node_modules/damerau-levenshtein": { @@ -4931,6 +4990,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -5002,7 +5062,8 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/debug": { "version": "4.4.1", @@ -5101,6 +5162,7 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -5195,6 +5257,7 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -5646,6 +5709,22 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -5806,6 +5885,37 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -6093,13 +6203,15 @@ "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/execa": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -6123,6 +6235,7 @@ "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.2.0" }, @@ -6182,13 +6295,21 @@ "dev": true, "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -6278,6 +6399,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } @@ -6287,6 +6409,7 @@ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -6302,6 +6425,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -6393,6 +6517,7 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } @@ -6402,6 +6527,7 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6423,6 +6549,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -6521,6 +6648,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -6574,6 +6702,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -6607,6 +6736,7 @@ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, + "license": "MIT", "dependencies": { "ini": "2.0.0" }, @@ -6802,6 +6932,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6938,6 +7095,7 @@ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", @@ -6958,6 +7116,7 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8.12.0" } @@ -7035,6 +7194,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7049,6 +7209,7 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -7342,6 +7503,7 @@ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, + "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" @@ -7407,6 +7569,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7532,7 +7695,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -7605,7 +7769,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/iterator.prototype": { "version": "1.1.5", @@ -7653,7 +7818,8 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsep": { "version": "1.4.0", @@ -7691,7 +7857,8 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7711,7 +7878,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json5": { "version": "1.0.2", @@ -7787,6 +7955,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -7851,6 +8020,7 @@ "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", "dev": true, + "license": "MIT", "engines": { "node": "> 0.8" } @@ -7897,6 +8067,7 @@ "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", @@ -7968,7 +8139,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.topath": { "version": "4.5.2", @@ -7999,6 +8171,7 @@ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -8015,6 +8188,7 @@ "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", @@ -8028,28 +8202,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8952,6 +9110,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8961,6 +9120,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -12650,7 +12810,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/own-keys": { "version": "1.0.1", @@ -12704,6 +12865,7 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -12808,7 +12970,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -12832,6 +12995,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12942,11 +13106,41 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -12959,6 +13153,7 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -13009,7 +13204,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pump": { "version": "3.0.2", @@ -13044,6 +13240,7 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -13681,6 +13878,7 @@ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", "dev": true, + "license": "MIT", "dependencies": { "throttleit": "^1.0.0" } @@ -13744,6 +13942,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -13766,7 +13965,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/run-parallel": { "version": "1.2.0", @@ -13796,6 +13996,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -13881,7 +14082,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", @@ -14229,17 +14431,21 @@ } }, "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/source-map": { @@ -14279,6 +14485,7 @@ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -14598,6 +14805,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -14646,6 +14854,22 @@ "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -14695,6 +14919,7 @@ "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -14703,7 +14928,8 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.12", @@ -14748,22 +14974,24 @@ } }, "node_modules/tldts": { - "version": "6.1.85", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.85.tgz", - "integrity": "sha512-gBdZ1RjCSevRPFix/hpaUWeak2/RNUZB4/8frF1r5uYMHjFptkiT0JXIebWvgI/0ZHXvxaUDDJshiA0j6GdL3w==", + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, + "license": "MIT", "dependencies": { - "tldts-core": "^6.1.85" + "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.85", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.85.tgz", - "integrity": "sha512-DTjUVvxckL1fIoPSb3KE7ISNtkWSawZdpfxGxwiIrZoO6EbHVDXXUIlIuWympPaeS+BLGyggozX/HTMsRAdsoA==", - "dev": true + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" }, "node_modules/tmp": { "version": "0.2.5", @@ -14796,6 +15024,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" }, @@ -14814,6 +15043,7 @@ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } @@ -14907,7 +15137,8 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", @@ -14927,6 +15158,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -15324,6 +15556,7 @@ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -15422,6 +15655,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -15687,6 +15921,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/package.json b/package.json index f491e7f71..8e5fa865c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "devDependencies": { "@types/react": "^19.1.0", "cross-env": "^7.0.3 ", - "cypress": "^13.6.1", + "cypress": "^14.5.4", "eslint-config-next": "^15.2.4", "orval": "^7.11.2", "typescript": "^5.8.3" diff --git a/utils/QueryUtils.tsx b/utils/QueryUtils.tsx new file mode 100644 index 000000000..c912bef2d --- /dev/null +++ b/utils/QueryUtils.tsx @@ -0,0 +1,46 @@ +// utils/QueryUtils.tsx +import { + isServer, + QueryClient, + defaultShouldDehydrateQuery, +} from '@tanstack/react-query' + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + dehydrate: { + // include pending queries in dehydration + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + shouldRedactErrors: (error) => { + // We should not catch Next.js server errors + // as that's how Next.js detects dynamic pages + // so we cannot redact them. + // Next.js also automatically redacts errors for us + // with better digests. + return false + }, + }, + }, + }) +} + +let browserQueryClient: QueryClient | undefined = undefined + +export function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient() + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient() + return browserQueryClient + } +} \ No newline at end of file