diff --git a/src/echo-app/package-lock.json b/src/echo-app/package-lock.json index f9ac83be..796a21a2 100644 --- a/src/echo-app/package-lock.json +++ b/src/echo-app/package-lock.json @@ -69,6 +69,7 @@ "pino-pretty": "^10.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hook-form": "^7.49.2", "react-i18next": "^13.5.0", "rxjs": "^7.8.1", @@ -422,6 +423,7 @@ "object-hash": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hook-form": "^7.49.2", "react-i18next": "^13.4.1", "rxjs": "^7.8.1", @@ -12992,6 +12994,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.12.tgz", + "integrity": "sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.49.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", diff --git a/src/echo-app/package.json b/src/echo-app/package.json index e2e23ee4..6ac330e0 100644 --- a/src/echo-app/package.json +++ b/src/echo-app/package.json @@ -87,6 +87,7 @@ "pino-pretty": "^10.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hook-form": "^7.49.2", "react-i18next": "^13.5.0", "rxjs": "^7.8.1", diff --git a/src/echo-plugins/networth-plugin/package-lock.json b/src/echo-plugins/networth-plugin/package-lock.json index 5cf4d33b..7a93cfad 100644 --- a/src/echo-plugins/networth-plugin/package-lock.json +++ b/src/echo-plugins/networth-plugin/package-lock.json @@ -67,6 +67,7 @@ "object-hash": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hook-form": "^7.49.2", "react-i18next": "^13.4.1", "rxjs": "^7.8.1", @@ -26354,6 +26355,18 @@ "resolved": "../../echo-common/node_modules/react-dom", "link": true }, + "node_modules/react-error-boundary": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.12.tgz", + "integrity": "sha512-kJdxdEYlb7CPC1A0SeUY38cHpjuu6UkvzKiAmqmOFL21VRfMhOcWxTCBgLVCO0VEMh9JhFNcVaXlV4/BTpiwOA==", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.49.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", diff --git a/src/echo-plugins/networth-plugin/package.json b/src/echo-plugins/networth-plugin/package.json index ffa13202..a48de145 100644 --- a/src/echo-plugins/networth-plugin/package.json +++ b/src/echo-plugins/networth-plugin/package.json @@ -42,6 +42,7 @@ "object-hash": "^3.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.12", "react-hook-form": "^7.49.2", "react-i18next": "^13.4.1", "rxjs": "^7.8.1", diff --git a/src/echo-plugins/networth-plugin/src/components/Columns/Columns.tsx b/src/echo-plugins/networth-plugin/src/components/Columns/Columns.tsx index 352b66b5..8baf5c3b 100644 --- a/src/echo-plugins/networth-plugin/src/components/Columns/Columns.tsx +++ b/src/echo-plugins/networth-plugin/src/components/Columns/Columns.tsx @@ -1,9 +1,9 @@ import { ColumnDef, filterFns } from '@tanstack/react-table' import { rarityColors, currencyChangeColors } from '../../assets/theme' -import { getRarity, parseTabNames } from '../../utils/item.utils' +import { getRarity, parseTabNames, parseUnsafeHashProps } from '../../utils/item.utils' import { SageValuation, cn } from 'echo-common' import { CurrencySwitch } from '../../store/settingStore' -import { IPricedItem } from '../../interfaces/priced-item.interface' +import { IDisplayedItem } from '../../interfaces/priced-item.interface' import { observer } from 'mobx-react' import { useStore } from '../../hooks/useStore' import { TableColumnHeader } from './ColumnHeader' @@ -14,18 +14,18 @@ import { formatValue } from '../../utils/currency.utils' import * as Highcharts from 'highcharts' import HighchartsReact from 'highcharts-react-official' import { Badge } from 'echo-common/components-v1' +import { SageItemGroup } from 'sage-common' -type PricedItem = keyof IPricedItem | 'cumulative' +type DisplayedItem = keyof IDisplayedItem | keyof SageItemGroup | 'cumulative' -export function itemIcon(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { header, accessorKey } = options +export function itemIcon(): ColumnDef { + const key: DisplayedItem = 'icon' + const header = 'icon' return { header: ({ column }) => , - accessorKey, + accessorKey: key, + accessorFn: (val) => val.icon, size: 65, minSize: 52, enableSorting: false, @@ -34,21 +34,19 @@ export function itemIcon(options: { headerWording: header }, cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function itemName(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { header, accessorKey } = options +export function itemName(): ColumnDef { + const key: DisplayedItem = 'displayName' + const header = 'name' return { header: ({ column }) => , - accessorKey, + accessorKey: key, enableSorting: true, enableGlobalFilter: true, size: 300, @@ -57,21 +55,20 @@ export function itemName(options: { headerWording: header }, cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function itemTag(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { header, accessorKey } = options +export function itemTag(): ColumnDef { + const key: DisplayedItem = 'tag' + const header = 'tag' return { header: ({ column }) => , - accessorKey, + accessorKey: key, + accessorFn: (val) => val.group?.tag, enableSorting: true, enableGlobalFilter: true, size: 65, @@ -80,51 +77,20 @@ export function itemTag(options: { headerWording: header }, cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function itemProps(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { header, accessorKey } = options +export function itemProps(): ColumnDef { + const key: DisplayedItem = 'unsafeHashProperties' + const header = 'properties' return { header: ({ column }) => , - accessorKey, - accessorFn: (val) => { - const hashProps = Object.entries(val.unsafeHashProperties || {}) - .filter(([name, value]) => { - const excludeNameByIncludes = (name: string) => - !['mods', 'Mods'].some((key) => name.includes(key)) - - const excludeFalseValues = (value: any) => { - if (!value || value === '0') return false - return true - } - return excludeNameByIncludes(name) && excludeFalseValues(value) - }) - .map(([name, value]) => { - let displayValue = `${name}: ${value}` - // Name only - if (value === true) { - displayValue = name - } - return { name, value, displayValue } - }) - - if (val.key && val.name.toLowerCase() !== val.key.toLowerCase()) { - // Used for contract or cluster jewels - hashProps.push({ name: val.name, value: val.key, displayValue: val.key }) - } - - hashProps.sort((a, b) => a.displayValue.length - b.displayValue.length) - // Filterfn does not work, when an object is returned - return hashProps.map((p) => `${p.name};;${p.displayValue}`).join(';;;') - }, + accessorKey: key, + accessorFn: (val) => parseUnsafeHashProps(val), enableSorting: true, enableGlobalFilter: true, size: 400, @@ -133,21 +99,20 @@ export function itemProps(options: { headerWording: header }, cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function itemTabs(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { header, accessorKey } = options +export function itemTabs(): ColumnDef { + const key: DisplayedItem = 'tab' + const header = 'tab' return { header: ({ column }) => , - accessorKey, + accessorKey: key, + accessorFn: (val) => parseTabNames(val.tab), enableSorting: true, enableGlobalFilter: true, size: 180, @@ -155,25 +120,22 @@ export function itemTabs(options: { meta: { headerWording: header }, - accessorFn: (val) => - val.tab && typeof val.tab === 'object' ? parseTabNames(val.tab || []) : '', cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function itemQuantity(options: { - accessorKey: PricedItem - header: string - diff?: boolean -}): ColumnDef { - const { header, accessorKey, diff } = options +export function itemQuantity(options: { diff?: boolean }): ColumnDef { + const { diff } = options + + const key: DisplayedItem = 'stackSize' + const header = 'quantity' return { header: ({ column }) => , - accessorKey, + accessorKey: key, enableSorting: true, enableGlobalFilter: false, size: 110, @@ -183,21 +145,19 @@ export function itemQuantity(options: { }, // maxSize: 80, cell: ({ row }) => { - const value = row.getValue(accessorKey) + const value = row.getValue(key) return } } } -export function sparkLine(options: { - accessorKey: PricedItem - header: string -}): ColumnDef { - const { accessorKey, header } = options +export function sparkLine(): ColumnDef { + const key: DisplayedItem = 'valuation' + const header = 'priceLast24Hours' return { header: ({ column }) => , - accessorKey, + accessorKey: key, accessorFn: (pricedItem) => { const valuation = pricedItem.valuation if (!valuation) return 0 @@ -225,20 +185,20 @@ export function sparkLine(options: { }, cell: ({ row }) => { const value = row.original.valuation - const totalChange = row.getValue(accessorKey) + const totalChange = row.getValue(key) return } } } export function itemValue(options: { - accessorKey: PricedItem + accessorKey: DisplayedItem header: string cumulative?: boolean showChange?: boolean toCurrency?: 'chaos' | 'divine' | 'both' enableSorting?: boolean -}): ColumnDef { +}): ColumnDef { const { header, accessorKey, cumulative, showChange, toCurrency, enableSorting } = options return { diff --git a/src/echo-plugins/networth-plugin/src/components/ErrorBroundary/ErrorBoundaryFallback.tsx b/src/echo-plugins/networth-plugin/src/components/ErrorBroundary/ErrorBoundaryFallback.tsx new file mode 100644 index 00000000..7589c4f3 --- /dev/null +++ b/src/echo-plugins/networth-plugin/src/components/ErrorBroundary/ErrorBoundaryFallback.tsx @@ -0,0 +1,52 @@ +import { observer } from 'mobx-react-lite' +import React from 'react' +import { Button, Card } from 'echo-common/components-v1' +import { FallbackProps, useErrorBoundary } from 'react-error-boundary' +import { resetStore } from '../..' +import { resetAll } from '../../db' +import { app } from '@electron/remote' + +const ErrorBoundaryFallback: React.FC = ({ error }) => { + // const { resetBoundary } = useErrorBoundary() + + return ( +
+ + + Ouch.... the plugin crashed... + + The plugin creashed, please send us more information... + + + +

+ The plugin is probably corrupted. The only option currently is to reset the plugin. If + you know what this issue might be, please create an issue on Github{' '} + + here. + +

+ +
+ + + +
+
+ ) +} + +export default observer(ErrorBoundaryFallback) diff --git a/src/echo-plugins/networth-plugin/src/components/ItemTable/ItemTableContainer.tsx b/src/echo-plugins/networth-plugin/src/components/ItemTable/ItemTableContainer.tsx index 793b2a0a..82c16959 100644 --- a/src/echo-plugins/networth-plugin/src/components/ItemTable/ItemTableContainer.tsx +++ b/src/echo-plugins/networth-plugin/src/components/ItemTable/ItemTableContainer.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react' import { itemTableColumns } from './itemTableColumns' import ItemTable from './ItemTable' -import { IPricedItem } from '../../interfaces/priced-item.interface' +import { IDisplayedItem, IPricedItem } from '../../interfaces/priced-item.interface' import { observer } from 'mobx-react' import { useStore } from '../../hooks/useStore' import { FilterFn, filterFns } from '@tanstack/react-table' @@ -20,7 +20,7 @@ const ItemTableContainer: React.FC = () => { return itemTableColumns() }, []) - const fuzzyFilter: FilterFn = (row, columnId, filterValue, addMeta) => { + const fuzzyFilter: FilterFn = (row, columnId, filterValue, addMeta) => { if (columnId === 'icon') { const rarity = getRarityIdentifier(filterValue?.toString()?.toLowerCase()) return rarity >= 0 && row.original.frameType === rarity diff --git a/src/echo-plugins/networth-plugin/src/components/ItemTable/itemTableColumns.ts b/src/echo-plugins/networth-plugin/src/components/ItemTable/itemTableColumns.ts index 11cc4932..d8272d78 100644 --- a/src/echo-plugins/networth-plugin/src/components/ItemTable/itemTableColumns.ts +++ b/src/echo-plugins/networth-plugin/src/components/ItemTable/itemTableColumns.ts @@ -9,31 +9,16 @@ import { itemValue, sparkLine } from '../Columns/Columns' -import { IPricedItem } from '../../interfaces/priced-item.interface' +import { IDisplayedItem } from '../../interfaces/priced-item.interface' -export const itemTableColumns = (): ColumnDef[] => [ - itemIcon({ - accessorKey: 'icon', - header: 'icon' - }), - itemName({ - accessorKey: 'name', - header: 'name' - }), - itemProps({ accessorKey: 'unsafeHashProperties', header: 'properties' }), - itemTag({ - accessorKey: 'tag', - header: 'tag' - }), - itemTabs({ - accessorKey: 'tab', - header: 'tab' - }), - itemQuantity({ - accessorKey: 'stackSize', - header: 'quantity' - }), - sparkLine({ accessorKey: 'valuation', header: 'priceLast24Hours' }), +export const itemTableColumns = (): ColumnDef[] => [ + itemIcon(), + itemName(), + itemProps(), + itemTag(), + itemTabs(), + itemQuantity({}), + sparkLine(), itemValue({ accessorKey: 'calculated', header: 'price', diff --git a/src/echo-plugins/networth-plugin/src/components/Toolbar/ProfileMenu.tsx b/src/echo-plugins/networth-plugin/src/components/Toolbar/ProfileMenu.tsx index b6a4c127..29cd863e 100644 --- a/src/echo-plugins/networth-plugin/src/components/Toolbar/ProfileMenu.tsx +++ b/src/echo-plugins/networth-plugin/src/components/Toolbar/ProfileMenu.tsx @@ -17,7 +17,7 @@ import { cn } from 'echo-common' const ProfileMenu = () => { const { t } = useTranslation() - const { accountStore } = useStore() + const { accountStore, uiStateStore } = useStore() const activeAccount = accountStore.activeAccount const [menuOpen, setMenuOpen] = useState(false) const [selectedProfile, setSelectedProfile] = useState() @@ -54,6 +54,7 @@ const ProfileMenu = () => { role="combobox" aria-expanded={menuOpen} aria-label={t('label.selectProfile')} + disabled={!uiStateStore.initiated} > {activeAccount.activeProfile?.name ?? t('label.addProfile')} @@ -73,6 +74,7 @@ const ProfileMenu = () => { role="combobox" aria-expanded={menuOpen} aria-label={t('label.selectProfile')} + disabled={!uiStateStore.initiated} > {t('label.addProfile')} diff --git a/src/echo-plugins/networth-plugin/src/components/Toolbar/Toolbar.tsx b/src/echo-plugins/networth-plugin/src/components/Toolbar/Toolbar.tsx index e097d87e..7e32d87f 100644 --- a/src/echo-plugins/networth-plugin/src/components/Toolbar/Toolbar.tsx +++ b/src/echo-plugins/networth-plugin/src/components/Toolbar/Toolbar.tsx @@ -14,6 +14,7 @@ import ToolbarContentSkeleton from '../LoadingStates/ToolbarContentSkeleton' type ToolbarProps = { isSubmitting: boolean isInitiating: boolean + isInitiated: boolean isSnapshotting: boolean isProfileValid: boolean readyToSnapshot: boolean @@ -26,6 +27,7 @@ type ToolbarProps = { const Toolbar: React.FC = ({ isSubmitting, isInitiating, + isInitiated, isSnapshotting, isProfileValid, readyToSnapshot, @@ -41,8 +43,8 @@ const Toolbar: React.FC = ({ {(isInitiating || isSnapshotting) && } {(statusMessage || rateLimiterActive) && } - {isInitiating && } - {!isInitiating && ( + {(isInitiating || !isInitiated) && } + {(!isInitiating || !isInitiated) && (
diff --git a/src/echo-plugins/networth-plugin/src/components/Toolbar/ToolbarContainer.tsx b/src/echo-plugins/networth-plugin/src/components/Toolbar/ToolbarContainer.tsx index f604ad4d..d670f2bf 100644 --- a/src/echo-plugins/networth-plugin/src/components/Toolbar/ToolbarContainer.tsx +++ b/src/echo-plugins/networth-plugin/src/components/Toolbar/ToolbarContainer.tsx @@ -14,6 +14,7 @@ const ToolbarContainer: React.FC = () => { { export const saveRootSnapshot = (data: SnapshotOutOfModel) => { return db.update(schema.rootStore).set({ root: data }).where(eq(schema.rootStore.id, 1)).execute() } + +export const resetAll = () => { + return db.delete(schema.rootStore).where(eq(schema.rootStore.id, 1)).execute() +} diff --git a/src/echo-plugins/networth-plugin/src/index.tsx b/src/echo-plugins/networth-plugin/src/index.tsx index 3a346937..bb7d1a1b 100644 --- a/src/echo-plugins/networth-plugin/src/index.tsx +++ b/src/echo-plugins/networth-plugin/src/index.tsx @@ -10,6 +10,8 @@ import Notifier from './components/Notifier/Notifier' import { Toaster } from 'echo-common/components-v1' import { initDrizzle, getRootSnapshot, saveRootSnapshot } from './db' import _ from 'lodash' +import { ErrorBoundary } from 'react-error-boundary' +import ErrorBoundaryFallback from './components/ErrorBroundary/ErrorBoundaryFallback' export function createRootStore() { const rootStore = new RootStore({}) @@ -41,11 +43,13 @@ const App = () => { return ( - - - - - + + + + + + + ) diff --git a/src/echo-plugins/networth-plugin/src/interfaces/api/api-stash-tab-snapshot.interface.ts b/src/echo-plugins/networth-plugin/src/interfaces/api/api-stash-tab-snapshot.interface.ts index 037980b6..cae3532c 100644 --- a/src/echo-plugins/networth-plugin/src/interfaces/api/api-stash-tab-snapshot.interface.ts +++ b/src/echo-plugins/networth-plugin/src/interfaces/api/api-stash-tab-snapshot.interface.ts @@ -1,11 +1,10 @@ -import { Frozen } from 'mobx-keystone' import { IPricedItem } from '../priced-item.interface' export interface IApiStashTabSnapshot { uuid: string value: number stashTabId: string - pricedItems: Frozen + pricedItems: IPricedItem[] index: number name: string color: string diff --git a/src/echo-plugins/networth-plugin/src/interfaces/priced-item.interface.ts b/src/echo-plugins/networth-plugin/src/interfaces/priced-item.interface.ts index 376d500b..ff3f84f7 100644 --- a/src/echo-plugins/networth-plugin/src/interfaces/priced-item.interface.ts +++ b/src/echo-plugins/networth-plugin/src/interfaces/priced-item.interface.ts @@ -1,29 +1,24 @@ import { SageValuation } from 'echo-common' import { ICompactTab } from './stash.interface' +import { PoeItem, SageItemGroup } from 'sage-common' +// The persisted priced item export interface IPricedItem { uuid: string - tag: string | undefined - key: string | undefined - hash: string | undefined - unsafeHashProperties: any - itemId: string - name: string - typeLine: string - frameType: number - identified: boolean - total: number - calculated: number + percentile: number + group?: SageItemGroup valuation: SageValuation | undefined - icon: string - ilvl: number - tier: number - corrupted: boolean - links: number - sockets: number - quality: number - level: number + items: PoeItem[] + tab: ICompactTab[] +} + +// The hydrated displayed item +export interface IDisplayedItem extends IPricedItem { + displayName: string + calculated: number + total: number stackSize: number totalStacksize: number - tab: ICompactTab[] + icon: string + frameType: number } diff --git a/src/echo-plugins/networth-plugin/src/store/domains/notification.ts b/src/echo-plugins/networth-plugin/src/store/domains/notification.ts index 9bd089e8..87b4f731 100644 --- a/src/echo-plugins/networth-plugin/src/store/domains/notification.ts +++ b/src/echo-plugins/networth-plugin/src/store/domains/notification.ts @@ -1,5 +1,4 @@ -import { computed } from 'mobx' -import { detach, idProp, model, Model, prop, rootRef, tProp, types } from 'mobx-keystone' +import { idProp, model, Model, prop, tProp, types } from 'mobx-keystone' import { NotificationType } from '../../interfaces/notification.interface' import dayjs from 'dayjs' diff --git a/src/echo-plugins/networth-plugin/src/store/domains/profile.ts b/src/echo-plugins/networth-plugin/src/store/domains/profile.ts index d06c998e..2866fc92 100644 --- a/src/echo-plugins/networth-plugin/src/store/domains/profile.ts +++ b/src/echo-plugins/networth-plugin/src/store/domains/profile.ts @@ -37,12 +37,12 @@ import { createCompactTab, mapItemsToPricedItems, mapMapStashItemToPoeItem as mapMapStashItemsToPoeItems, - mergeItemStacks + mergeItems } from '../../utils/item.utils' import { PoeItem } from 'sage-common' import { StashTabSnapshot } from './stashtab-snapshot' import { diffSnapshots, filterItems, filterSnapshotItems } from '../../utils/snapshot.utils' -import { IPricedItem } from '../../interfaces/priced-item.interface' +import { IDisplayedItem } from '../../interfaces/priced-item.interface' import { PersistWrapper } from '../../utils/persist.utils' import dayjs from 'dayjs' @@ -553,14 +553,14 @@ export class Profile extends Model( compactStash, settingStore.primaryPercentile ) - const pricedStackedItems = mergeItemStacks(pricedItems) + const pricedStackedItems = mergeItems(pricedItems) const stashTabId = valuatedStash.stashTab instanceof StashTab ? valuatedStash.stashTab.id : compactStash.id const stashTabSnapshots = new StashTabSnapshot({ stashTabId: stashTabId, - pricedItems: frozen(pricedStackedItems) + pricedItems: pricedStackedItems }) return of(stashTabSnapshots) }) @@ -618,7 +618,7 @@ export class Profile extends Model( // } // const pricedStashTabs = stashTabsWithItems.map((stashTabWithItems: IStashTabSnapshot) => { - // stashTabWithItems.pricedItems = stashTabWithItems.pricedItems.map((item: IPricedItem) => { + // stashTabWithItems.pricedItems = stashTabWithItems.pricedItems.map((item: IDisplayedItem) => { // return pricingService.priceItem(item, prices) // }) // return stashTabWithItems @@ -673,7 +673,7 @@ export class Profile extends Model( this.snapshots = nextSnapshots } - diffSnapshotPriceResolver(removedItems: IPricedItem[]) { + diffSnapshotPriceResolver(removedItems: IDisplayedItem[]) { // TODO: use some flow generator function & convert PricedItem to PoeItem, that it can priced again or save the PoeItem in the PricedItem return 0 diff --git a/src/echo-plugins/networth-plugin/src/store/domains/stashtab-snapshot.ts b/src/echo-plugins/networth-plugin/src/store/domains/stashtab-snapshot.ts index ef8a8a1d..ba3455ec 100644 --- a/src/echo-plugins/networth-plugin/src/store/domains/stashtab-snapshot.ts +++ b/src/echo-plugins/networth-plugin/src/store/domains/stashtab-snapshot.ts @@ -1,8 +1,9 @@ -import { frozen, idProp, model, Model, prop, rootRef, tProp, types } from 'mobx-keystone' -import { IPricedItem } from '../../interfaces/priced-item.interface' +import { idProp, model, Model, prop, rootRef, tProp, types } from 'mobx-keystone' +import { IDisplayedItem, IPricedItem } from '../../interfaces/priced-item.interface' import { StashTab } from './stashtab' import { computed } from 'mobx' import { PersistWrapper } from '../../utils/persist.utils' +import { mapItemsToDisplayedItems, mergeItemStacks } from '../../utils/item.utils' export const stashTabSnapshotStashTabRef = rootRef('nw/stashTabSnapshotStashTabRef') @@ -13,14 +14,26 @@ export class StashTabSnapshot extends Model( // The stash may be deleted. The stash can be a snapshot for a character and has no reference stashTabId: tProp(types.string), value: tProp(0).withSetter(), - pricedItems: tProp(types.frozen(types.unchecked()), () => - frozen([]) - ), + pricedItems: prop(() => []), version: prop(1) }) ) { + _displayedItems: IDisplayedItem[] = [] + _isInit = false + + get displayedItems() { + // Displayed items are static and @computed somehow does compute sometimes new although its not changed + if (this._isInit) return this._displayedItems + this._isInit = true + const displayedItems = mapItemsToDisplayedItems(this.pricedItems) + this._displayedItems = mergeItemStacks(displayedItems) + // Set value once + this.setValue(this._displayedItems.reduce((total, item) => total + item.total, 0)) + return this._displayedItems + } + @computed get totalValue() { - return this.pricedItems.data.reduce((total, item) => total + item.total, 0) + return this.value } } diff --git a/src/echo-plugins/networth-plugin/src/utils/item.utils.ts b/src/echo-plugins/networth-plugin/src/utils/item.utils.ts index 39b8bbcf..bfa45f04 100644 --- a/src/echo-plugins/networth-plugin/src/utils/item.utils.ts +++ b/src/echo-plugins/networth-plugin/src/utils/item.utils.ts @@ -1,4 +1,4 @@ -import { PoeItem, PoeItemProperty } from 'sage-common' +import { PoeItem, PoeItemProperty, PoeItemSocket } from 'sage-common' import { Rarity } from '../assets/theme' import { IChildStashTab, @@ -7,7 +7,7 @@ import { IStashTabNode } from '../interfaces/stash.interface' import { v4 as uuidv4 } from 'uuid' -import { IPricedItem } from '../interfaces/priced-item.interface' +import { IDisplayedItem, IPricedItem } from '../interfaces/priced-item.interface' import { EchoPoeItem } from 'echo-common' const rarities: (keyof Rarity)[] = [ @@ -39,6 +39,37 @@ export const parseTabNames = (tabs: ICompactTab[]) => { return tabs.map((t) => t.name).join(', ') } +export const parseUnsafeHashProps = (item: IDisplayedItem) => { + const hashProps = Object.entries(item.group?.unsafeHashProperties || {}) + .filter(([name, value]) => { + const excludeNameByIncludes = (name: string) => + !['mods', 'Mods'].some((key) => name.includes(key)) + + const excludeFalseValues = (value: any) => { + if (!value || value === '0') return false + return true + } + return excludeNameByIncludes(name) && excludeFalseValues(value) + }) + .map(([name, value]) => { + let displayValue = `${name}: ${value}` + // Name only + if (value === true) { + displayValue = name + } + return { name, value, displayValue } + }) + + if (item.group?.key && item.displayName.toLowerCase() !== item.group.key?.toLowerCase()) { + // Used for contract or cluster jewels + hashProps.push({ name: item.displayName, value: item.group.key, displayValue: item.group.key }) + } + + hashProps.sort((a, b) => a.displayValue.length - b.displayValue.length) + // Filterfn does not work, when an object is returned + return hashProps.map((p) => `${p.name};;${p.displayValue}`).join(';;;') +} + export const getItemName = (name?: string, typeline?: string) => { let itemName = name || '' if (typeline) { @@ -47,6 +78,12 @@ export const getItemName = (name?: string, typeline?: string) => { return itemName.replace('<><><>', '').trim() } +export const getItemDisplayName = (item: PoeItem) => { + return getMapTier(item.properties) && item.frameType !== 3 + ? getSpecialMapFromImplicits(item.implicitMods) || item.baseType! + : getItemName(item.name, item.frameType !== 3 ? item.typeLine : undefined) +} + export const getPropertyValue = (props: PoeItemProperty[], key: string) => { const prop = props.find((t) => t.name === key) const quality = prop && prop.values ? prop.values[0][0] : '0' @@ -68,26 +105,54 @@ export const getMapTier = (props?: PoeItemProperty[]) => { return getPropertyValue(props, 'Map Tier') } -export function getLinks(array: any[]) { +export const getStackSize = (item: PoeItem) => { + return item.stackSize || 1 +} + +export const getTotalStackSize = (item: PoeItem) => { + return item.maxStackSize || 1 +} + +export const getSockets = (item: PoeItem) => { + return item.sockets !== undefined && item.sockets !== null ? item.sockets.length : 0 +} + +export function getLinks(sockets?: PoeItemSocket[]) { + if (sockets === undefined || sockets === null) return 0 + const numMapping: any = {} let greatestFreq = 0 - array.forEach((number) => { - numMapping[number] = (numMapping[number] || 0) + 1 + sockets + .map((t) => t.group!) + .forEach((number) => { + numMapping[number] = (numMapping[number] || 0) + 1 - if (greatestFreq < numMapping[number]) { - greatestFreq = numMapping[number] + if (greatestFreq < numMapping[number]) { + greatestFreq = numMapping[number] + } + }) + return greatestFreq +} + +export function findItem(array: T[], toFind: T) { + return array.find((x) => { + if (toFind.group?.hash) { + return x.group?.hash === toFind.group?.hash + } + if (!x.group && toFind.totalStacksize > 1) { + return x.displayName === toFind.displayName } + return false }) - return greatestFreq } -export function findItem(array: T[], toFind: T) { +export function findPricedItem(array: T[], toFind: T) { return array.find((x) => { - if (toFind.hash) { - return x.hash === toFind.hash + if (toFind.group?.hash) { + return x.group?.hash === toFind.group?.hash } - if (toFind.totalStacksize > 1) { - return x.name === toFind.name + if (!x.group && getTotalStackSize(toFind.items[0]) > 1) { + return getItemDisplayName(x.items[0]) === getItemDisplayName(toFind.items[0]) } return false }) @@ -206,76 +271,90 @@ export const createCompactTab = (stashTab: IStashTabNode | string): ICompactTab } } -export function mapItemsToPricedItems( +export const mapItemsToPricedItems = ( valuation: EchoPoeItem[], stashTab: ICompactTab, percentile: number -): IPricedItem[] { +): IPricedItem[] => { return valuation.map((valuatedItem) => { - const item = valuatedItem.data - const valuation = valuatedItem.valuation + const pricedItem: IPricedItem = { + uuid: uuidv4(), + percentile: percentile, + group: valuatedItem.group?.primaryGroup, + valuation: valuatedItem.valuation || undefined, + items: [valuatedItem.data], + tab: [stashTab] + } + return pricedItem + }) +} - const mapTier = getMapTier(item.properties) - const stackSize = item.stackSize || 1 +export const mapItemsToDisplayedItems = (pricedItems: IPricedItem[]): IDisplayedItem[] => { + return pricedItems.map((pItem) => { + const item = pItem.items[0] + const valuation = pItem.valuation - const mappedItem: IPricedItem = { - uuid: uuidv4(), - tag: valuatedItem.group?.primaryGroup.tag, - key: valuatedItem.group?.primaryGroup.key, - hash: valuatedItem.group?.primaryGroup.hash, - unsafeHashProperties: valuatedItem.group?.primaryGroup.unsafeHashProperties, - itemId: item.id!, - name: - mapTier && item.frameType !== 3 - ? getSpecialMapFromImplicits(item.implicitMods) || item.baseType! - : getItemName(item.name, item.frameType !== 3 ? item.typeLine : undefined), - typeLine: item.typeLine!, - frameType: item.frameType!, - identified: item.identified!, - total: valuation ? valuation.pValues[percentile] * stackSize : 0, - calculated: valuation ? valuation.pValues[percentile] : 0, - valuation: valuation === null ? undefined : valuation, - icon: item.icon!, - ilvl: item.ilvl!, - tier: mapTier, - corrupted: item.corrupted || false, - links: - item.sockets !== undefined && item.sockets !== null - ? getLinks(item.sockets.map((t) => t.group)) - : 0, - sockets: item.sockets !== undefined && item.sockets !== null ? item.sockets.length : 0, - quality: getQuality(item.properties), - level: getLevel(item.properties), + const stackSize = getStackSize(item) + + const mappedItem: IDisplayedItem = { + displayName: getItemDisplayName(item), + total: valuation ? valuation.pValues[pItem.percentile] * stackSize : 0, + calculated: valuation ? valuation.pValues[pItem.percentile] : 0, stackSize: stackSize, - totalStacksize: item.maxStackSize || 1, - tab: [stashTab] + totalStacksize: getTotalStackSize(item), + icon: item.icon!, + frameType: item.frameType!, + ...pItem } - if (mappedItem.name === 'Chaos Orb') { + if (mappedItem.displayName === 'Chaos Orb') { mappedItem.calculated = 1 - mappedItem.total = mappedItem.stackSize + mappedItem.total = stackSize } return mappedItem }) } -export function mergeItemStacks(items: IPricedItem[]) { +export function mergeItemStacks(items: IDisplayedItem[]) { + const mergedItems: IDisplayedItem[] = [] + + items.forEach((dItem) => { + const foundItem = findItem(mergedItems, dItem) + if (!foundItem) { + mergedItems.push({ + ...dItem + }) + } else { + const idx = mergedItems.indexOf(foundItem) + mergedItems[idx].stackSize += dItem.stackSize + mergedItems[idx].total = mergedItems[idx].stackSize * mergedItems[idx].calculated + if (dItem.tab !== undefined) { + mergedItems[idx].tab = [...(mergedItems[idx].tab || []), ...dItem.tab] + mergedItems[idx].tab = mergedItems[idx].tab.filter( + (v, i, a) => a.findIndex((t) => t.id === v.id) === i + ) + } + } + }) + + return mergedItems +} + +export function mergeItems(items: IPricedItem[]) { const mergedItems: IPricedItem[] = [] - items.forEach((item) => { - const foundItem = findItem(mergedItems, item) + items.forEach((pItem) => { + const foundItem = findPricedItem(mergedItems, pItem) if (!foundItem) { mergedItems.push({ - ...item + ...pItem }) } else { - const foundIdx = mergedItems.indexOf(foundItem) - mergedItems[foundIdx].stackSize += item.stackSize - mergedItems[foundIdx].total = - mergedItems[foundIdx].stackSize * mergedItems[foundIdx].calculated - if (mergedItems[foundIdx].tab !== undefined && item.tab !== undefined) { - mergedItems[foundIdx].tab = [...mergedItems[foundIdx].tab, ...item.tab] - mergedItems[foundIdx].tab = mergedItems[foundIdx].tab.filter( + const idx = mergedItems.indexOf(foundItem) + mergedItems[idx].items = [...(mergedItems[idx].items || []), ...(pItem.items || [])] + if (pItem.tab !== undefined) { + mergedItems[idx].tab = [...(mergedItems[idx].tab || []), ...pItem.tab] + mergedItems[idx].tab = mergedItems[idx].tab.filter( (v, i, a) => a.findIndex((t) => t.id === v.id) === i ) } diff --git a/src/echo-plugins/networth-plugin/src/utils/snapshot.utils.ts b/src/echo-plugins/networth-plugin/src/utils/snapshot.utils.ts index 214c63ee..3713e459 100644 --- a/src/echo-plugins/networth-plugin/src/utils/snapshot.utils.ts +++ b/src/echo-plugins/networth-plugin/src/utils/snapshot.utils.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs' import { IApiSnapshot } from '../interfaces/api/api-snapshot.interface' import { IApiStashTabSnapshot } from '../interfaces/api/api-stash-tab-snapshot.interface' -import { IPricedItem } from '../interfaces/priced-item.interface' +import { IDisplayedItem } from '../interfaces/priced-item.interface' import { IStashTab } from '../interfaces/stash.interface' import { Snapshot } from '../store/domains/snapshot' import { StashTab } from '../store/domains/stashtab' @@ -11,16 +11,12 @@ import { IChartStashTabSnapshot } from '../interfaces/hc-chart-series.interface' export const diffSnapshots = ( snapshot1: Snapshot, snapshot2: Snapshot, - removedItemsPriceResolver?: (items: IPricedItem[]) => void + removedItemsPriceResolver?: (items: IDisplayedItem[]) => void ) => { - const difference: IPricedItem[] = [] - const removedItems: IPricedItem[] = [] - const itemsInSnapshot1 = mergeItemStacks( - snapshot1.stashTabs.flatMap((sts) => sts.pricedItems.data) - ) - const itemsInSnapshot2 = mergeItemStacks( - snapshot2.stashTabs.flatMap((sts) => sts.pricedItems.data) - ) + const difference: IDisplayedItem[] = [] + const removedItems: IDisplayedItem[] = [] + const itemsInSnapshot1 = mergeItemStacks(snapshot1.stashTabs.flatMap((sts) => sts.displayedItems)) + const itemsInSnapshot2 = mergeItemStacks(snapshot2.stashTabs.flatMap((sts) => sts.displayedItems)) // items that exist in snapshot 2 but not in snapshot 1 & items that exist in both snapshots but should be updated const itemsToAddOrUpdate = itemsInSnapshot2.filter((x) => { @@ -63,7 +59,7 @@ export const diffSnapshots = ( } export const filterItems = ( - items: IPricedItem[], + items: IDisplayedItem[], showPricedItems: boolean, showUnpricedItems: boolean ) => { @@ -93,7 +89,7 @@ export const filterSnapshotItems = ( ) ) .flatMap((sts) => - sts.pricedItems.data.filter((i) => { + sts.displayedItems.filter((i) => { return (showPricedItems && i.calculated > 0) || (showUnpricedItems && !i.calculated) }) ) diff --git a/src/green-app/package-lock.json b/src/green-app/package-lock.json index 840e5a13..cc92053c 100644 --- a/src/green-app/package-lock.json +++ b/src/green-app/package-lock.json @@ -26,57 +26,63 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", - "@tanstack/query-sync-storage-persister": "^5.21.4", - "@tanstack/react-query": "^5.17.19", - "@tanstack/react-query-devtools": "^5.17.21", - "@tanstack/react-query-persist-client": "^5.21.4", - "@tanstack/react-table": "^8.11.7", + "@tanstack/query-sync-storage-persister": "^5.28.0", + "@tanstack/react-query": "^5.28.0", + "@tanstack/react-query-devtools": "^5.28.0", + "@tanstack/react-query-persist-client": "^5.28.0", + "@tanstack/react-table": "^8.13.2", "@types/object-hash": "^3.0.6", - "axios": "^1.6.5", + "axios": "^1.6.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", - "cmdk": "^0.2.0", + "cmdk": "^1.0.0", "dayjs": "^1.11.10", - "immer": "^10.0.3", - "jotai": "^2.6.4", + "framer-motion": "^11.0.12", + "i18next": "^23.10.1", + "i18next-resources-to-backend": "^1.2.0", + "immer": "^10.0.4", + "jotai": "^2.7.0", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", - "lucide-react": "^0.314.0", - "next": "14.1.0", - "next-themes": "^0.2.1", + "lucide-react": "^0.357.0", + "next": "^14.1.3", + "next-i18n-router": "^5.3.1", + "next-themes": "^0.3.0", "object-hash": "^3.0.0", "react": "^18", "react-dom": "^18", - "react-hook-form": "^7.49.3", + "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.51.0", + "react-i18next": "^14.1.0", "react-toastify": "^10.0.4", - "recharts": "^2.11.0", + "recharts": "^2.12.2", "rxjs": "^7.8.1", "sage-common": "file:../sage-common", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^2.14.0", + "usehooks-ts": "^3.0.1", "uuid": "^9.0.1", "zod": "^3.22.4", - "zustand": "^4.5.0" + "zustand": "^4.5.2" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.17.22", + "@tanstack/eslint-plugin-query": "^5.27.7", "@types/lodash-es": "^4.17.12", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^6.19.1", - "@typescript-eslint/parser": "^6.19.1", - "autoprefixer": "^10.4.17", - "eslint": "^8.56.0", - "eslint-config-next": "^14.1.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", + "eslint-config-next": "^14.1.3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8", - "prettier": "^3.2.4", + "prettier": "^3.2.5", "tailwindcss": "^3.4.1", "typescript": "^5" } @@ -129,9 +135,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", - "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -209,37 +215,37 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@floating-ui/core": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz", - "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", "dependencies": { - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.1" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz", - "integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", "dependencies": { - "@floating-ui/core": "^1.5.3", + "@floating-ui/core": "^1.0.0", "@floating-ui/utils": "^0.2.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.6.tgz", - "integrity": "sha512-IB8aCRFxr8nFkdYZgH+Otd9EVQPJoynxeFRGTB8voPoZMRWo8XjYuCRgpI1btvuKY69XMiLnW+ym7zoBHM90Rw==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", + "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", "dependencies": { - "@floating-ui/dom": "^1.5.4" + "@floating-ui/dom": "^1.6.1" }, "peerDependencies": { "react": ">=16.8.0", @@ -251,6 +257,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.4.tgz", + "integrity": "sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@hookform/resolvers": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", @@ -330,31 +344,56 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -365,32 +404,32 @@ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@next/env": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", - "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==" + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.3.tgz", + "integrity": "sha512-VhgXTvrgeBRxNPjyfBsDIMvgsKDxjlpw4IAUsHCX8Gjl1vtHUYRT3+xfQ/wwvLPDd/6kqfLqk9Pt4+7gysuCKQ==" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz", - "integrity": "sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.3.tgz", + "integrity": "sha512-VCnZI2cy77Yaj3L7Uhs3+44ikMM1VD/fBMwvTBb3hIaTIuqa+DmG4dhUDq+MASu3yx97KhgsVJbsas0XuiKyww==", "dev": true, "dependencies": { "glob": "10.3.10" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", - "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.3.tgz", + "integrity": "sha512-LALu0yIBPRiG9ANrD5ncB3pjpO0Gli9ZLhxdOu6ZUNf3x1r3ea1rd9Q+4xxUkGrUXLqKVK9/lDkpYIJaCJ6AHQ==", "cpu": [ "arm64" ], @@ -403,9 +442,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", - "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.3.tgz", + "integrity": "sha512-E/9WQeXxkqw2dfcn5UcjApFgUq73jqNKaE5bysDm58hEUdUGedVrnRhblhJM7HbCZNhtVl0j+6TXsK0PuzXTCg==", "cpu": [ "x64" ], @@ -418,9 +457,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", - "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.3.tgz", + "integrity": "sha512-USArX9B+3rZSXYLFvgy0NVWQgqh6LHWDmMt38O4lmiJNQcwazeI6xRvSsliDLKt+78KChVacNiwvOMbl6g6BBw==", "cpu": [ "arm64" ], @@ -433,9 +472,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", - "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.3.tgz", + "integrity": "sha512-esk1RkRBLSIEp1qaQXv1+s6ZdYzuVCnDAZySpa62iFTMGTisCyNQmqyCTL9P+cLJ4N9FKCI3ojtSfsyPHJDQNw==", "cpu": [ "arm64" ], @@ -448,9 +487,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", - "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.3.tgz", + "integrity": "sha512-8uOgRlYEYiKo0L8YGeS+3TudHVDWDjPVDUcST+z+dUzgBbTEwSSIaSgF/vkcC1T/iwl4QX9iuUyUdQEl0Kxalg==", "cpu": [ "x64" ], @@ -463,9 +502,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", - "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.3.tgz", + "integrity": "sha512-DX2zqz05ziElLoxskgHasaJBREC5Y9TJcbR2LYqu4r7naff25B4iXkfXWfcp69uD75/0URmmoSgT8JclJtrBoQ==", "cpu": [ "x64" ], @@ -478,9 +517,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", - "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.3.tgz", + "integrity": "sha512-HjssFsCdsD4GHstXSQxsi2l70F/5FsRTRQp8xNgmQs15SxUfUJRvSI9qKny/jLkY3gLgiCR3+6A7wzzK0DBlfA==", "cpu": [ "arm64" ], @@ -493,9 +532,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", - "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.3.tgz", + "integrity": "sha512-DRuxD5axfDM1/Ue4VahwSxl1O5rn61hX8/sF0HY8y0iCbpqdxw3rB3QasdHn/LJ6Wb2y5DoWzXcz3L1Cr+Thrw==", "cpu": [ "ia32" ], @@ -508,9 +547,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", - "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.3.tgz", + "integrity": "sha512-uC2DaDoWH7h1P/aJ4Fok3Xiw6P0Lo4ez7NbowW2VGNXw/Xv6tOuLUcxhBYZxsSUJtpeknCi8/fvnSpyCFp4Rcg==", "cpu": [ "x64" ], @@ -1609,12 +1648,12 @@ } }, "node_modules/@tanstack/eslint-plugin-query": { - "version": "5.17.22", - "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.17.22.tgz", - "integrity": "sha512-9y85gtYcU60M4zW5/CNSxNygbil2jairs4DbtNvGKxQHi4LYUs30sLRcsuXDhu1cPGCY/yRN7LTjWFQCrWFGbQ==", + "version": "5.27.7", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.27.7.tgz", + "integrity": "sha512-I0bQGypBu7gmbjHhRPglZRnYZObiXu7JotDxqRJfjr8sP5YiCx2zm+qbQClrgUGER++Hx4EA4suL7hSiBMWgJg==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^5.62.0" + "@typescript-eslint/utils": "^6.20.0" }, "funding": { "type": "github", @@ -1624,152 +1663,30 @@ "eslint": "^8.0.0" } }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@tanstack/eslint-plugin-query/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@tanstack/query-core": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.21.4.tgz", - "integrity": "sha512-k3u4RcDAtcCurs8KVEIf52k4yUayc852v4ZQrtI8pkEii71riM9758A2WVGo5T/v4/X7b1RLON5g0aDvkoZYCA==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.28.0.tgz", + "integrity": "sha512-BfltXqnoIAXTCFrQCu40M3Ch7odQ6IJraTy0t8n12jAwXMYKIgDwOBWTqkSUYD+vxMi8Ag0+9F8lw9wZKhi2Yg==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-devtools": { - "version": "5.17.21", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.17.21.tgz", - "integrity": "sha512-WWfcnNjTEqcuAS5GyKkVGkseuES6yd197MJWGImBu+MoCjWPqxSXKCCfm+utSXJauJUGm7xoMmhqCphiQdjf8w==", + "version": "5.27.8", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.27.8.tgz", + "integrity": "sha512-K94gnqvEe6TsDvi8eZYP2JrnQJOIymhVXRR+Xa0xcsryNqG+PeMIDmQQqjwIqbDq36qyUlPAyT6LxXVvVv1Nyw==", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/query-persist-client-core": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.21.4.tgz", - "integrity": "sha512-UKdvjSPPb21MLm4Eek/yYST+emk81c62GkLiVyVVv6ke5Gwdwx+z/xfDntJozBe8/q9uOIkJlHjD0FFp7JDnYA==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.28.0.tgz", + "integrity": "sha512-hMiK9ciWCAtlHjRzu29TeuEdP91RlPy9YQk+3AH8Kjvc4JLREkRdzexy2jatQiaTOIJUH8SL+04FoU+nAaRW+A==", "dependencies": { - "@tanstack/query-core": "5.21.4" + "@tanstack/query-core": "5.28.0" }, "funding": { "type": "github", @@ -1777,12 +1694,12 @@ } }, "node_modules/@tanstack/query-sync-storage-persister": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.21.4.tgz", - "integrity": "sha512-NoQmuOkvyTz+ShGz9VW31dr1xBAxjVqMTPIjT6Lp64ICCaYVXpzSpt92MSX1lTy+kZlhDXpFmdnAzwMLxtg8nQ==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.28.0.tgz", + "integrity": "sha512-tx5Y+K3VHeBoPMqSpsivSDJ22EwCLpQJLeLW01AppWDrYm1aglovCsXWSYtxV5xgsnTtRCbte/c+FZbc/+0Xnw==", "dependencies": { - "@tanstack/query-core": "5.21.4", - "@tanstack/query-persist-client-core": "5.21.4" + "@tanstack/query-core": "5.28.0", + "@tanstack/query-persist-client-core": "5.28.0" }, "funding": { "type": "github", @@ -1790,11 +1707,11 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.21.4.tgz", - "integrity": "sha512-tzl4mGerfPKmJsPDWbfKol0eBEk8bsgtMZJOwnbUvvSRnYZzS9OfX9CD/dbQLr+JjIvSekWKaDBTo3oXeeFhoQ==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.28.0.tgz", + "integrity": "sha512-nF4E4rFMQdh30gECGkTfyzgjgfSr4MLVgYoIsf7KqVkjUjEQHPpi9jyx10kO3Yq/OQMKOLMHAzD31st/lxDPbQ==", "dependencies": { - "@tanstack/query-core": "5.21.4" + "@tanstack/query-core": "5.28.0" }, "funding": { "type": "github", @@ -1805,43 +1722,43 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.17.21", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.17.21.tgz", - "integrity": "sha512-Ri1AuWpN67eyPdMTlPxx1TMGNUaxTHrGv0ll0S20ZObz/Xms5wfANV3c6OX0HZTY0igudP1k5jpRLXNkd249mg==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.28.0.tgz", + "integrity": "sha512-uGuMgG9hqexr6FidePWZvhYMptGzuoSQjSc8LCIoYxi0Hn+quOe5Ey0SleGNyIdMzZHgsv5B6r/2iT9cw3iQvA==", "dependencies": { - "@tanstack/query-devtools": "5.17.21" + "@tanstack/query-devtools": "5.27.8" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.17.19", + "@tanstack/react-query": "^5.28.0", "react": "^18.0.0" } }, "node_modules/@tanstack/react-query-persist-client": { - "version": "5.21.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.21.4.tgz", - "integrity": "sha512-n8SKWJcH/V3mB4Efp4UGlKtCoBeMm5sQ5qcG6nuPN16XXoUMB6GJT3ZqYXsCRb3QxVkrsLiCGKiR4kUrWOnMmg==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.28.0.tgz", + "integrity": "sha512-g5sa4HAmrY1fvNrR6jPwtT62YPCPcroSDr0HEr2pDnBj28vC51KH2zWXp/FB1faQ9IATrbURLnfEDX9kew8qEA==", "dependencies": { - "@tanstack/query-persist-client-core": "5.21.4" + "@tanstack/query-persist-client-core": "5.28.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.21.4", + "@tanstack/react-query": "^5.28.0", "react": "^18.0.0" } }, "node_modules/@tanstack/react-table": { - "version": "8.11.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.7.tgz", - "integrity": "sha512-ZbzfMkLjxUTzNPBXJYH38pv2VpC9WUA+Qe5USSHEBz0dysDTv4z/ARI3csOed/5gmlmrPzVUN3UXGuUMbod3Jg==", + "version": "8.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.13.2.tgz", + "integrity": "sha512-b6mR3mYkjRtJ443QZh9sc7CvGTce81J35F/XMr0OoWbx0KIM7TTTdyNP2XKObvkLpYnLpCrYDwI3CZnLezWvpg==", "dependencies": { - "@tanstack/table-core": "8.11.7" + "@tanstack/table-core": "8.13.2" }, "engines": { "node": ">=12" @@ -1856,9 +1773,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.11.7", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.7.tgz", - "integrity": "sha512-N3ksnkbPbsF3PjubuZCB/etTqvctpXWRHIXTmYfJFnhynQKjeZu8BCuHvdlLPpumKbA+bjY4Ay9AELYLOXPWBg==", + "version": "8.13.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.13.2.tgz", + "integrity": "sha512-/2saD1lWBUV6/uNAwrsg2tw58uvMJ07bO2F1IWMxjFRkJiXKQRuc3Oq2aufeobD3873+4oIM/DRySIw7+QsPPw==", "engines": { "node": ">=12" }, @@ -1891,9 +1808,9 @@ } }, "node_modules/@types/d3-path": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.2.tgz", - "integrity": "sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" }, "node_modules/@types/d3-scale": { "version": "4.0.8", @@ -1934,9 +1851,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", "dev": true }, "node_modules/@types/lodash-es": { @@ -1949,9 +1866,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", - "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==", + "version": "20.11.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", + "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1969,9 +1886,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "18.2.48", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", - "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", + "version": "18.2.65", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.65.tgz", + "integrity": "sha512-98TsY0aW4jqx/3RqsUXwMDZSWR1Z4CUlJNue8ueS2/wcxZOsz4xmW1X8ieaWVRHcmmQM3R8xVA4XWB3dJnWwDQ==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -1980,9 +1897,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", - "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", "devOptional": true, "dependencies": { "@types/react": "*" @@ -1995,9 +1912,9 @@ "devOptional": true }, "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/uuid": { @@ -2007,16 +1924,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.1.tgz", - "integrity": "sha512-roQScUGFruWod9CEyoV5KlCYrubC/fvG8/1zXuT0WTcxX87GnMMmnksMwSg99lo1xiKrBzw2icsJPMAw1OtKxg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz", + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/type-utils": "6.19.1", - "@typescript-eslint/utils": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/type-utils": "7.2.0", + "@typescript-eslint/utils": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2032,8 +1949,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2041,16 +1958,41 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.19.1.tgz", - "integrity": "sha512-WEfX22ziAh6pRE9jnbkkLGp/4RhTpffr2ZK5bJ18M8mIfA8A+k97U9ZyaXCEJRlmMHh7R9MJZWXp/r73DzINVQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4" }, "engines": { @@ -2061,7 +2003,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2070,13 +2012,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.19.1.tgz", - "integrity": "sha512-4CdXYjKf6/6aKNMSly/BP4iCSOpvMmqtDzRtqFyyAae3z5kkqEjKndR5vDHL8rSuMIIWP8u4Mw4VxLyxZW6D5w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1" + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2087,13 +2029,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.19.1.tgz", - "integrity": "sha512-0vdyld3ecfxJuddDjACUvlAeYNrHP/pDeQk2pWBR2ESeEzQhg52DF53AbI9QCBkYE23lgkhLCZNkHn2hEXXYIg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz", + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.19.1", - "@typescript-eslint/utils": "6.19.1", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/utils": "7.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2105,7 +2047,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -2113,10 +2055,35 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, "node_modules/@typescript-eslint/types": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.19.1.tgz", - "integrity": "sha512-6+bk6FEtBhvfYvpHsDgAL3uo4BfvnTnoge5LrrCj2eJN8g3IJdLTD4B/jK3Q6vo4Ql/Hoip9I8aB6fF+6RfDqg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2127,13 +2094,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.19.1.tgz", - "integrity": "sha512-aFdAxuhzBFRWhy+H20nYu19+Km+gFfwNO4TEqyszkMcgBDYQjmPJ61erHxuT2ESJXhlhrO7I5EFIlZ+qGR8oVA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/visitor-keys": "6.19.1", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2155,17 +2122,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.19.1.tgz", - "integrity": "sha512-JvjfEZuP5WoMqwh9SPAPDSHSg9FBHHGhjPugSRxu5jMfjvBpq5/sGTD+9M9aQ5sh6iJ8AY/Kk/oUYVEMAPwi7w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.19.1", - "@typescript-eslint/types": "6.19.1", - "@typescript-eslint/typescript-estree": "6.19.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", "semver": "^7.5.4" }, "engines": { @@ -2179,13 +2146,88 @@ "eslint": "^7.0.0 || ^8.0.0" } }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.19.1.tgz", - "integrity": "sha512-gkdtIO+xSO/SmI0W68DBg4u1KElmIUo3vXzgHyGPs6cxgB0sa3TlptRAAE0hUY1hM6FcDKEv7aIwiTGm76cXfQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.19.1", + "@typescript-eslint/types": "7.2.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2240,22 +2282,22 @@ } }, "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -2310,15 +2352,18 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" }, - "funding": { + "engines": { + "node": ">= 0.4" + }, + "funding": { "url": "https://github.com/sponsors/ljharb" } }, @@ -2350,17 +2395,55 @@ "node": ">=8" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "node_modules/array.prototype.filter": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz", + "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz", + "integrity": "sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz", + "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2405,31 +2488,44 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.tosorted": { + "node_modules/array.prototype.toreversed": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "es-shim-unscopables": "^1.0.0" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", + "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.1.0", + "es-shim-unscopables": "^1.0.2" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -2460,9 +2556,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", "dev": true, "funding": [ { @@ -2479,8 +2575,8 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -2497,10 +2593,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2518,9 +2617,9 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { "follow-redirects": "^1.15.4", "form-data": "^4.0.0", @@ -2569,9 +2668,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -2588,8 +2687,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -2612,14 +2711,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2643,9 +2747,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001579", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz", - "integrity": "sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "funding": [ { "type": "opencollective", @@ -2677,31 +2781,10 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2714,6 +2797,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -2762,251 +2848,18 @@ } }, "node_modules/cmdk": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-0.2.0.tgz", - "integrity": "sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", "dependencies": { - "@radix-ui/react-dialog": "1.0.0", - "command-score": "0.1.2" + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, - "node_modules/cmdk/node_modules/@radix-ui/primitive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", - "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", - "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-context": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", - "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dialog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.0.tgz", - "integrity": "sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-dismissable-layer": "1.0.0", - "@radix-ui/react-focus-guards": "1.0.0", - "@radix-ui/react-focus-scope": "1.0.0", - "@radix-ui/react-id": "1.0.0", - "@radix-ui/react-portal": "1.0.0", - "@radix-ui/react-presence": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-slot": "1.0.0", - "@radix-ui/react-use-controllable-state": "1.0.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.4" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz", - "integrity": "sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.0", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-use-callback-ref": "1.0.0", - "@radix-ui/react-use-escape-keydown": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", - "integrity": "sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.0.tgz", - "integrity": "sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-primitive": "1.0.0", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-id": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", - "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-portal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.0.tgz", - "integrity": "sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-presence": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", - "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0", - "@radix-ui/react-use-layout-effect": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-primitive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.0.tgz", - "integrity": "sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-slot": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.0.tgz", - "integrity": "sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", - "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", - "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.0.tgz", - "integrity": "sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.0" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", - "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0" - } - }, - "node_modules/cmdk/node_modules/react-remove-scroll": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.4.tgz", - "integrity": "sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3034,11 +2887,6 @@ "node": ">= 0.8" } }, - "node_modules/command-score": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/command-score/-/command-score-0.1.2.tgz", - "integrity": "sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==" - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -3080,8 +2928,7 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { "version": "3.2.4", @@ -3233,17 +3080,20 @@ "dev": true }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3320,11 +3170,12 @@ } }, "node_modules/dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "dependencies": { - "@babel/runtime": "^7.1.2" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, "node_modules/eastasianwidth": { @@ -3333,9 +3184,9 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/electron-to-chromium": { - "version": "1.4.642", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.642.tgz", - "integrity": "sha512-M4+u22ZJGpk4RY7tne6W+APkZhnnhmAH48FNl8iEFK2lEgob+U5rUQsIqQhvAwCXYpfd3H20pHK/ENsCvwTbsA==", + "version": "1.4.703", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.703.tgz", + "integrity": "sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==", "dev": true }, "node_modules/emoji-regex": { @@ -3344,9 +3195,9 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -3357,50 +3208,52 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.22.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz", + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.1", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.0", + "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.8", "string.prototype.trimend": "^1.0.7", "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.5", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -3409,37 +3262,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz", + "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==", "dev": true, "dependencies": { "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", + "es-abstract": "^1.22.4", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.2", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", + "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", + "internal-slot": "^1.0.7", "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "safe-array-concat": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -3472,9 +3356,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -3493,16 +3377,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -3548,12 +3432,12 @@ } }, "node_modules/eslint-config-next": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.1.0.tgz", - "integrity": "sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.1.3.tgz", + "integrity": "sha512-sUCpWlGuHpEhI0pIT0UtdSLJk5Z8E2DYinPTwsBiWaSYQomchdl0i60pjynY48+oXvtyWMQ7oE+G3m49yrfacg==", "dev": true, "dependencies": { - "@next/eslint-plugin-next": "14.1.0", + "@next/eslint-plugin-next": "14.1.3", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", @@ -3573,6 +3457,109 @@ } } }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -3631,9 +3618,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -3822,27 +3809,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.34.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz", + "integrity": "sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.7", + "array.prototype.findlast": "^1.2.4", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.17", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7", + "object.hasown": "^1.1.3", + "object.values": "^1.1.7", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.10" }, "engines": { "node": ">=4" @@ -3951,15 +3940,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3982,18 +3962,6 @@ "node": "*" } }, - "node_modules/eslint/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4117,9 +4085,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -4178,9 +4146,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/follow-redirects": { @@ -4252,6 +4220,30 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "11.0.12", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.12.tgz", + "integrity": "sha512-1VW4pk+4EH8RwWHdqntWTwF9peranyHn/BczvMzF9TGh/FwMKxoRZzkig8Nak9BQA8GymfIyCGo5adXwRuXDXA==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4307,16 +4299,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4330,13 +4326,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -4346,9 +4343,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", + "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -4481,21 +4478,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -4517,12 +4514,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4532,9 +4529,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4542,19 +4539,57 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "23.10.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.1.tgz", + "integrity": "sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-resources-to-backend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", + "integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/immer": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", - "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.4.tgz", + "integrity": "sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -4602,12 +4637,12 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -4632,14 +4667,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4792,18 +4829,21 @@ } }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -4861,21 +4901,27 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4912,12 +4958,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4927,10 +4973,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4948,13 +4997,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5010,9 +5062,9 @@ } }, "node_modules/jotai": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.6.4.tgz", - "integrity": "sha512-RniwQPX4893YlNR1muOtyUGHYaTD1fhEN4qnOuZJSrDHj6xdEMrqlRSN/hCm2fshwk78ruecB/P2l+NCVWe6TQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jotai/-/jotai-2.7.0.tgz", + "integrity": "sha512-4qsyFKu4MprI39rj2uoItyhu24NoCHzkOV7z70PQr65SpzV6CSyhQvVIfbNlNqOIOspNMdf5OK+kTXLvqe63Jw==", "engines": { "node": ">=12.20.0" }, @@ -5200,17 +5252,17 @@ } }, "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "engines": { "node": "14 || >=16.14" } }, "node_modules/lucide-react": { - "version": "0.314.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.314.0.tgz", - "integrity": "sha512-c2zOW7TOyKxPCaSs1og2lFdoI3SR4iii3yrZJU2Zpdc78nOw4fUmrcFNultaCiwZcr4CyiKdzjMdvKNlBBk+UQ==", + "version": "0.357.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.357.0.tgz", + "integrity": "sha512-ILK6Ye6BMFyXyIHqG8FwMit1XKrj4KocLLLW4fDAAwsFjt6gSL9U6e3sZlbARIOqv0oP5XzLVacHEcb2SxuWkw==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } @@ -5324,12 +5376,20 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", - "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", + "version": "14.1.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.1.3.tgz", + "integrity": "sha512-oexgMV2MapI0UIWiXKkixF8J8ORxpy64OuJ/J9oVUmIthXOUCcuVEZX+dtpgq7wIfIqtBwQsKEDXejcjTsan9g==", "dependencies": { - "@next/env": "14.1.0", + "@next/env": "14.1.3", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", @@ -5344,15 +5404,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.0", - "@next/swc-darwin-x64": "14.1.0", - "@next/swc-linux-arm64-gnu": "14.1.0", - "@next/swc-linux-arm64-musl": "14.1.0", - "@next/swc-linux-x64-gnu": "14.1.0", - "@next/swc-linux-x64-musl": "14.1.0", - "@next/swc-win32-arm64-msvc": "14.1.0", - "@next/swc-win32-ia32-msvc": "14.1.0", - "@next/swc-win32-x64-msvc": "14.1.0" + "@next/swc-darwin-arm64": "14.1.3", + "@next/swc-darwin-x64": "14.1.3", + "@next/swc-linux-arm64-gnu": "14.1.3", + "@next/swc-linux-arm64-musl": "14.1.3", + "@next/swc-linux-x64-gnu": "14.1.3", + "@next/swc-linux-x64-musl": "14.1.3", + "@next/swc-win32-arm64-msvc": "14.1.3", + "@next/swc-win32-ia32-msvc": "14.1.3", + "@next/swc-win32-x64-msvc": "14.1.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5369,14 +5429,22 @@ } } }, + "node_modules/next-i18n-router": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/next-i18n-router/-/next-i18n-router-5.3.1.tgz", + "integrity": "sha512-OZGy2AcHbI6RtZ90NFrKrtqO6XEPqQ2AkOOeuFpKbxaSSfw6Sa+lUCX1t/nixlkkkjOOqO9VwCAHe+GB84bc+A==", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.2", + "negotiator": "^0.6.3" + } + }, "node_modules/next-themes": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz", - "integrity": "sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", "peerDependencies": { - "next": "*", - "react": "*", - "react-dom": "*" + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" } }, "node_modules/next/node_modules/postcss": { @@ -5513,15 +5581,16 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz", + "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "array.prototype.filter": "^1.0.3", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0" } }, "node_modules/object.hasown": { @@ -5709,10 +5778,19 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -5805,11 +5883,14 @@ } }, "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/postcss-nested": { @@ -5831,9 +5912,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", - "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -5857,9 +5938,9 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5949,13 +6030,23 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { - "version": "7.49.3", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.3.tgz", - "integrity": "sha512-foD6r3juidAT1cOZzpmD/gOKt7fRsDhXXZ0y28+Al1CHgX+AY1qIN9VSIIItXRq1dN68QrRwl1ORFlwjBaAqeQ==", + "version": "7.51.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.0.tgz", + "integrity": "sha512-BggOy5j58RdhdMzzRUHGOYhSz1oeylFAv6jUSG86OvCIvlAvS7KvnRY7yoAf2pfEiPN7BesnR0xx73nEk3qIiw==", "engines": { - "node": ">=18", - "pnpm": "8" + "node": ">=12.22.0" }, "funding": { "type": "opencollective", @@ -5965,16 +6056,32 @@ "react": "^16.8.0 || ^17 || ^18" } }, + "node_modules/react-i18next": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.0.tgz", + "integrity": "sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" - }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -6000,9 +6107,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", + "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -6021,17 +6128,17 @@ } }, "node_modules/react-smooth": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.5.tgz", - "integrity": "sha512-BMP2Ad42tD60h0JW6BFaib+RJuV5dsXJK9Baxiv/HlNFjvRLqA9xrNKxVWnUIZPQfzUwGXIlU/dSYLU+54YGQA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.0.tgz", + "integrity": "sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==", "dependencies": { - "fast-equals": "^5.0.0", - "react-transition-group": "2.9.0" + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" }, "peerDependencies": { - "prop-types": "^15.6.0", - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/react-style-singleton": { @@ -6069,18 +6176,18 @@ } }, "node_modules/react-transition-group": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", - "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { - "dom-helpers": "^3.4.0", + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" + "prop-types": "^15.6.2" }, "peerDependencies": { - "react": ">=15.0.0", - "react-dom": ">=15.0.0" + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/read-cache": { @@ -6103,15 +6210,15 @@ } }, "node_modules/recharts": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.11.0.tgz", - "integrity": "sha512-5s+u1m5Hwxb2nh0LABkE3TS/lFqFHyWl7FnPbQhHobbQQia4ih1t3o3+ikPYr31Ns+kYe4FASIthKeKi/YYvMg==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.2.tgz", + "integrity": "sha512-9bpxjXSF5g81YsKkTSlaX7mM4b6oYI1mIYck6YkUcWuL3tomADccI51/6thY4LmvhYuRTwpfrOvE80Zc3oBRfQ==", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", - "lodash": "^4.17.19", + "lodash": "^4.17.21", "react-is": "^16.10.2", - "react-smooth": "^2.0.5", + "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" @@ -6120,7 +6227,6 @@ "node": ">=14" }, "peerDependencies": { - "prop-types": "^15.6.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" } @@ -6134,15 +6240,16 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", + "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.0.0", + "get-intrinsic": "^1.2.3", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" }, @@ -6159,14 +6266,15 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -6306,13 +6414,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -6324,13 +6432,13 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -6353,9 +6461,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -6380,30 +6488,32 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6429,14 +6539,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6508,28 +6622,34 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { + "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/string.prototype.matchall": { @@ -6598,17 +6718,14 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi-cjs": { @@ -6623,14 +6740,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6825,9 +6934,9 @@ } }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -6841,12 +6950,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -6874,27 +6983,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6920,29 +7008,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6952,16 +7041,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6971,23 +7061,29 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz", + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7107,9 +7203,9 @@ } }, "node_modules/usehooks-ts": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz", - "integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.0.1.tgz", + "integrity": "sha512-bgJ8S9w/SnQyACd3RvWp3CGncROxEENGqQLCsdaoyTb0zTENIna7MIV3OW6ywCfPaYYD2OPokw7oLPmSLLWP4w==", "dependencies": { "lodash.debounce": "^4.0.8" }, @@ -7138,9 +7234,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.9.0", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.0.tgz", - "integrity": "sha512-n1A0J1xgwHb5nh56M0d8XlQabMCeTktvEqqr5WNAHspWEsVVGGaaaRg0TcQUtyC1akX0Cox1lMZdIv0Jl7o0ew==", + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -7158,6 +7254,14 @@ "d3-timer": "^3.0.1" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7215,31 +7319,34 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7281,28 +7388,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -7321,15 +7406,40 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -7345,9 +7455,12 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -7373,9 +7486,9 @@ } }, "node_modules/zustand": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.0.tgz", - "integrity": "sha512-zlVFqS5TQ21nwijjhJlx4f9iGrXSL0o/+Dpy4txAP22miJ8Ti6c1Ol1RLNN98BMib83lmDH/2KmLwaNXpjrO1A==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", "dependencies": { "use-sync-external-store": "1.2.0" }, diff --git a/src/green-app/package.json b/src/green-app/package.json index b122a002..f9de76a7 100644 --- a/src/green-app/package.json +++ b/src/green-app/package.json @@ -10,7 +10,6 @@ "format": "prettier --write ./src" }, "dependencies": { - "sage-common": "file:../sage-common", "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -29,56 +28,63 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", - "@tanstack/query-sync-storage-persister": "^5.21.4", - "@tanstack/react-query": "^5.17.19", - "@tanstack/react-query-devtools": "^5.17.21", - "@tanstack/react-query-persist-client": "^5.21.4", - "@tanstack/react-table": "^8.11.7", + "@tanstack/query-sync-storage-persister": "^5.28.0", + "@tanstack/react-query": "^5.28.0", + "@tanstack/react-query-devtools": "^5.28.0", + "@tanstack/react-query-persist-client": "^5.28.0", + "@tanstack/react-table": "^8.13.2", "@types/object-hash": "^3.0.6", - "axios": "^1.6.5", + "axios": "^1.6.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", - "cmdk": "^0.2.0", + "cmdk": "^1.0.0", "dayjs": "^1.11.10", - "immer": "^10.0.3", - "jotai": "^2.6.4", + "framer-motion": "^11.0.12", + "i18next": "^23.10.1", + "i18next-resources-to-backend": "^1.2.0", + "immer": "^10.0.4", + "jotai": "^2.7.0", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", - "lucide-react": "^0.314.0", - "next": "14.1.0", - "next-themes": "^0.2.1", + "lucide-react": "^0.357.0", + "next": "^14.1.3", + "next-i18n-router": "^5.3.1", + "next-themes": "^0.3.0", "object-hash": "^3.0.0", "react": "^18", "react-dom": "^18", - "react-hook-form": "^7.49.3", + "react-error-boundary": "^4.0.13", + "react-hook-form": "^7.51.0", + "react-i18next": "^14.1.0", "react-toastify": "^10.0.4", - "recharts": "^2.11.0", + "recharts": "^2.12.2", "rxjs": "^7.8.1", + "sage-common": "file:../sage-common", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^2.14.0", + "usehooks-ts": "^3.0.1", "uuid": "^9.0.1", "zod": "^3.22.4", - "zustand": "^4.5.0" + "zustand": "^4.5.2" }, "devDependencies": { - "@tanstack/eslint-plugin-query": "^5.17.22", + "@tanstack/eslint-plugin-query": "^5.27.7", "@types/lodash-es": "^4.17.12", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^6.19.1", - "@typescript-eslint/parser": "^6.19.1", - "autoprefixer": "^10.4.17", - "eslint": "^8.56.0", - "eslint-config-next": "^14.1.0", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", + "eslint-config-next": "^14.1.3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", "postcss": "^8", - "prettier": "^3.2.4", + "prettier": "^3.2.5", "tailwindcss": "^3.4.1", "typescript": "^5" } diff --git a/src/green-app/src/app/account/page.tsx b/src/green-app/src/app/[locale]/account/page.tsx similarity index 100% rename from src/green-app/src/app/account/page.tsx rename to src/green-app/src/app/[locale]/account/page.tsx diff --git a/src/green-app/src/app/discord/connect/page.tsx b/src/green-app/src/app/[locale]/discord/connect/page.tsx similarity index 100% rename from src/green-app/src/app/discord/connect/page.tsx rename to src/green-app/src/app/[locale]/discord/connect/page.tsx diff --git a/src/green-app/src/app/ggg/connect/page.tsx b/src/green-app/src/app/[locale]/ggg/connect/page.tsx similarity index 100% rename from src/green-app/src/app/ggg/connect/page.tsx rename to src/green-app/src/app/[locale]/ggg/connect/page.tsx diff --git a/src/green-app/src/app/globals.css b/src/green-app/src/app/[locale]/globals.css similarity index 94% rename from src/green-app/src/app/globals.css rename to src/green-app/src/app/[locale]/globals.css index c205bdd3..d47a67ec 100644 --- a/src/green-app/src/app/globals.css +++ b/src/green-app/src/app/[locale]/globals.css @@ -114,4 +114,10 @@ * { min-width: 0; +} + +@keyframes pulsatingDot { + 0% {transform: scale(0.1, 0.1); opacity: 0.0;} + 50% {opacity: 1.0;} + 100% {transform: scale(1.2, 1.2); opacity: 0.0;} } \ No newline at end of file diff --git a/src/green-app/src/app/[locale]/layout.tsx b/src/green-app/src/app/[locale]/layout.tsx new file mode 100644 index 00000000..9f65bc9a --- /dev/null +++ b/src/green-app/src/app/[locale]/layout.tsx @@ -0,0 +1,75 @@ +import Navbar from '@/components/navbar' +import { ProfileMenu } from '@/components/profile-menu' +import { Providers } from '@/components/providers' +import TranslationsProvider from '@/components/translations-provider' +import { cn } from '@/lib/utils' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import utc from 'dayjs/plugin/utc' +import { dir } from 'i18next' +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import Image from 'next/image' +import 'react-toastify/dist/ReactToastify.css' +import poestackPic from '../../assets/poestack.png' +import '../../assets/toastify.css' +import initTranslations, { i18nConfig } from '../../config/i18n.config' +import './globals.css' + +dayjs.extend(relativeTime) +dayjs.extend(utc) + +const inter = Inter({ subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'PoeStack - Bulk', + description: 'Sell and buy items easily.' +} + +export function generateStaticParams() { + return i18nConfig.locales.map((locale) => ({ locale })) +} + +export default async function RootLayout({ + children, + params: { locale } +}: Readonly<{ + children: React.ReactNode + params: { locale: string } +}>) { + const { t, resources } = await initTranslations(locale) + + return ( + + + + +
+
+
+
+ poestack + +
{t('title.appTitle')}
+
+ +
+
+ +
+
+
+
{children}
+
+
+ + + ) +} diff --git a/src/green-app/src/app/listing-tool/listing-card.tsx b/src/green-app/src/app/[locale]/listing-tool/listing-card.tsx similarity index 82% rename from src/green-app/src/app/listing-tool/listing-card.tsx rename to src/green-app/src/app/[locale]/listing-tool/listing-card.tsx index 50ca98db..e62433c2 100644 --- a/src/green-app/src/app/listing-tool/listing-card.tsx +++ b/src/green-app/src/app/[locale]/listing-tool/listing-card.tsx @@ -8,24 +8,28 @@ import { cn } from '@/lib/utils' import { ListingMode } from '@/types/sage-listing-type' import { LayoutListIcon, PackageIcon, RefreshCwIcon } from 'lucide-react' import { useListingToolStore } from './listingToolStore' +import { useTranslation } from 'react-i18next' type ListingCardProps = { listingMode: ListingMode selectedCategory: string | null + selectedSubCategory: string | null postListingButtonDisabled: boolean isPostListingLoading: boolean - onListingModeChange: (mode: ListingMode) => void + onListingModeChange: (listingMode: ListingMode, category?: string | null) => void onPostItemsClicked: () => void } export function ListingCard({ listingMode, selectedCategory, + selectedSubCategory, postListingButtonDisabled, isPostListingLoading, onListingModeChange, onPostItemsClicked }: ListingCardProps) { + const { t } = useTranslation() const setMultiplier = useListingToolStore((state) => state.setLocalMultiplier) const multiplier = useListingToolStore((state) => state.localMultiplier) const totalPrice = useListingToolStore((state) => state.totalPrice) @@ -47,12 +51,12 @@ export function ListingCard({ {listingMode === 'bulk' ? ( <> - {'Whole Offering '} + {t('label.wholeOffering')} ) : ( <> - {'Individual Priced Items '} + {t('label.individualOffering')} )} @@ -71,28 +75,30 @@ export function ListingCard({
- Whole offering: Sell the whole offering at once + + {t('label.wholeOfferingTT')}
- Individual: Sell individual priced items + + {t('label.individualOfferingTT')}
- + { - setMultiplier(e[0], selectedCategory) + setMultiplier(e[0], selectedCategory + (selectedSubCategory || '')) }} />
- +
@@ -104,7 +110,7 @@ export function ListingCard({ } className="flex flex-row gap-2" > - Post Offering + {t('action.postOffering')}
diff --git a/src/green-app/src/app/[locale]/listing-tool/listing-tool-handler.tsx b/src/green-app/src/app/[locale]/listing-tool/listing-tool-handler.tsx new file mode 100644 index 00000000..0fab8485 --- /dev/null +++ b/src/green-app/src/app/[locale]/listing-tool/listing-tool-handler.tsx @@ -0,0 +1,384 @@ +import { useListingToolStore } from '@/app/[locale]/listing-tool/listingToolStore' +import { currentUserAtom } from '@/components/providers' +import { DEFAULT_VALUATION_INDEX } from '@/lib/constants' +import { listStash, listValuations } from '@/lib/http-util' +import { ItemGroupingService } from 'sage-common' +import { + createCompactTab, + filterPricedItems, + mapItemsToDisplayedItems, + mapItemsToPricedItems, + mapMapStashItemToPoeItem, + mergeItemStacks, + mergeItems +} from '@/lib/item-util' +import { LISTING_CATEGORIES, ListingCategory, ListingSubCategory } from '@/lib/listing-categories' +import { IStashTab } from '@/types/echo-api/stash' +import { GroupedItem, StashItem, ValuatedItem } from '@/types/item' +import { PoeItem } from '@/types/poe-api-models' +import { useQueries } from '@tanstack/react-query' +import { useAtomValue } from 'jotai' +import { memo, useEffect, useMemo } from 'react' +import { useShallow } from 'zustand/react/shallow' + +type SelectableCategories = Record< + string, + { + category: ListingCategory + count: number + subcategories: Record< + string, + { + subCategory: ListingSubCategory + count: number + } + > + } +> + +type ListingToolHandlerProps = { + setRefetchAll: (refetchAll: () => void) => void + setStashListFetching: (fetching: boolean) => void +} + +const ListingToolHandler = ({ setRefetchAll, setStashListFetching }: ListingToolHandlerProps) => { + const currentUser = useAtomValue(currentUserAtom) + const stashes = useListingToolStore(useShallow((state) => state.stashes[state.league] || [])) + const league = useListingToolStore((state) => state.league) + const selectedCategory = useListingToolStore((state) => state.category) + const selectedSubCategory = useListingToolStore((state) => state.subCategory) + const setSelectedCategory = useListingToolStore((state) => state.setCategory) + const setSelectedSubCategory = useListingToolStore((state) => state.setSubCategory) + const setInitialItems = useListingToolStore((state) => state.setInitialItems) + const setSelectableCategories = useListingToolStore((state) => state.setSelectableCategories) + const setSelectableSubCategories = useListingToolStore( + (state) => state.setSelectableSubCategories + ) + + // const setRefetchAll = useSetAtom(refetchAllAtom) + // const setStashListFetching = useSetAtom(stashListFetchingAtom) + + // TODO: Attention, this data is not stable! idk how to fix it. Is not fixable with useMemo somehow idk. + const groupedItemsResults = useQueries({ + queries: stashes + ? stashes.map((stash) => { + return { + queryKey: [currentUser?.profile?.uuid, 'stash', league, stash.id], + queryFn: async () => { + if (!currentUser?.profile?.uuid || !league) return [] + const stashItems = await listStash(league, stash.id) + + let items: PoeItem[] = [] + if (stashItems.type === 'MapStash') { + items = mapMapStashItemToPoeItem(stashItems, league) + } else if (stashItems.items) { + items = stashItems.items + } + return new ItemGroupingService().withGroup(items).map((x) => ({ ...x, stash })) + }, + enabled: false + } + }) + : [] + }) + + const { + data: [categoryTagItem, selectableCategories, ungroupedItems], + isGroupedItemsSuccess, + isGroupedItemsLoading, + isGroupedItemsFetching, + isGroupedItemsError, + refetchAll + } = useMemo(() => { + const categoryTagItem: Record = {} + const selectableCategories: SelectableCategories = {} + const ungroupedItems: StashItem[] = [] + + groupedItemsResults.forEach((result) => { + result.data?.map((item) => { + let itemInSelectedCategory = false + + if (item.group) { + const selectedItemMainCategory = LISTING_CATEGORIES.find( + (c) => c.name === selectedCategory && c.tags.includes(item.group!.primaryGroup.tag) + ) + const selectedItemSubCategory = selectedItemMainCategory?.subCategories.find( + (c) => c.name === selectedSubCategory + ) + + itemInSelectedCategory = selectedSubCategory + ? !!selectedItemSubCategory?.tags.includes(item.group.primaryGroup.tag) + : !!selectedItemMainCategory + + const currentItemCategories = LISTING_CATEGORIES.filter((e) => + e.tags.includes(item.group!.primaryGroup.tag) + ) + + currentItemCategories.forEach((currentCategory) => { + const itemIncluded = currentCategory.filter?.({ group: item.group!.primaryGroup }) + if (itemIncluded === false) { + itemInSelectedCategory = false + return + } + if (!selectableCategories[currentCategory.name]) { + selectableCategories[currentCategory.name] = { + category: currentCategory, + count: 1, + subcategories: {} + } + } else { + selectableCategories[currentCategory.name].count += 1 + } + + const selectableSubcategories = + selectableCategories[currentCategory.name].category.subCategories + + const selectableMainSubcategories = selectableSubcategories.filter((c) => !c.restItems) + const selectableRestSubcategories = selectableSubcategories.filter((c) => c.restItems) + + const excludeItem = (subCategory: ListingSubCategory) => { + if (subCategory === selectedItemSubCategory) { + itemInSelectedCategory = false + } + } + + selectableMainSubcategories.forEach((subCategory) => { + if (!item.group) return excludeItem(subCategory) + const itemInGroup = subCategory.tags.includes(item.group.primaryGroup.tag) + const itemIncluded = subCategory.filter?.({ group: item.group.primaryGroup }) + if (!((itemIncluded ?? true) && itemInGroup)) return excludeItem(subCategory) + + if (!selectableCategories[currentCategory.name].subcategories[subCategory.name]) { + selectableCategories[currentCategory.name].subcategories[subCategory.name] = { + count: 1, + subCategory + } + } else { + selectableCategories[currentCategory.name].subcategories[subCategory.name].count += + 1 + } + }) + selectableRestSubcategories.forEach((subCategory) => { + if (!item.group) return excludeItem(subCategory) + const itemInGroup = subCategory.tags.includes(item.group.primaryGroup.tag) + const itemIncluded = subCategory.filter?.({ group: item.group.primaryGroup }) + if (!((itemIncluded ?? true) && itemInGroup)) return excludeItem(subCategory) + const itemInOtherCat = selectableMainSubcategories.some((c) => { + const itemInGroup = c.tags.includes(item.group!.primaryGroup.tag) + const itemIncluded = c.filter?.({ group: item.group!.primaryGroup }) + return (itemIncluded ?? true) && itemInGroup + }) + if (itemInOtherCat) return excludeItem(subCategory) + + if (!selectableCategories[currentCategory.name].subcategories[subCategory.name]) { + selectableCategories[currentCategory.name].subcategories[subCategory.name] = { + count: 1, + subCategory + } + } else { + selectableCategories[currentCategory.name].subcategories[subCategory.name].count += + 1 + } + }) + }) + } + + if (item.group && (itemInSelectedCategory || !selectedCategory)) { + if (categoryTagItem[item.group.primaryGroup.tag]) { + categoryTagItem[item.group.primaryGroup.tag].push(item) + } else { + categoryTagItem[item.group.primaryGroup.tag] = [item] + } + } else if (!selectedCategory) { + // No category items; No valuated items; Only stackable items will be shown + ungroupedItems.push(item) + } + }) + }) + + console.log(selectableCategories) + + return { + data: [categoryTagItem, selectableCategories, ungroupedItems], + isGroupedItemsSuccess: groupedItemsResults.some((result) => result.isSuccess), + isGroupedItemsLoading: groupedItemsResults.some((result) => result.isLoading), + isGroupedItemsFetching: groupedItemsResults.some((result) => result.isFetching), + isGroupedItemsError: groupedItemsResults.some((result) => result.isError), + refetchAll: () => + groupedItemsResults.forEach((result) => { + result.refetch() + }) + } + }, [groupedItemsResults, selectedCategory, selectedSubCategory]) + + useEffect(() => { + // Autoselect logic: + // - If the selected tag has items autoselect the first tag which was found with the most items in it + // - If the next stashes contains the selected tag, then do not change the tag. Even if stashes are reloaded + // - If a stash selected but not loaded the tag will not deselected + // - Auto deselect tag when the selected tag is not available + if (!(isGroupedItemsSuccess && !isGroupedItemsFetching)) return + + const selectedCategoryWithItems = Object.values(selectableCategories).find( + (category) => category.category.name === selectedCategory && category.count > 0 + ) + + if (selectedCategoryWithItems) { + const selectedSubCategoryWithItems = Object.values( + selectedCategoryWithItems.subcategories + ).find((category) => category.subCategory.name === selectedSubCategory && category.count > 0) + if (!selectedSubCategoryWithItems) { + const subCategory = Object.entries(selectedCategoryWithItems.subcategories).toSorted( + (a, b) => b[1].count - a[1].count + )[0] + setSelectedSubCategory(subCategory?.[0] || null) + } + return + } + + if (Object.keys(selectableCategories).length === 0) { + console.log('Deselect category') + setSelectedCategory(null) + setSelectedSubCategory(null) + return + } + + const category = Object.entries(selectableCategories).toSorted( + (a, b) => b[1].count - a[1].count + )[0] + + const subCategory = Object.entries(category[1].subcategories).toSorted( + (a, b) => b[1].count - a[1].count + )[0] + + if (category[0]) { + console.log('Autoselect category', category[0]) + setSelectedCategory(category[0]) + setSelectedSubCategory(subCategory?.[0] || null) + } else { + console.log('Deselect category') + setSelectedCategory(null) + setSelectedSubCategory(null) + } + // Some objects are not stable! We use booleans to determine the change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [league, stashes, isGroupedItemsSuccess, isGroupedItemsFetching, setSelectedCategory]) + + const { + data: displayedItems, + isValuationPending, + isValuationError + } = useQueries({ + queries: + categoryTagItem && league + ? Object.keys(categoryTagItem).map((tag) => { + return { + queryKey: ['valuations', league, tag], + queryFn: () => listValuations(league, tag), + gcTime: 20 * 60 * 1000, + staleTime: 20 * 60 * 1000, + enabled: !!league && !!tag + } + }) + : [], // if users is undefined, an empty array will be returned + combine: (valuationResults) => { + const valuationShards = valuationResults + .filter((x) => x.data && !x.isPending) + .map((x) => x.data!) + + const valuationItems = Object.entries(categoryTagItem) + .map(([tag, items]) => { + const valuations = valuationShards.find((shard) => shard.meta.tag === tag)?.valuations + return items.map((item): GroupedItem | ValuatedItem => { + if (valuations && item.group && valuations[item.group.primaryGroup.hash]) { + return { + ...item, + valuation: valuations[item.group.primaryGroup.hash] + } + } + return item + }) + }) + .flat() + + // Merge all items to stashtabs + const mergedStashItems: { stashTab: IStashTab; valuation: ValuatedItem[] }[] = [] + valuationItems.forEach((item) => { + if (item) { + const stashTab = mergedStashItems.find((x) => x.stashTab.id === item.stash.id) + if (stashTab) { + stashTab.valuation.push(item) + } else { + mergedStashItems.push({ stashTab: item.stash, valuation: [item] }) + } + } + }) + // Not grouped items - Which are not selectable and an overprice can not be set + ungroupedItems.forEach((item) => { + const stashTab = mergedStashItems.find((x) => x.stashTab.id === item.stash.id) + if (stashTab) { + stashTab.valuation.push(item) + } else { + mergedStashItems.push({ stashTab: item.stash, valuation: [item] }) + } + }) + const currentState = useListingToolStore.getState() + const selectedMultiplier = currentState.localMultiplier / 100 + const overprices = currentState.overprices + + let displayedItems = mergedStashItems + .map((valuatedStash) => { + const compactStash = createCompactTab(valuatedStash.stashTab) + let pricedItems = mapItemsToPricedItems( + valuatedStash.valuation, + compactStash, + DEFAULT_VALUATION_INDEX + ) + pricedItems = filterPricedItems(pricedItems, selectedCategory, selectedSubCategory) + const pricedStackedItems = mergeItems(pricedItems) + return mapItemsToDisplayedItems(pricedStackedItems, selectedMultiplier, overprices) + }) + .flat() + + displayedItems = mergeItemStacks(displayedItems) + + const isValuationPending = valuationResults.some((result) => result.isPending) + const isValuationError = + isGroupedItemsError || valuationResults.some((result) => result.isError) + return { + data: isGroupedItemsLoading || isValuationPending || isValuationError ? [] : displayedItems, + isValuationPending, + isValuationError + } + } + }) + + useEffect(() => { + console.log('Set setInitialItems') + setInitialItems(displayedItems) + }, [displayedItems, selectedCategory, selectedSubCategory, setInitialItems]) + + useEffect(() => { + console.log('Set refetchAll') + setRefetchAll(refetchAll) + }, [refetchAll, setRefetchAll]) + + useEffect(() => { + console.log('Set setStashListFetching') + setStashListFetching(isGroupedItemsLoading || isValuationPending) + }, [isGroupedItemsLoading, isValuationPending, isValuationError, setStashListFetching]) + + useEffect(() => { + console.log('Set setSelectableCategories and setSelectableSubCategories') + setSelectableCategories(Object.values(selectableCategories).map((cat) => cat.category)) + setSelectableSubCategories( + Object.values(selectableCategories).flatMap((cat) => + Object.values(cat.subcategories).map((subCat) => subCat.subCategory) + ) + ) + }, [selectableCategories, setSelectableCategories, setSelectableSubCategories]) + + return null +} + +export default memo(ListingToolHandler) diff --git a/src/green-app/src/app/[locale]/listing-tool/listing-tool-table-columns.tsx b/src/green-app/src/app/[locale]/listing-tool/listing-tool-table-columns.tsx new file mode 100644 index 00000000..0b3039fc --- /dev/null +++ b/src/green-app/src/app/[locale]/listing-tool/listing-tool-table-columns.tsx @@ -0,0 +1,46 @@ +'use client' + +import { checkColumn } from '@/components/table-columns/check-column' +import { historyColumn } from '@/components/table-columns/history-column' +import { nameColumn } from '@/components/table-columns/name-column' +import { priceColumn } from '@/components/table-columns/price-column' +import { propsColumn } from '@/components/table-columns/props-column' +import { quantityColumn } from '@/components/table-columns/quantity-column' +import { priceInputColumn } from '@/components/table-columns/price-input-column' +import { tabsColumn } from '@/components/table-columns/tabs-column' +import { IDisplayedItem } from '@/types/echo-api/priced-item' +import { ColumnDef } from '@tanstack/react-table' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import utc from 'dayjs/plugin/utc' +import { tagColumn } from '@/components/table-columns/tag-column' +dayjs.extend(relativeTime) +dayjs.extend(utc) + +export const listingToolTableEditModeColumns = (): ColumnDef[] => [ + checkColumn(), + nameColumn(), + propsColumn(), + tagColumn(), + tabsColumn(), + quantityColumn(), + historyColumn(), + historyColumn({ mode: '7 days' }), + priceInputColumn(), + priceColumn({ + accessorKey: 'calculatedPrice', + headerName: 'price', + accessorFn: (item) => (item.calculatedPrice !== undefined ? item.calculatedPrice : '?') + }), + priceColumn({ + accessorKey: 'calculatedTotal', + headerName: 'totalPrice', + accessorFn: (item) => item.calculatedTotalPrice + }), + priceColumn({ + accessorKey: 'cumulative', + headerName: 'commulativePrice', + cumulativeColumn: 'calculatedTotal', + enableSorting: false + }) +] diff --git a/src/green-app/src/app/[locale]/listing-tool/listing-tool-table.tsx b/src/green-app/src/app/[locale]/listing-tool/listing-tool-table.tsx new file mode 100644 index 00000000..4f21b86f --- /dev/null +++ b/src/green-app/src/app/[locale]/listing-tool/listing-tool-table.tsx @@ -0,0 +1,156 @@ +/* eslint-disable no-extra-boolean-cast */ +'use client' + +import DataTable, { DataTableOptions } from '@/components/data-table/data-table' +import { useSkipper } from '@/hooks/useSkipper' +import { IDisplayedItem } from '@/types/echo-api/priced-item' +import { + ColumnDef, + ColumnOrderState, + ColumnSizingState, + FilterFnOption, + Table, + VisibilityState +} from '@tanstack/react-table' +import { atom, useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { memo, useEffect, useMemo, useState } from 'react' +import { useListingToolStore } from './listingToolStore' + +export const modifiedItemsAtom = atom([]) + +const columnSizingAtom = atomWithStorage('lt-table-columnSizing', {}) + +interface DataTableProps { + columns: ColumnDef[] + className?: string + isLoading?: boolean + globalFilter: string + onGlobalFilterChange: (value: string) => void + globalFilterFn?: FilterFnOption + columnVisibility: VisibilityState + columnOrder: ColumnOrderState + onColumnVisibility: React.Dispatch> + onColumnOrder: React.Dispatch> + tableRef: React.MutableRefObject | undefined> +} + +// Tutorial: https://ui.shadcn.com/docs/components/data-table +const ListingToolTable = ({ + columns, + className, + isLoading, + globalFilter, + onGlobalFilterChange, + globalFilterFn, + columnVisibility, + columnOrder, + onColumnVisibility, + onColumnOrder, + tableRef +}: DataTableProps) => { + // const { t } = useTranslation(); + + const modifiedItems = useListingToolStore((state) => state.modifiedItems) + const selectedItems = useListingToolStore((state) => state.selectedItems) + const setSelectedItems = useListingToolStore((state) => state.setSelectedItems) + const updateData = useListingToolStore((state) => state.updateData) + + const [columnSizing, setColumnSizing] = useAtom(columnSizingAtom) + + const [localColumnSizing, setLocalColumnSizing] = useState(columnSizing) + + useEffect(() => { + const timeout = setTimeout(() => { + setColumnSizing(localColumnSizing) + }, 250) + return () => { + clearTimeout(timeout) + } + }, [localColumnSizing, setColumnSizing]) + + const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper() + + const pageSizes = useMemo(() => [10, 15, 25, 50, 75, 100], []) + + const tableOptions = useMemo((): DataTableOptions => { + return { + data: modifiedItems, + columns, + enableRowSelection: (row) => !!row.original.group, + getRowId: (row) => `${row.group?.hash || row.displayName}`, + autoResetPageIndex, + enableMultiSort: false, + onGlobalFilterChange: onGlobalFilterChange, + onRowSelectionChange: setSelectedItems, + globalFilterFn: globalFilterFn, + onColumnVisibilityChange: onColumnVisibility, + onColumnOrderChange: onColumnOrder, + onColumnSizingChange: setLocalColumnSizing, + state: { + globalFilter: globalFilter, + rowSelection: selectedItems, + columnVisibility: columnVisibility, + columnOrder: columnOrder, + columnSizing: localColumnSizing + }, + initialState: { + pagination: { + pageSize: 25 + }, + sorting: [ + columnVisibility['2_day_history'] ?? true + ? { + desc: true, + id: '2_day_history' + } + : { + desc: true, + id: '7_day_history' + } + ], + columnVisibility: { + tag: false, + cumulative: false, + '7_day_history': false + } + }, + meta: { + // https://muhimasri.com/blogs/react-editable-table/ + updateData: (...params) => { + skipAutoResetPageIndex() + updateData(...params) + } + } + } + }, [ + autoResetPageIndex, + columnOrder, + columnVisibility, + columns, + globalFilter, + globalFilterFn, + localColumnSizing, + modifiedItems, + onColumnOrder, + onColumnVisibility, + onGlobalFilterChange, + selectedItems, + setSelectedItems, + skipAutoResetPageIndex, + updateData + ]) + + return ( +
+ +
+ ) +} + +export default memo(ListingToolTable) diff --git a/src/green-app/src/app/listing-tool/listingToolStore.ts b/src/green-app/src/app/[locale]/listing-tool/listingToolStore.ts similarity index 78% rename from src/green-app/src/app/listing-tool/listingToolStore.ts rename to src/green-app/src/app/[locale]/listing-tool/listingToolStore.ts index f79c9be9..d4072f01 100644 --- a/src/green-app/src/app/listing-tool/listingToolStore.ts +++ b/src/green-app/src/app/[locale]/listing-tool/listingToolStore.ts @@ -2,22 +2,32 @@ import { SUPPORTED_LEAGUES } from '@/lib/constants' import { calculateItemPrices } from '@/lib/item-util' -import { ListingCategory } from '@/lib/listing-categories' +import { ListingCategory, ListingSubCategory } from '@/lib/listing-categories' import { IDisplayedItem } from '@/types/echo-api/priced-item' import { IStashTab } from '@/types/echo-api/stash' +import { ListingMode } from '@/types/sage-listing-type' import { RowSelectionState } from '@tanstack/react-table' -import { produce } from 'immer' import _ from 'lodash-es' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' -import { immer } from 'zustand/middleware/immer' + +export const getCategory = ( + state?: State, + category?: string | null, + subCategory?: string | null +) => { + return (category || state?.category || '') + (subCategory || state?.subCategory || '') +} type State = { league: string // Persisted stashes: Record category: string | null + subCategory: string | null selectableCategories: ListingCategory[] + selectableSubCategories: ListingSubCategory[] localMultiplier: number + categoryListingMode: Record // Persisted categoryMultiplier: Record // Persisted selectedItems: RowSelectionState modifiedItems: IDisplayedItem[] @@ -30,14 +40,17 @@ type Actions = { setLeague: (league: string) => void setStashes: (stashes: IStashTab[]) => void setCategory: (category: string | null) => void + setSubCategory: (category: string | null) => void setSelectableCategories: (categories: ListingCategory[]) => void + setSelectableSubCategories: (categories: ListingSubCategory[]) => void debouncedMultiplier: _.DebouncedFunc< (multiplier: number, selectedCategory: string | null) => void > + setCategoryListingMode: (listingMode: ListingMode, category?: string | null) => void setLocalMultiplier: (multiplier: number, category: string | null) => void setMultiplier: (localMultiplier: number, category: string | null) => void resetData: () => void - setInitialItems: (items: IDisplayedItem[], category: string | null) => void + setInitialItems: (items: IDisplayedItem[]) => void updateData: (rowIndex: number, columnId: string, value: number | string) => void setSelectedItems: React.Dispatch> reset: () => void @@ -45,7 +58,7 @@ type Actions = { const calculateTotalPrice = (item: IDisplayedItem, selectedItems: RowSelectionState) => { if (item.group && selectedItems[item.group.hash]) { - return item.calculatedTotal + return item.calculatedTotalPrice } return 0 } @@ -54,8 +67,11 @@ const initialState: State = { league: SUPPORTED_LEAGUES[0], // Persisted stashes: Object.fromEntries(SUPPORTED_LEAGUES.map((l) => [l, []])), category: null, + subCategory: null, selectableCategories: [], + selectableSubCategories: [], localMultiplier: 100, + categoryListingMode: {}, // Persisted categoryMultiplier: {}, // Persisted selectedItems: {}, modifiedItems: [], @@ -77,7 +93,9 @@ export const useListingToolStore = create()( return { stashes } }), setCategory: (category) => set({ category }), + setSubCategory: (subCategory) => set({ subCategory }), setSelectableCategories: (selectableCategories) => set({ selectableCategories }), + setSelectableSubCategories: (selectableSubCategories) => set({ selectableSubCategories }), resetData: () => { set((state) => { let totalPrice = 0 @@ -104,21 +122,33 @@ export const useListingToolStore = create()( } }) }, - + setCategoryListingMode: (listingMode, category) => + set((state) => { + let categoryListingMode = state.categoryListingMode + if (category) { + categoryListingMode = { ...state.categoryListingMode, [category]: listingMode } + } else if (category === undefined) { + categoryListingMode = { + ...state.categoryListingMode, + [getCategory(state)]: listingMode + } + } + return { categoryListingMode } + }), debouncedMultiplier: _.debounce( (localMultiplier: number, selectedCategory: string | null) => get().setMultiplier(localMultiplier, selectedCategory), 5 ), setLocalMultiplier: (localMultiplier, category) => { - set(() => { - get().debouncedMultiplier(localMultiplier, category) + set((state) => { + state.debouncedMultiplier(localMultiplier, category) return { localMultiplier } }) }, setMultiplier: (localMultiplier, category) => set((state) => { - let categoryMultiplier: Record = state.categoryMultiplier + let categoryMultiplier = state.categoryMultiplier if (category) { console.log('Set categoryMultiplier', category, localMultiplier) categoryMultiplier = { ...state.categoryMultiplier, [category]: localMultiplier } @@ -138,7 +168,7 @@ export const useListingToolStore = create()( categoryMultiplier } }), - setInitialItems: (items, category) => + setInitialItems: (items) => set((state) => { const preSelectedItems: RowSelectionState = {} // Preselect all items @@ -147,10 +177,9 @@ export const useListingToolStore = create()( preSelectedItems[item.group.hash] = true } }) - - const multiplier = category - ? state.categoryMultiplier[category] || state.localMultiplier - : state.localMultiplier + const multiplier = getCategory(state) + ? state.categoryMultiplier[getCategory(state)] || 100 + : 100 let totalPrice = 0 items.forEach((item) => { @@ -232,10 +261,20 @@ export const useListingToolStore = create()( storage: createJSONStorage(() => localStorage), partialize: (state) => ({ league: state.league, + categoryListingMode: state.categoryListingMode, categoryMultiplier: state.categoryMultiplier, overprices: state.overprices, unselectedItems: state.unselectedItems }), + // TODO: On league change => increase version! + version: 1, + migrate: (persistedState: unknown, version: number) => { + const nextState = persistedState as State & Actions + if (nextState && typeof nextState === 'object' && 'league' in nextState) { + nextState.league = SUPPORTED_LEAGUES[0] + } + return nextState + }, onRehydrateStorage: (state) => { console.log('hydration starts') diff --git a/src/green-app/src/app/[locale]/listing-tool/my-offerings-card.tsx b/src/green-app/src/app/[locale]/listing-tool/my-offerings-card.tsx new file mode 100644 index 00000000..4e824018 --- /dev/null +++ b/src/green-app/src/app/[locale]/listing-tool/my-offerings-card.tsx @@ -0,0 +1,310 @@ +import CurrencyDisplay from '@/components/currency-display' +import { currentUserAtom } from '@/components/providers' +import { TimeTracker } from '@/components/time-tracker' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { + SageDatabaseOfferingTypeExt, + deleteListing, + listMyListings, + listStashes +} from '@/lib/http-util' +import { LISTING_CATEGORIES, ListingSubCategory } from '@/lib/listing-categories' +import { IStashTab } from '@/types/echo-api/stash' +import { ListingMode, SageOfferingType } from '@/types/sage-listing-type' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import utc from 'dayjs/plugin/utc' +import { AnimatePresence, motion } from 'framer-motion' +import { useAtomValue } from 'jotai' +import { + ArrowLeftToLineIcon, + CircleUserIcon, + LayoutListIcon, + PackageIcon, + RefreshCwIcon, + Trash2Icon +} from 'lucide-react' +import Image from 'next/image' +import { memo, useEffect, useMemo, useState } from 'react' +import { getCategory } from './listingToolStore' +import { cn } from '@/lib/utils' +import { useTranslation } from 'react-i18next' +dayjs.extend(relativeTime) +dayjs.extend(utc) + +const variants = { + item: { + open: { + y: 0, + opacity: 1, + transition: { + y: { stiffness: 1000, velocity: -100 } + } + }, + closed: { + y: 50, + opacity: 0, + transition: { + y: { stiffness: 1000 } + } + } + } +} + +type MyOfferingsCardProps = { + league: string | null + setCategory: (category: string | null) => void + setSubCategory: (subCategory: string | null) => void + setStashes: (stashes: IStashTab[]) => void + setListingMode: (listingMode: ListingMode, category?: string | null) => void +} + +function MyOfferingsCard({ + league, + setCategory, + setSubCategory, + setStashes, + setListingMode +}: MyOfferingsCardProps) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const currentUser = useAtomValue(currentUserAtom) + const [listings, setListings] = useState() + + const { data: stashes } = useQuery({ + queryKey: [currentUser?.profile?.uuid, 'stashes', league], + queryFn: () => { + if (!league) return [] as IStashTab[] + return listStashes(league) + }, + staleTime: 5 * 60 * 1000, + enabled: !!currentUser?.profile?.uuid && !!league + }) + + const { data: allListings, isFetching } = useQuery({ + queryKey: [currentUser?.profile?.uuid, 'my-listings'], + queryFn: () => + listMyListings().then((res) => + res.filter((l) => { + if (l.deleted) return false + const now = dayjs.utc().valueOf() + return now - l.meta.timestampMs < 30 * 60 * 1000 + }) + ), + enabled: !!currentUser?.profile?.uuid + }) + + useEffect(() => { + let now = dayjs.utc().valueOf() + setListings(allListings?.filter((l) => now - l.meta.timestampMs < 30 * 60 * 1000)) + const interval = setInterval(() => { + now = dayjs.utc().valueOf() + setListings(allListings?.filter((l) => now - l.meta.timestampMs < 30 * 60 * 1000)) + }, 5000) + + return () => clearInterval(interval) + }, [allListings]) + + const shownListings = useMemo( + () => listings?.filter((l) => l.meta.league === league), + [listings, league] + ) + + // Optimistic delete; See: https://tanstack.com/query/v4/docs/framework/react/guides/optimistic-updates + const deleteMutation = useMutation({ + mutationFn: ({ + league, + category, + subCategory, + uuid + }: { + league: string + category: string + subCategory: string + uuid: string + }) => deleteListing(league, category, subCategory, uuid), + onMutate: async (deleted) => { + await queryClient.cancelQueries({ queryKey: [currentUser?.profile?.uuid, 'my-listings'] }) + const previousListings = queryClient.getQueryData([currentUser?.profile?.uuid, 'my-listings']) + queryClient.setQueryData( + [currentUser?.profile?.uuid, 'my-listings'], + (old: SageDatabaseOfferingTypeExt[]) => + old.filter( + (x) => !(x.meta.league === deleted.league && x.meta.category === deleted.category) + ) + ) + return { previousListings } + }, + onError: (err, deletedListing, context) => { + queryClient.setQueryData( + [currentUser?.profile?.uuid, 'my-listings'], + context?.previousListings + ) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: [currentUser?.profile?.uuid, 'my-listings'] }) + } + }) + + return ( +
+

{t('title.myOfferings')}

+
+
+ + {(shownListings?.length || 0) > 0 ? ( + shownListings?.map((listing) => { + const meta = listing.meta + const categoryItem = LISTING_CATEGORIES.find((cat) => cat.name === meta.category) + const selectedCategory = meta.subCategory + ? categoryItem?.subCategories.find((cat) => cat.name === meta.subCategory) + : categoryItem + + return ( + + +
+
+ {selectedCategory && ( + {selectedCategory.name} + )} +
+ {t(`categories.${selectedCategory?.name || ''}` as any)} +
+
+
+ +
+
+
+
+ {meta.listingMode === 'bulk' ? ( + <> + + {t('label.wholeOfferingShort')} + + ) : ( + <> + + {t('label.individualOfferingShort')} + + )} + {/* {meta.tabs.map((tab, i) => ( + {tab} + ))} */} +
+ +
+
+ {/*
*/} +
+ + {meta.ign} + {/* {' - '} */} + {/* {meta.league} */} +
+
+ + + + + + + {t('label.offeringPreviewTT')} +
    +
  • {t('label.offeringPreviewLi1TT')}
  • +
  • {t('label.offeringPreviewLi2TT')}
  • +
  • {t('label.offeringPreviewLi3TT')}
  • +
+
+
+ + {/* + */} +
+
+
+ + + ) + }) + ) : isFetching ? ( +
+ {t('label.loading')} + +
+ ) : ( +
+ {t('label.noOfferingResults')} +
+ )} + +
+
+
+ ) +} + +export default memo(MyOfferingsCard) diff --git a/src/green-app/src/app/listing-tool/page.tsx b/src/green-app/src/app/[locale]/listing-tool/page.tsx similarity index 59% rename from src/green-app/src/app/listing-tool/page.tsx rename to src/green-app/src/app/[locale]/listing-tool/page.tsx index 3c29bac6..3891505e 100644 --- a/src/green-app/src/app/listing-tool/page.tsx +++ b/src/green-app/src/app/[locale]/listing-tool/page.tsx @@ -2,80 +2,112 @@ import CharacterSelect from '@/components/character-select' import DebouncedInput from '@/components/debounced-input' -import { ListingCategorySelect } from '@/components/poe/ListingCategorySelect' +import { ListingCategorySelect } from '@/components/listing-category-select' import { currentUserAtom } from '@/components/providers' import StashSelect from '@/components/stash-select' -import ListingFilterCard, { ListingFilterGroup } from '@/components/trade-filter-card' +import TableColumnToggle from '@/components/table-column-toggle' import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger -} from '@/components/ui/accordion' + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { useDivinePrice } from '@/hooks/useDivinePrice' import { postListing } from '@/lib/http-util' +import { IDisplayedItem } from '@/types/echo-api/priced-item' import { PoeItem } from '@/types/poe-api-models' -import { ListingMode, SageDatabaseOfferingType } from '@/types/sage-listing-type' +import { SageDatabaseOfferingType } from '@/types/sage-listing-type' import { MagnifyingGlassIcon } from '@radix-ui/react-icons' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { FilterFn, filterFns } from '@tanstack/react-table' +import { + ColumnOrderState, + FilterFn, + Table, + VisibilityState, + filterFns +} from '@tanstack/react-table' import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' -import { useAtomValue } from 'jotai' +import { atom, useAtom, useAtomValue } from 'jotai' +import { atomWithStorage } from 'jotai/utils' import { ArrowLeftToLineIcon, ArrowRightToLineIcon } from 'lucide-react' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { v4 as uuidv4 } from 'uuid' import { useShallow } from 'zustand/react/shallow' import { ListingCard } from './listing-card' import ListingToolHandler from './listing-tool-handler' import ListingToolTable from './listing-tool-table' import { listingToolTableEditModeColumns } from './listing-tool-table-columns' -import { useListingToolStore } from './listingToolStore' -import { MyOfferingsCard } from './my-offerings-card' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger -} from '@/components/ui/alert-dialog' +import { getCategory, useListingToolStore } from './listingToolStore' +import MyOfferingsCard from './my-offerings-card' +import { cn } from '@/lib/utils' dayjs.extend(utc) // TODO: -// Save listings to the localstore without items => When they got inactive(> 30min) then they will move over to another subsection in my-offerings-view -// Errorhandling & notification system -// Go through all categories -// Add offerings loading indicator -// Add character combobox loading indicator +// improve errorhandling // Hide "Connect discord" when discord is connected? - Get the current connected discord // Add "Pin all selected items" switch +// Clear cache on logout -export default function PageHydration() { - return ( - // TODO: Add loading indicator - // - - // - ) -} +// feat: translations +// feat: economy page +// feat: minimum value +// feat: notification page +// feat: sound notifications + +// Backend errors: +// Switch back to redis? +// Listing deletion should not delete the listings immediately +// Ratelimiter changes? +// Listing group changes? Forbidden tome; unmodifiable + +const showRightSidePanelAtom = atom(false) -type PageProps = {} +const columnOrderAtom = atomWithStorage('lt-table-columnOrder', []) +const columnVisiblityAtom = atomWithStorage('lt-table-columnVisibility', { + tag: false, + cumulative: false, + '7_day_history': false +}) -function Page() { +export default function Page() { + const { t } = useTranslation() const queryClient = useQueryClient() - const selectedLeague = useListingToolStore((state) => state.league) - const [stashes, setStashes] = useListingToolStore( - useShallow((state) => [state.stashes[state.league], state.setStashes]) - ) - const [selectedCategory, setSelectedCategory] = useListingToolStore( - useShallow((state) => [state.category, state.setCategory]) + + const { + selectedLeague, + selectableCategories, + selectableSubCategories, + selectedCategory, + setSelectedCategory, + selectedSubCategory, + setSelectedSubCategory, + stashes, + setStashes, + selectedListingMode, + setSelectedListingMode + } = useListingToolStore( + useShallow((state) => ({ + selectedLeague: state.league, + selectableCategories: state.selectableCategories, + selectableSubCategories: state.selectableSubCategories, + selectedCategory: state.category, + setSelectedCategory: state.setCategory, + selectedSubCategory: state.subCategory, + setSelectedSubCategory: state.setSubCategory, + stashes: state.stashes[state.league] || [], + setStashes: state.setStashes, + selectedListingMode: state.categoryListingMode[getCategory(state)] || 'bulk', + setSelectedListingMode: state.setCategoryListingMode + })) ) - const selectableCategories = useListingToolStore((state) => state.selectableCategories) const [[refetchAll], setRefetchAll] = useState<(() => void)[]>([]) const [isStashListItemsFetching, setStashListFetching] = useState(false) @@ -84,48 +116,15 @@ function Page() { const mutation = useMutation({ mutationFn: (listing: SageDatabaseOfferingType) => postListing(listing), - onMutate: async (offering) => { - await queryClient.cancelQueries({ queryKey: ['my-listings'] }) - - const previousListings = queryClient.getQueryData(['my-listings']) - - queryClient.setQueryData(['my-listings'], (old?: SageDatabaseOfferingType[]) => { - return [ - ...(old || []).filter( - (x) => - !( - x.meta.league === offering.meta.league && x.meta.category === offering.meta.category - ) - ), - { - ...offering, - meta: { - ...offering.meta, - subCategory: "test", - totalPrice: offering.items.reduce((sum, item) => item.price * item.quantity + sum, 0) - } - } - ].sort((a, b) => b.meta.timestampMs - a.meta.timestampMs) - }) - - return { previousListings } - }, - onError: (err, offering, context) => { - queryClient.setQueryData(['my-listings'], context?.previousListings) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['my-listings'] }) + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: [currentUser?.profile?.uuid, 'my-listings'] }) } }) const currentUser = useAtomValue(currentUserAtom) const [selectedIgn, setSelectedIgn] = useState(null) - const [selectedListingMode, setSelectedListingMode] = useState('bulk') - const [filterGroups, setFilterGroups] = useState([ - { mode: 'AND', filters: [], selected: true } - ]) - const [showFilter, setShowFilter] = useState(false) - const [showRightSidePanel, setShowRightSidePanel] = useState(false) + + const [showRightSidePanel, setShowRightSidePanel] = useAtom(showRightSidePanelAtom) const [globalFilter, setGlobalFilter] = useState('') const resetData = useListingToolStore((state) => state.resetData) @@ -157,7 +156,7 @@ function Page() { meta: { league: selectedLeague, category: selectedCategory, - subCategory: "test", + subCategory: selectedSubCategory || 'ALL', ign: selectedIgn, listingMode: selectedListingMode, timestampMs: dayjs.utc().valueOf(), @@ -178,6 +177,7 @@ function Page() { currentUser?.profile?.uuid, mutation, selectedCategory, + selectedSubCategory, selectedIgn, selectedLeague, selectedListingMode, @@ -199,6 +199,15 @@ function Page() { setRefetchAll([refetchAll]) }, []) + const tableRef = useRef | undefined>() + const [columnVisibility, setColumnVisibility] = useAtom(columnVisiblityAtom) + const [columnOrder, setColumnOrder] = useAtom(columnOrderAtom) + const handleTableReset = useCallback(() => { + tableRef.current?.resetColumnOrder() + tableRef.current?.resetColumnVisibility() + tableRef.current?.resetColumnSizing() + }, []) + return ( <> - {/* */} - {/* */}
-
-
+
+
setGlobalFilter(String(value))} onBlur={(value) => setGlobalFilter(String(value))} className="pl-8 max-w-60" - placeholder={'Search ...'} + placeholder={t('label.searchPh')} startIcon={
@@ -252,34 +260,61 @@ function Page() { />
+
+
+
+ - Are you absolutely sure? - - This action cannot be undone.
- If you confirm, the following data will be reset: + {t('title.alertDialogQuesting')} + + {t('body.softReset')}
    -
  • Multiplier per category
  • -
  • Overrides/Overprices
  • -
  • Unselected items
  • +
  • {t('body.softResetLi1')}
  • +
  • {t('body.softResetLi2')}
  • +
  • {t('body.softResetLi3')}
  • +
  • {t('body.softResetLi4')}
- Cancel - resetData()}>Reset Data + {t('action.cancel')} + resetData()}> + {t('action.softReset')} +
@@ -294,44 +329,31 @@ function Page() { )} - {/* */}
{showRightSidePanel && (
- {!showFilter &&
} +
- {showFilter && ( - - - Item Filter - - - - - - )}
diff --git a/src/green-app/src/app/listings/listing-dialog-content.tsx b/src/green-app/src/app/[locale]/listings/listing-dialog-content.tsx similarity index 75% rename from src/green-app/src/app/listings/listing-dialog-content.tsx rename to src/green-app/src/app/[locale]/listings/listing-dialog-content.tsx index f849a3d3..882abcd0 100644 --- a/src/green-app/src/app/listings/listing-dialog-content.tsx +++ b/src/green-app/src/app/[locale]/listings/listing-dialog-content.tsx @@ -4,39 +4,36 @@ import { currentDivinePriceAtom } from '@/components/providers' import { Button } from '@/components/ui/button' import { DialogClose, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { ScrollArea } from '@/components/ui/scroll-area' -import { useWhisperHashCopied } from '@/hooks/useWhisperHashCopied' +import { useWhisperHashCopied } from '@/hooks/useWhisperHash' +import { cn } from '@/lib/utils' import { createWishperAndCopyToClipboard } from '@/lib/whsiper-util' import { SageListingItemType } from '@/types/sage-listing-type' import { FilterFn, filterFns } from '@tanstack/react-table' import { useAtomValue } from 'jotai' -import { useCallback, useMemo, useState } from 'react' +import { RefreshCwIcon } from 'lucide-react' +import { useCallback, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' -import { ListingFilterGroup } from '../../components/trade-filter-card' import ListingMetaOverview from './listing-meta-overview' import ListingTable from './listing-table' import { listingTableBulkModeColumns, listingTradeSingleModeColumns } from './listing-table-columns' -import { useListingsStore } from './listingsStore' +import { getListingsByCategory, useListingsStore } from './listingsStore' +import { useTranslation } from 'react-i18next' type ListingDialogContentProps = {} export default function ListingDialogContent() { + const { t } = useTranslation() // FilteredListings can only be undefined, if we would remove deleted listings. But for this we have to ensure, that the dialog get closed automatically or something else const selectedListing = useListingsStore( useShallow( - (state) => - state.listingsMap[state.category || ''].find((l) => l.uuid === state.selectedListingId)! + (state) => getListingsByCategory(state).find((l) => l.uuid === state.selectedListingId)! ) ) - const [copyBtnDisabled, messageCopied, messageSent, setMessageCopied] = + const [copyBtnDisabled, isLoading, messageCopied, messageSent, setMessageCopied] = useWhisperHashCopied(selectedListing) const divinePrice = useAtomValue(currentDivinePriceAtom) - // TODO: Implement logic - const [subFilterGroups, setSubFilterGroups] = useState([ - { mode: 'AND', filters: [], selected: true } - ]) - const fuzzyFilter: FilterFn = useCallback( (row, columnId, filterValue, addMeta) => { return filterFns.includesString(row, columnId, filterValue, addMeta) @@ -46,16 +43,16 @@ export default function ListingDialogContent() { const columns = useMemo(() => { if (selectedListing.meta.listingMode === 'bulk') { - return listingTableBulkModeColumns({ listingMode: selectedListing.meta.listingMode }) + return listingTableBulkModeColumns() } else { - return listingTradeSingleModeColumns({ listingMode: selectedListing.meta.listingMode }) + return listingTradeSingleModeColumns() } }, [selectedListing.meta.listingMode]) return ( <> - Bulk Listing + {t('title.bulkListing')} {/* */} @@ -86,8 +83,9 @@ export default function ListingDialogContent() { diff --git a/src/green-app/src/app/listings/listing-meta-overview.tsx b/src/green-app/src/app/[locale]/listings/listing-meta-overview.tsx similarity index 75% rename from src/green-app/src/app/listings/listing-meta-overview.tsx rename to src/green-app/src/app/[locale]/listings/listing-meta-overview.tsx index c22d11e5..34292d5b 100644 --- a/src/green-app/src/app/listings/listing-meta-overview.tsx +++ b/src/green-app/src/app/[locale]/listings/listing-meta-overview.tsx @@ -6,12 +6,14 @@ import { SageListingType } from '@/types/sage-listing-type' import { LayoutListIcon, PackageIcon } from 'lucide-react' import Image from 'next/image' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' type ListingMetaOverviewProps = { selectedListing: SageListingType } export default function ListingMetaOverview({ selectedListing }: ListingMetaOverviewProps) { + const { t } = useTranslation() const multiplier = useMemo(() => { if (selectedListing.meta.multiplier === -1) return '- ' return selectedListing.meta.multiplier.toLocaleString(undefined, { @@ -23,9 +25,9 @@ export default function ListingMetaOverview({ selectedListing }: ListingMetaOver return ( <>
- +
{selectedListing.meta.ign}
- +
{selectedListing.meta.altIcon} -
{selectedListing.meta.category}
+
{t(`categories.${selectedListing.meta.category}` as any)}
- +
{selectedListing.meta.listingMode === 'bulk' ? ( <> - {'Whole Offering '} + {t('label.wholeListingShort')} ) : ( <> - {'Individual Priced Items '} + {t('label.individualListingShort')} )}
- +
- +
{multiplier}%
diff --git a/src/green-app/src/app/[locale]/listings/listing-table-columns.tsx b/src/green-app/src/app/[locale]/listings/listing-table-columns.tsx new file mode 100644 index 00000000..26051759 --- /dev/null +++ b/src/green-app/src/app/[locale]/listings/listing-table-columns.tsx @@ -0,0 +1,80 @@ +'use client' + +import { checkColumn } from '@/components/table-columns/check-column' +import { historyColumn } from '@/components/table-columns/history-column' +import { multiplierColumn } from '@/components/table-columns/multiplier-column' +import { nameColumn } from '@/components/table-columns/name-column' +import { priceColumn } from '@/components/table-columns/price-column' +import { propsColumn } from '@/components/table-columns/props-column' +import { quantityColumn } from '@/components/table-columns/quantity-column' +import { quantityInputColumn } from '@/components/table-columns/quantity-input-column' +import { SageListingItemType } from '@/types/sage-listing-type' +import { ColumnDef } from '@tanstack/react-table' + +export const listingTableBulkModeColumns = (): ColumnDef[] => [ + nameColumn(), + propsColumn(), + quantityColumn(), + historyColumn({ mode: '2 days', animation: false }), + historyColumn({ mode: '7 days', animation: false }), + priceColumn({ + accessorKey: 'price', + accessorFn: (item) => item.price, + headerName: 'price' + }), + priceColumn({ + accessorKey: 'calculatedTotal', + accessorFn: (item) => item.price * item.selectedQuantity, + headerName: 'totalPrice' + }), + multiplierColumn({ + accessorFn: (item) => { + if (item.primaryValuation === 0 || item.price === 0) return '- ' + return ((item.price / item.primaryValuation) * 100).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }) + } + }), + priceColumn({ + accessorKey: 'cumulative', + headerName: 'commulativePrice', + cumulativeColumn: 'calculatedTotal', + enableSorting: false + }) +] + +export const listingTradeSingleModeColumns = (): ColumnDef[] => [ + checkColumn(), + nameColumn(), + propsColumn(), + quantityColumn(), + historyColumn({ mode: '2 days', animation: false }), + historyColumn({ mode: '7 days', animation: false }), + priceColumn({ + accessorKey: 'price', + accessorFn: (item) => item.price, + headerName: 'price' + }), + priceColumn({ + accessorKey: 'calculatedTotal', + accessorFn: (item) => item.price * item.selectedQuantity, + headerName: 'totalPrice' + }), + multiplierColumn({ + accessorFn: (item) => { + if (item.primaryValuation === 0 || item.price === 0) return '- ' + return ((item.price / item.primaryValuation) * 100).toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }) + } + }), + quantityInputColumn(), + priceColumn({ + accessorKey: 'cumulative', + headerName: 'commulativePrice', + cumulativeColumn: 'calculatedTotal', + enableSorting: false + }) +] diff --git a/src/green-app/src/app/[locale]/listings/listing-table.tsx b/src/green-app/src/app/[locale]/listings/listing-table.tsx new file mode 100644 index 00000000..f99608d3 --- /dev/null +++ b/src/green-app/src/app/[locale]/listings/listing-table.tsx @@ -0,0 +1,202 @@ +/* eslint-disable no-extra-boolean-cast */ +'use client' + +import { BasicSelect } from '@/components/basic-select' +import DataTable, { DataTableOptions } from '@/components/data-table/data-table' +import DebouncedInput from '@/components/debounced-input' +import TableColumnToggle from '@/components/table-column-toggle' +import { useSkipper } from '@/hooks/useSkipper' +import { SageListingItemType, SageListingType } from '@/types/sage-listing-type' +import { MagnifyingGlassIcon } from '@radix-ui/react-icons' +import { + ColumnDef, + ColumnOrderState, + ColumnSizingState, + FilterFnOption, + Table, + VisibilityState +} from '@tanstack/react-table' +import { atom, useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' +import { getListingsByCategory, useListingsStore } from './listingsStore' +import { useTranslation } from 'react-i18next' + +interface DataTableProps { + columns: ColumnDef[] + className?: string + globalFilterFn?: FilterFnOption +} + +type ShowItemMode = 'showAll' | 'showSelected' | 'showUnselected' +const options: ShowItemMode[] = ['showAll', 'showSelected', 'showUnselected'] +const showItemsModeAtom = atom('showAll') + +const columnOrderAtom = atomWithStorage('l-table-columnOrder', []) +const columnVisiblityAtom = atomWithStorage('l-table-columnVisibility', { + cumulative: false, + '7_day_history': false +}) +const columnSizingAtom = atomWithStorage('l-table-columnSizing', {}) + +// Tutorial: https://ui.shadcn.com/docs/components/data-table +const ListingTable = ({ columns, className, globalFilterFn }: DataTableProps) => { + const { t } = useTranslation() + const [globalFilter, setGlobalFilter] = useState('') + const [showItemsMode, setShowItemsMode] = useAtom(showItemsModeAtom) + + const listing = useListingsStore( + useShallow((state) => { + return getListingsByCategory(state)?.find((l) => l.uuid === state.selectedListingId) + }) + ) + + const selectedItems = useListingsStore( + useShallow((state) => + state.selectedListingId ? state.selectedItemsMap[state.selectedListingId] : {} + ) + ) + + const filteredItems = useMemo((): SageListingItemType[] => { + if (!listing) return [] + if (listing.meta.listingMode === 'bulk' || showItemsMode === 'showAll') return listing.items + return listing.items.filter((item) => { + const selected = !!selectedItems[item.hash] + if (showItemsMode === 'showSelected') return selected + return !selected + }) + }, [listing, showItemsMode, selectedItems]) + + const setSelectedItems = useListingsStore((state) => state.setSelectedItems) + const updateData = useListingsStore((state) => state.updateData) + + const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper() + + const tableRef = useRef | undefined>() + const [columnVisibility, setColumnVisibility] = useAtom(columnVisiblityAtom) + const [columnOrder, setColumnOrder] = useAtom(columnOrderAtom) + const [columnSizing, setColumnSizing] = useAtom(columnSizingAtom) + const handleTableReset = useCallback(() => { + tableRef.current?.resetColumnOrder() + tableRef.current?.resetColumnVisibility() + tableRef.current?.resetColumnSizing() + }, []) + + const [localColumnSizing, setLocalColumnSizing] = useState(columnSizing) + + useEffect(() => { + const timeout = setTimeout(() => { + setColumnSizing(localColumnSizing) + }, 250) + return () => { + clearTimeout(timeout) + } + }, [localColumnSizing, setColumnSizing]) + + const tableOptions = useMemo((): DataTableOptions => { + return { + data: filteredItems, + columns, + getRowId: (row) => row.hash, + enableMultiSort: true, + autoResetPageIndex, + onGlobalFilterChange: setGlobalFilter, + onRowSelectionChange: setSelectedItems, + globalFilterFn: globalFilterFn, + onColumnVisibilityChange: setColumnVisibility, + onColumnOrderChange: setColumnOrder, + onColumnSizingChange: setLocalColumnSizing, + state: { + globalFilter: globalFilter, + rowSelection: selectedItems, + columnVisibility: columnVisibility, + columnOrder: columnOrder, + columnSizing: localColumnSizing + }, + initialState: { + pagination: { + pageSize: 10 + }, + sorting: [ + columnVisibility['2_day_history'] ?? true + ? { + desc: true, + id: '2_day_history' + } + : { + desc: true, + id: '7_day_history' + } + ], + columnVisibility: { + cumulative: false, + '7_day_history': false + } + }, + meta: { + // https://muhimasri.com/blogs/react-editable-table/ + updateData: (...params) => { + skipAutoResetPageIndex() + updateData(...params) + } + } + } + }, [ + filteredItems, + columns, + autoResetPageIndex, + setSelectedItems, + globalFilterFn, + setColumnVisibility, + setColumnOrder, + globalFilter, + selectedItems, + columnVisibility, + columnOrder, + localColumnSizing, + skipAutoResetPageIndex, + updateData + ]) + + return ( +
+
+ setGlobalFilter(String(value))} + onBlur={(value) => setGlobalFilter(String(value))} + className="pl-8 max-w-60" + placeholder={t('label.searchPh')} + startIcon={ +
+ +
+ } + /> + {listing?.meta.listingMode === 'single' && ( +
+ +
+ )} +
+ +
+ +
+ ) +} + +export default memo(ListingTable) diff --git a/src/green-app/src/app/listings/listings-handler.tsx b/src/green-app/src/app/[locale]/listings/listings-handler.tsx similarity index 60% rename from src/green-app/src/app/listings/listings-handler.tsx rename to src/green-app/src/app/[locale]/listings/listings-handler.tsx index c3dcdbdf..125e10ea 100644 --- a/src/green-app/src/app/listings/listings-handler.tsx +++ b/src/green-app/src/app/[locale]/listings/listings-handler.tsx @@ -1,14 +1,14 @@ 'use client' -import { DEFAULT_VALUATION_INDEX } from '@/lib/constants' import { listListings, listSummaries, listValuations } from '@/lib/http-util' import { LISTING_CATEGORIES } from '@/lib/listing-categories' +import { calculateListingFromOfferingListing } from '@/lib/listing-util' import { SageItemGroupSummaryShard } from '@/types/echo-api/item-group' import { SageValuationShard } from '@/types/echo-api/valuation' import { SageListingType } from '@/types/sage-listing-type' import { useQueries, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' -import { memo, useEffect, useMemo, useRef } from 'react' +import { memo, useEffect, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' import { useListingsStore } from './listingsStore' @@ -17,58 +17,36 @@ interface ListingsHandlerProps {} // Tutorial: https://ui.shadcn.com/docs/components/data-table const ListingsHandler = () => { const league = useListingsStore((state) => state.league) - const categoryTagItem = useListingsStore( + const categoryItem = useListingsStore( useShallow((state) => LISTING_CATEGORIES.find((ca) => ca.name === state.category)) ) // Starts with 0 const fetchTimeStamp = useListingsStore( - (state) => state.fetchTimeStamps[state.league][state.category || ''] + (state) => state.fetchTimeStamps[state.league]?.[state.category || ''] ) const setFetchTimestamp = useListingsStore((state) => state.setFetchTimestamps) const addListings = useListingsStore((state) => state.addListings) const cleanupListings = useListingsStore((state) => state.cleanupListings) - const { data: listings, isError } = useQuery({ + const { data: listings } = useQuery({ // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: ['listings', league, categoryTagItem?.name || '', fetchTimeStamp], + queryKey: ['listings', league, categoryItem?.name || ''], queryFn: async () => { - const listingsRes = await listListings(league, categoryTagItem!.name, fetchTimeStamp) - const listings = Object.entries(listingsRes).map( - (e): SageListingType => ({ - userId: e[0], - ...(e[1] as any) - }) - ) + const listings = await listListings(league, categoryItem!.name, fetchTimeStamp) + const nextMs = dayjs.utc().valueOf() + setFetchTimestamp(nextMs - 2000) return listings }, // We do not save any cache - this has the effect, that the query starts directly after changing the category gcTime: 0, - enabled: !!categoryTagItem, - refetchOnWindowFocus: false, + enabled: !!categoryItem, + refetchInterval: 2000, retry: true }) - const errorRef = useRef(isError) - errorRef.current = isError - - useEffect(() => { - const interval = setInterval(() => { - if (errorRef.current) { - // We do not start the next request until the first is finished - console.warn('Skip request for next timestamp') - return - } - const nextMs = dayjs.utc().valueOf() - setFetchTimestamp(nextMs - 2000) // Request the data 2 sec ago - }, 2000) - - return () => clearInterval(interval) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [categoryTagItem?.name]) - const { summaries, isSummaryPending, isSummaryFetching, isSummaryError } = useQueries({ - queries: categoryTagItem - ? categoryTagItem.tags.map((tag) => { + queries: categoryItem + ? categoryItem.tags.map((tag) => { return { queryKey: ['summaries', tag], queryFn: () => listSummaries(tag), @@ -108,10 +86,10 @@ const ListingsHandler = () => { const { valuations, isValuationPending, isValuationFetching, isValuationError } = useQueries({ queries: - leagues.length > 0 && categoryTagItem + leagues.length > 0 && categoryItem ? leagues .map((league) => - categoryTagItem.tags.map((tag) => { + categoryItem.tags.map((tag) => { return { queryKey: ['valuations', league, tag], queryFn: () => listValuations(league, tag), @@ -122,17 +100,16 @@ const ListingsHandler = () => { } }) ) - .flatMap((x) => x) + .flat() : [], combine: (valuationResults) => { const valuationShards = valuationResults .filter((x) => x.data && !x.isPending) .map((x) => x.data!) - let valuations: SageValuationShard['valuations'] = {} + const valuations: Record = {} valuationShards.forEach((e) => { - // TODO: Distinct between leagues - valuations = { ...valuations, ...e.valuations } + valuations[e.meta.league] = { ...valuations[e.meta.league], ...e.valuations } }) const isValuationError = valuationResults.some((result) => result.isError) @@ -148,55 +125,37 @@ const ListingsHandler = () => { } }) - const startCalculation = !!categoryTagItem && valuations !== undefined && summaries !== undefined + const startCalculation = !!categoryItem && valuations !== undefined && summaries !== undefined useEffect(() => { if (listings && listings.length > 0 && startCalculation) { - const nextListings = listings.map((listing: SageListingType) => { - listing.items.forEach((e) => { - e.valuation = valuations[e.hash] - e.primaryValuation = e.valuation?.pValues?.[DEFAULT_VALUATION_INDEX] ?? 0 - e.calculatedTotalPrice = e.quantity * e.price - e.summary = summaries[e.hash] - e.selectedQuantity = e.quantity - e.displayName = summaries[e.hash]?.displayName - if (e.primaryValuation <= 0) { - console.log('NOT FOUND', e) - } - e.icon = summaries[e.hash]?.icon - }) - listing.meta.calculatedTotalPrice = listing.items.reduce( - (a, b) => a + b.calculatedTotalPrice, - 0 - ) - listing.meta.calculatedTotalValuation = listing.items.reduce( - (a, b) => a + b.primaryValuation * b.quantity, - 0 - ) - listing.meta.multiplier = - (listing.meta.calculatedTotalPrice / listing.meta.calculatedTotalValuation) * 100 - listing.meta.icon = categoryTagItem!.icon - listing.meta.altIcon = '' - return listing - }) + const nextListings = listings.map((listing) => + calculateListingFromOfferingListing(listing, summaries, valuations[listing.meta.league]) + ) // One user can have one category per league active. We delete or replace this const categories: Record = {} nextListings.forEach((l) => { - if (categories[l.meta.category]) { - categories[l.meta.category].push(l) + const categoryKey = l.meta.category + (!l.meta.subCategory ? '' : `_${l.meta.subCategory}`) + if (categories[categoryKey]) { + categories[categoryKey].push(l) } else { - categories[l.meta.category] = [l] + categories[categoryKey] = [l] } }) - Object.entries(categories).forEach(([category, listings]) => { - addListings(listings, category) + Object.entries(categories).forEach(([categoryKey, listings]) => { + const [category, subCategory] = categoryKey.split('_') + addListings(listings, category, subCategory) }) - cleanupListings() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [startCalculation, listings]) + useEffect(() => { + const interval = setInterval(cleanupListings, 2000) + return () => clearInterval(interval) + }, [cleanupListings]) + return null } diff --git a/src/green-app/src/app/listings/listings-table-columns.tsx b/src/green-app/src/app/[locale]/listings/listings-table-columns.tsx similarity index 70% rename from src/green-app/src/app/listings/listings-table-columns.tsx rename to src/green-app/src/app/[locale]/listings/listings-table-columns.tsx index a4d5a728..fe90da8c 100644 --- a/src/green-app/src/app/listings/listings-table-columns.tsx +++ b/src/green-app/src/app/[locale]/listings/listings-table-columns.tsx @@ -1,36 +1,43 @@ /* eslint-disable react-hooks/rules-of-hooks */ 'use client' -import CurrencyDisplay from '@/components/currency-display' import { currentDivinePriceAtom } from '@/components/providers' +import { multiplierColumn } from '@/components/table-columns/multiplier-column' +import { priceColumn } from '@/components/table-columns/price-column' +import { TimeTracker } from '@/components/time-tracker' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { useWhisperHashCopied } from '@/hooks/useWhisperHashCopied' -import { CurrencySwitch } from '@/lib/currency' +import { useWhisperHashCopied } from '@/hooks/useWhisperHash' +import { cn } from '@/lib/utils' import { createWishperAndCopyToClipboard } from '@/lib/whsiper-util' import { ListingMode, SageListingType } from '@/types/sage-listing-type' import { ColumnDef } from '@tanstack/react-table' import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' import utc from 'dayjs/plugin/utc' import { useAtomValue } from 'jotai' -import { ArrowUpRightFromSquareIcon, LayoutListIcon, PackageIcon } from 'lucide-react' +import { + ArrowUpRightFromSquareIcon, + LayoutListIcon, + PackageIcon, + RefreshCwIcon +} from 'lucide-react' import Image from 'next/image' import { useState } from 'react' -import { TableColumnHeader } from '../../components/column-header' +import { TableColumnHeader } from '../../../components/column-header' import { useListingsStore } from './listingsStore' -dayjs.extend(relativeTime) +import { useTranslation } from 'react-i18next' dayjs.extend(utc) export function categoryColumn(): ColumnDef { const key = 'category' - const header = 'Category' + const header = 'category' return { header: ({ column }) => , + id: key, accessorKey: key, accessorFn: (listing) => { - return listing.meta.category + return listing.meta.subCategory || listing.meta.category }, enableSorting: true, enableGlobalFilter: true, @@ -38,16 +45,25 @@ export function categoryColumn(): ColumnDef { headerWording: header }, cell: ({ row }) => { + const { t } = useTranslation() const value = row.getValue(key) return ( -
+
{row.original.meta.altIcon} - {value} + {t(`categories.${value}` as any)}
) } @@ -56,10 +72,11 @@ export function categoryColumn(): ColumnDef { export function listingModeColumn(): ColumnDef { const key = 'listingMode' - const header = 'Sell Mode' + const header = 'listingMode' return { header: ({ column }) => , + id: key, accessorKey: key, accessorFn: (listing) => { return listing.meta.listingMode @@ -70,6 +87,7 @@ export function listingModeColumn(): ColumnDef { headerWording: header }, cell: ({ row }) => { + const { t } = useTranslation() const mode = row.getValue(key) return (
@@ -78,7 +96,9 @@ export function listingModeColumn(): ColumnDef { ) : ( )} - {mode === 'bulk' ? 'Whole Listing' : 'Individual'} + + {mode === 'bulk' ? t('label.wholeListingShort') : t('label.individualListingShort')} +
) } @@ -87,10 +107,11 @@ export function listingModeColumn(): ColumnDef { export function sellerColumn(): ColumnDef { const key = 'seller' - const header = 'Seller' + const header = 'seller' return { header: ({ column }) => , + id: key, accessorKey: key, accessorFn: (listing) => { return listing.meta.ign @@ -107,85 +128,13 @@ export function sellerColumn(): ColumnDef { } } -export function totalPriceColumn(): ColumnDef { - const key = 'totalPrice' - const header = 'Price' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (listing) => { - return listing.meta.calculatedTotalPrice - }, - enableResizing: false, - enableSorting: true, - enableGlobalFilter: false, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -type ItemValueCellProps = { - value: number | string - editable?: boolean - showChange?: boolean - toCurrency?: CurrencySwitch -} - -const ItemValueCell = ({ value, showChange, toCurrency }: ItemValueCellProps) => { - return ( -
- {typeof value === 'number' ? ( - - ) : ( - value - )} -
- ) -} - -export function multiplierColumn(): ColumnDef { - const key = 'multiplier' - const header = 'Multiplier' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (listing) => { - if (listing.meta.multiplier === -1) return '- ' - return listing.meta.multiplier.toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 2 - }) - }, - enableSorting: true, - enableGlobalFilter: false, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return
{value}%
- } - } -} - export function createdColumn(): ColumnDef { const key = 'created' - const header = 'Created' + const header = 'created' return { header: ({ column }) => , + id: key, accessorKey: key, accessorFn: (listing) => { return Math.trunc(listing.meta.timestampMs / 10000) * 10000 @@ -197,17 +146,18 @@ export function createdColumn(): ColumnDef { }, cell: ({ row }) => { const value = row.getValue(key) - return
{dayjs.utc(value).fromNow()}
+ return } } } export function actionsColumn(): ColumnDef { const key = 'actions' - const header = 'Actions' + const header = 'actions' return { header: ({ column }) => , + id: key, accessorKey: key, enableSorting: false, enableGlobalFilter: false, @@ -215,12 +165,12 @@ export function actionsColumn(): ColumnDef { headerWording: header }, cell: ({ row }) => { + const { t } = useTranslation() const divinePrice = useAtomValue(currentDivinePriceAtom) const [detailsTooltipOpen, setDetailsTooltipOpen] = useState(false) const setSelectedListingId = useListingsStore((state) => state.setSelectedListingId) - const [copyBtnDisabled, messageCopied, messageSent, setMessageCopied] = useWhisperHashCopied( - row.original - ) + const [copyBtnDisabled, isLoading, messageCopied, messageSent, setMessageCopied] = + useWhisperHashCopied(row.original) return ( @@ -240,13 +190,14 @@ export function actionsColumn(): ColumnDef { {/* */} - {'Show Details'} + {t('label.showDetailsTT')} {/* */} {/* {messageCopied ? 'Whisper copied' : 'Copy whisper'} @@ -299,8 +257,20 @@ export const listingsTableColumns = (): ColumnDef[] => [ categoryColumn(), listingModeColumn(), sellerColumn(), - totalPriceColumn(), - multiplierColumn(), + priceColumn({ + accessorKey: 'totalPrice', + headerName: 'price', + accessorFn: (listing) => listing.meta.calculatedTotalPrice + }), + multiplierColumn({ + accessorFn: (listing) => { + if (listing.meta.multiplier === -1) return '- ' + return listing.meta.multiplier.toLocaleString(undefined, { + minimumFractionDigits: 0, + maximumFractionDigits: 2 + }) + } + }), createdColumn(), actionsColumn() ] diff --git a/src/green-app/src/app/[locale]/listings/listings-table.tsx b/src/green-app/src/app/[locale]/listings/listings-table.tsx new file mode 100644 index 00000000..dcba4409 --- /dev/null +++ b/src/green-app/src/app/[locale]/listings/listings-table.tsx @@ -0,0 +1,219 @@ +/* eslint-disable no-extra-boolean-cast */ +'use client' + +import { BasicSelect } from '@/components/basic-select' +import DataTable, { DataTableOptions } from '@/components/data-table/data-table' +import DebouncedInput from '@/components/debounced-input' +import TableColumnToggle from '@/components/table-column-toggle' +import { Dialog, DialogContent } from '@/components/ui/dialog' +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' +import { SageListingType } from '@/types/sage-listing-type' +import { DialogPortal } from '@radix-ui/react-dialog' +import { MagnifyingGlassIcon } from '@radix-ui/react-icons' +import * as SliderPrimitive from '@radix-ui/react-slider' +import { + ColumnDef, + ColumnOrderState, + ColumnSizingState, + FilterFnOption, + Table, + VisibilityState +} from '@tanstack/react-table' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import { atom, useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useShallow } from 'zustand/react/shallow' +import ListingDialogContent from './listing-dialog-content' +import { getCategory, getListingsByCategory, useListingsStore } from './listingsStore' +import { useTranslation } from 'react-i18next' +dayjs.extend(utc) + +type SellModeOptions = 'showAllModes' | 'showWholeListings' | 'showIndividualListings' +const options: SellModeOptions[] = ['showAllModes', 'showWholeListings', 'showIndividualListings'] +const showSellModeAtom = atom('showAllModes') + +const columnOrderAtom = atomWithStorage('ls-table-columnOrder', []) +const columnVisiblityAtom = atomWithStorage('ls-table-columnVisibility', {}) +const columnSizingAtom = atomWithStorage('ls-table-columnSizing', {}) + +interface DataTableProps { + className?: string + columns: ColumnDef[] + globalFilterFn?: FilterFnOption +} + +// Tutorial: https://ui.shadcn.com/docs/components/data-table +const ListingsTable = ({ columns, globalFilterFn, className }: DataTableProps) => { + const { t } = useTranslation() + const [showSellMode, setShowSellMode] = useAtom(showSellModeAtom) + const [dialogOpen, setDialogOpen] = useListingsStore( + useShallow((state) => [state.dialogOpen, state.setDialogOpen]) + ) + const [multiplierRange, setMultiplierRange] = useListingsStore( + useShallow((state) => [state.multiplierRange, state.setMultiplierRange]) + ) + + const [globalFilter, setGlobalFilter] = useState('') + const modifiedListings = useListingsStore( + useShallow((state) => { + if (!getCategory(state)) return [] + const now = dayjs.utc().valueOf() + return getListingsByCategory(state).filter( + (l) => + l.meta.league === state.league && + state.filteredByGroupListings[l.uuid] && + now - l.meta.timestampMs < 30 * 60 * 1000 && + state.multiplierRange[0] <= l.meta.multiplier && + l.meta.multiplier <= state.multiplierRange[1] + ) + }) + ) + + const filteredListings = useMemo((): SageListingType[] => { + if (showSellMode === 'showAllModes') return modifiedListings + return modifiedListings.filter((l) => { + if (showSellMode === 'showWholeListings') return l.meta.listingMode === 'bulk' + else return l.meta.listingMode === 'single' + }) + }, [modifiedListings, showSellMode]) + + const tableRef = useRef | undefined>() + const [columnVisibility, setColumnVisibility] = useAtom(columnVisiblityAtom) + const [columnOrder, setColumnOrder] = useAtom(columnOrderAtom) + const [columnSizing, setColumnSizing] = useAtom(columnSizingAtom) + const handleTableReset = useCallback(() => { + tableRef.current?.resetColumnOrder() + tableRef.current?.resetColumnVisibility() + tableRef.current?.resetColumnSizing() + }, []) + + const [localColumnSizing, setLocalColumnSizing] = useState(columnSizing) + + useEffect(() => { + const timeout = setTimeout(() => { + setColumnSizing(localColumnSizing) + }, 250) + return () => { + clearTimeout(timeout) + } + }, [localColumnSizing, setColumnSizing]) + + const tableOptions = useMemo((): DataTableOptions => { + return { + data: filteredListings, + columns, + getRowId: (row) => row.uuid, + enableMultiSort: true, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: globalFilterFn, + onColumnVisibilityChange: setColumnVisibility, + onColumnOrderChange: setColumnOrder, + onColumnSizingChange: setLocalColumnSizing, + state: { + globalFilter: globalFilter, + columnVisibility: columnVisibility, + columnOrder: columnOrder, + columnSizing: localColumnSizing + }, + initialState: { + pagination: { + pageSize: 25 + }, + sorting: [ + { + desc: true, + id: 'created' + }, + { + desc: false, + id: 'multiplier' + } + ] + } + } + }, [ + columnOrder, + columnVisibility, + columns, + filteredListings, + globalFilter, + globalFilterFn, + localColumnSizing, + setColumnOrder, + setColumnVisibility + ]) + + return ( +
+ +
+ setGlobalFilter(String(value))} + onBlur={(value) => setGlobalFilter(String(value))} + className="pl-8 max-w-48" + placeholder={t('label.searchPh')} + startIcon={ +
+ +
+ } + /> +
+ +
+
+ + setMultiplierRange(e)} + > + + + + + + +
+
+ +
+ + + e.preventDefault()} + > + + + +
+
+ ) +} + +export default memo(ListingsTable) diff --git a/src/green-app/src/app/listings/listingsStore.ts b/src/green-app/src/app/[locale]/listings/listingsStore.ts similarity index 72% rename from src/green-app/src/app/listings/listingsStore.ts rename to src/green-app/src/app/[locale]/listings/listingsStore.ts index 50ff9d5c..5ec14306 100644 --- a/src/green-app/src/app/listings/listingsStore.ts +++ b/src/green-app/src/app/[locale]/listings/listingsStore.ts @@ -3,6 +3,7 @@ import { ListingFilterGroup } from '@/components/trade-filter-card' import { SUPPORTED_LEAGUES } from '@/lib/constants' import { ListingFilterUtil } from '@/lib/listing-filter-util' +import { useNotificationStore } from '@/store/notificationStore' import { SageListingType } from '@/types/sage-listing-type' import { RowSelectionState } from '@tanstack/react-table' import dayjs from 'dayjs' @@ -10,7 +11,6 @@ import utc from 'dayjs/plugin/utc' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' import { immer } from 'zustand/middleware/immer' -import { toast } from 'react-toastify' dayjs.extend(utc) const calculateMetaPrices = ( @@ -46,6 +46,25 @@ const calculateMetaPrices = ( listing.meta.multiplier = multiplier } +export const getCategory = ( + state: State, + category?: string | null, + subCategory?: string | null +) => { + return (category || state.category || '') + (subCategory || state.subCategory || '') +} + +export const getListingsByCategory = (state: State) => { + const categoryKey = getCategory(state) + if (state.subCategory) { + return state.listingsMap[categoryKey] + } + // Only main category selected -> Request category + subCategories + return Object.entries(state.listingsMap) + .filter(([category]) => category.startsWith(categoryKey)) + .flatMap(([_, listings]) => listings) +} + const calculateListings = ( listings: SageListingType[], selectedItemsMap: Record, @@ -92,7 +111,8 @@ const calculateListings = ( type State = { dialogOpen: boolean league: string // Persist - category: string | null // Persist + category: string | null + subCategory: string | null multiplierRange: number[] // Persist /** * The timestamps to start from for the indivitual categories @@ -115,6 +135,7 @@ type Actions = { setDialogOpen: (open: boolean) => void setLeague: (league: string) => void setCategory: (category: string | null) => void + setSubCategory: (subCategory: string | null) => void setMultiplierRange: (range: number[]) => void setFetchTimestamps: (timestamp: number) => void /** @@ -124,7 +145,7 @@ type Actions = { /** * Calculates the listings totalValue, selected items, selected total and adds this to the pool of listings & then filters them */ - addListings: (listings: SageListingType[], category: string) => void + addListings: (listings: SageListingType[], category: string, subCategory?: string) => void cleanupListings: () => void addWhisperedListing: (id: string, hash: string) => void setSelectedListingId: (id: string) => void @@ -138,6 +159,7 @@ const initialState: State = { dialogOpen: false as boolean, league: SUPPORTED_LEAGUES[0], category: null as string | null, + subCategory: null as string | null, multiplierRange: [0, 200], fetchTimeStamps: Object.fromEntries(SUPPORTED_LEAGUES.map((league) => [league, {}])), filterGroups: [{ selected: true, mode: 'AND', filters: [] }], @@ -156,7 +178,7 @@ export const useListingsStore = create()( setDialogOpen: (open) => set((state) => { if (!open) { - const listing = state.listingsMap[state.category || ''].find( + const listing = getListingsByCategory(state).find( (l) => l.uuid === state.selectedListingId ) if (listing) { @@ -176,29 +198,61 @@ export const useListingsStore = create()( setCategory: (category) => set((state) => { + if (state.category !== category) { + state.subCategory = null + } state.category = category // Set initial key to listingsmap - if (!state.listingsMap[category || '']) { - state.listingsMap[category || ''] = [] + const categoryKey = getCategory(state) + if (!state.listingsMap[categoryKey]) { + state.listingsMap[categoryKey] = [] } // Set initial timestamp - if (category && state.fetchTimeStamps[state.league][category] === undefined) { - state.fetchTimeStamps[state.league][category] = 0 - } + SUPPORTED_LEAGUES.forEach((league) => { + if (state.category && state.fetchTimeStamps[league][state.category] === undefined) { + state.fetchTimeStamps[league][state.category] = 0 + } + }) // Reset filterGroups state.filterGroups = [{ selected: true, mode: 'AND', filters: [] }] state.filteredByGroupListings = {} calculateListings( - state.listingsMap[state.category || ''], + getListingsByCategory(state), state.selectedItemsMap, state.filterGroups, state.filteredByGroupListings ) }), + setSubCategory: (subCategory) => + set((state) => { + state.subCategory = subCategory + // Set initial key to listingsmap + const categoryKey = getCategory(state) + if (!state.listingsMap[categoryKey]) { + state.listingsMap[categoryKey] = [] + } + + // Set initial timestamp + SUPPORTED_LEAGUES.forEach((league) => { + if (state.category && state.fetchTimeStamps[league][state.category] === undefined) { + state.fetchTimeStamps[league][state.category] = 0 + } + }) + + // Reset filterGroups + state.filterGroups = [{ selected: true, mode: 'AND', filters: [] }] + state.filteredByGroupListings = {} + calculateListings( + getListingsByCategory(state), + state.selectedItemsMap, + state.filterGroups, + state.filteredByGroupListings + ) + }), setMultiplierRange: (range) => set((state) => { state.multiplierRange = range @@ -211,39 +265,38 @@ export const useListingsStore = create()( setFilterGroups: (filterGroups) => set((state) => { - if (!state.category) return - state.filterGroups = filterGroups + if (!getCategory(state)) return state.filteredByGroupListings = {} calculateListings( - state.listingsMap[state.category || ''], + getListingsByCategory(state), state.selectedItemsMap, state.filterGroups, state.filteredByGroupListings ) }), - addListings: (listings, category) => + addListings: (listings, category, subCategory) => set((state) => { - if (!state.listingsMap[category]) { - state.listingsMap[category] = [] + const categoryKey = getCategory(state, category, subCategory) + if (!state.listingsMap[categoryKey]) { + state.listingsMap[categoryKey] = [] } // Add the new listings to the listingsMap const newListings = listings.filter((l) => { - // Unique key: userId:league:category - const idx = state.listingsMap[category || ''].findIndex( - (cl) => - cl.userId === l.userId && - cl.meta.league === l.meta.league && - cl.meta.category === l.meta.category + // Unique key: userId:league:category:subcategory + const idx = state.listingsMap[categoryKey].findIndex( + (cl) => cl.userId === l.userId && cl.meta.league === l.meta.league ) if (idx !== -1) { if (l.deleted) { if (state.dialogOpen && state.selectedListingId === l.uuid) { state.dialogOpen = false console.warn('The dialog was closed because the opened listing was deleted') - toast.warning('Sorry! The listing has been deleted!') + useNotificationStore + .getState() + .addNotification('Sorry! The listing has been deleted!', 'warning') } if (state.selectedItemsMap[l.uuid]) { delete state.selectedItemsMap[l.uuid] @@ -251,14 +304,16 @@ export const useListingsStore = create()( if (state.filteredByGroupListings[l.uuid]) { delete state.filteredByGroupListings[l.uuid] } - state.listingsMap[category || ''].splice(idx, 1) + state.listingsMap[categoryKey].splice(idx, 1) } else { - const prev = state.listingsMap[category || ''][idx] + const prev = state.listingsMap[categoryKey][idx] if (prev.uuid !== l.uuid) { if (state.dialogOpen && state.selectedListingId === prev.uuid) { state.selectedListingId = l.uuid console.warn('The dialog was updated because the opened listing was replaced') - toast.warning('The listing has been updated!') + useNotificationStore + .getState() + .addNotification('The listing has been updated!', 'warning') // Move the selection over if (state.selectedItemsMap[prev.uuid]) { @@ -299,7 +354,7 @@ export const useListingsStore = create()( delete state.filteredByGroupListings[prev.uuid] } - state.listingsMap[category || ''].splice(idx, 1, l) + state.listingsMap[categoryKey].splice(idx, 1, l) } } return false @@ -317,10 +372,10 @@ export const useListingsStore = create()( state.filteredByGroupListings ) - if (state.listingsMap[category]) { - state.listingsMap[category].push(...newListings) + if (state.listingsMap[categoryKey]) { + state.listingsMap[categoryKey].push(...newListings) } else { - state.listingsMap[category] = newListings + state.listingsMap[categoryKey] = newListings } // Add default rowselection @@ -333,23 +388,30 @@ export const useListingsStore = create()( cleanupListings: () => set((state) => { - if (!state.category) return + if (!getCategory(state)) return // Delete all listings timestampMs > 30min const now = dayjs.utc().valueOf() - let idx = state.listingsMap[state.category].length + let idx = state.listingsMap[getCategory(state)].length while (idx--) { - const l = state.listingsMap[state.category][idx] + const l = state.listingsMap[getCategory(state)][idx] if (now - l.meta.timestampMs > 30 * 60 * 1000) { - if (!(state.dialogOpen && state.selectedListingId === l.uuid)) { - // Delete only if the listing is not opened. The listing will be deleted in the next request - if (state.selectedItemsMap[l.uuid]) { - delete state.selectedItemsMap[l.uuid] - } - if (state.filteredByGroupListings[l.uuid]) { - delete state.filteredByGroupListings[l.uuid] - } - state.listingsMap[state.category].splice(idx, 1) + if (state.dialogOpen && state.selectedListingId === l.uuid) { + state.dialogOpen = false + console.warn('The dialog was closed because the opened listing got stale') + useNotificationStore + .getState() + .addNotification( + 'Sorry! The selected listing is stale and got deleted!', + 'warning' + ) + } + if (state.selectedItemsMap[l.uuid]) { + delete state.selectedItemsMap[l.uuid] } + if (state.filteredByGroupListings[l.uuid]) { + delete state.filteredByGroupListings[l.uuid] + } + state.listingsMap[getCategory(state)].splice(idx, 1) } } }), @@ -382,7 +444,7 @@ export const useListingsStore = create()( state.selectedItemsMap[state.selectedListingId || ''] = nextState } - const listing = state.listingsMap[state.category || ''].find( + const listing = getListingsByCategory(state).find( (l) => l.uuid === state.selectedListingId ) @@ -394,7 +456,7 @@ export const useListingsStore = create()( updateData: (rowIndex, columnId, value) => set((state) => { - const listing = state.listingsMap[state.category || ''].find( + const listing = getListingsByCategory(state).find( (l) => l.uuid === state.selectedListingId ) if (!listing) return {} @@ -416,6 +478,15 @@ export const useListingsStore = create()( { name: 'listings-items-storage', storage: createJSONStorage(() => localStorage), + // TODO: On league change => increase version! + version: 1, + migrate: (persistedState: unknown, version: number) => { + const nextState = persistedState as State & Actions + if (nextState && typeof nextState === 'object' && 'league' in nextState) { + nextState.league = SUPPORTED_LEAGUES[0] + } + return nextState + }, partialize: (state) => ({ league: state.league }), diff --git a/src/green-app/src/app/listings/page.tsx b/src/green-app/src/app/[locale]/listings/page.tsx similarity index 71% rename from src/green-app/src/app/listings/page.tsx rename to src/green-app/src/app/[locale]/listings/page.tsx index 78028755..774fdb76 100644 --- a/src/green-app/src/app/listings/page.tsx +++ b/src/green-app/src/app/[locale]/listings/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { ListingCategorySelect } from '@/components/poe/ListingCategorySelect' +import { ListingCategorySelect } from '@/components/listing-category-select' import ListingFilterCard from '@/components/trade-filter-card' import { Accordion, @@ -17,12 +17,17 @@ import ListingsHandler from './listings-handler' import ListingsTable from './listings-table' import { listingsTableColumns } from './listings-table-columns' import { useListingsStore } from './listingsStore' +import { useTranslation } from 'react-i18next' export default function ListingsPage() { + const { t } = useTranslation() const selectedLeague = useListingsStore((state) => state.league) const [selectedCategory, setSelectedCategory] = useListingsStore( useShallow((state) => [state.category, state.setCategory]) ) + const [selectedSubCategory, setSelectedSubCategory] = useListingsStore( + useShallow((state) => [state.subCategory, state.setSubCategory]) + ) const [filterGroups, setFilterGroups] = useListingsStore( useShallow((state) => [state.filterGroups, state.setFilterGroups]) ) @@ -50,19 +55,33 @@ export default function ListingsPage() {
+
+
+
- Item Filter + {t('title.itemFilter')} diff --git a/src/green-app/src/app/page.tsx b/src/green-app/src/app/[locale]/page.tsx similarity index 100% rename from src/green-app/src/app/page.tsx rename to src/green-app/src/app/[locale]/page.tsx diff --git a/src/green-app/src/app/test/page.tsx b/src/green-app/src/app/[locale]/test/page.tsx similarity index 100% rename from src/green-app/src/app/test/page.tsx rename to src/green-app/src/app/[locale]/test/page.tsx diff --git a/src/green-app/src/app/[locale]/trade/[notification]/page.tsx b/src/green-app/src/app/[locale]/trade/[notification]/page.tsx new file mode 100644 index 00000000..955038fc --- /dev/null +++ b/src/green-app/src/app/[locale]/trade/[notification]/page.tsx @@ -0,0 +1,5 @@ +const Page = () => { + return <>Test +} + +export default Page diff --git a/src/green-app/src/app/favicon.ico b/src/green-app/src/app/favicon.ico index 718d6fea..bd5bf91c 100644 Binary files a/src/green-app/src/app/favicon.ico and b/src/green-app/src/app/favicon.ico differ diff --git a/src/green-app/src/app/layout.tsx b/src/green-app/src/app/layout.tsx deleted file mode 100644 index e85e14b8..00000000 --- a/src/green-app/src/app/layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' -import './globals.css' -import 'react-toastify/dist/ReactToastify.css' -import { cn } from '@/lib/utils' -import { Button } from '@/components/ui/button' -import Link from 'next/link' -import { ProfileMenu } from '@/components/profile-menu' -import { Providers } from '@/components/providers' - -const inter = Inter({ subsets: ['latin'] }) - -export const metadata: Metadata = { - title: 'PoeStack - Bulk', - description: 'Sell and buy items easily.' -} - -export default function RootLayout({ - children -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - -
-
-
-
- - -
-
- -
-
-
-
{children}
-
- - - ) -} diff --git a/src/green-app/src/app/listing-tool/listing-tool-handler.tsx b/src/green-app/src/app/listing-tool/listing-tool-handler.tsx deleted file mode 100644 index 5c8358f9..00000000 --- a/src/green-app/src/app/listing-tool/listing-tool-handler.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { useListingToolStore } from '@/app/listing-tool/listingToolStore' -import { DEFAULT_VALUATION_INDEX } from '@/lib/constants' -import { listStash, listValuations } from '@/lib/http-util' -import { ItemGroupingService } from '@/lib/item-grouping-service' -import { - createCompactTab, - filterPricedItems, - mapItemsToDisplayedItems, - mapItemsToPricedItems, - mapMapStashItemToPoeItem, - mergeItemStacks, - mergeItems -} from '@/lib/item-util' -import { LISTING_CATEGORIES } from '@/lib/listing-categories' -import { IStashTab } from '@/types/echo-api/stash' -import { GroupedItem, StashItem, ValuatedItem } from '@/types/item' -import { PoeItem } from '@/types/poe-api-models' -import { useQueries } from '@tanstack/react-query' -import { memo, useEffect, useMemo } from 'react' - -type ListingToolHandlerProps = { - setRefetchAll: (refetchAll: () => void) => void - setStashListFetching: (fetching: boolean) => void -} - -const ListingToolHandler = ({ setRefetchAll, setStashListFetching }: ListingToolHandlerProps) => { - const stashes = useListingToolStore((state) => state.stashes[state.league]) - const league = useListingToolStore((state) => state.league) - const selectedCategory = useListingToolStore((state) => state.category) - const setSelectedCategory = useListingToolStore((state) => state.setCategory) - const setInitialItems = useListingToolStore((state) => state.setInitialItems) - const setSelectableCategories = useListingToolStore((state) => state.setSelectableCategories) - - // const setRefetchAll = useSetAtom(refetchAllAtom) - // const setStashListFetching = useSetAtom(stashListFetchingAtom) - - // TODO: Attention, this data is not stable! idk how to fix it. Is not fixable with useMemo somehow idk. - const groupedItemsResults = useQueries({ - queries: stashes - ? stashes.map((stash) => { - return { - queryKey: ['stash', league, stash.id], - queryFn: async () => { - if (!league) return [] - const stashItems = await listStash(league, stash.id) - - let items: PoeItem[] = [] - if (stashItems.type === 'MapStash') { - items = mapMapStashItemToPoeItem(stashItems, league) - } else if (stashItems.items) { - items = stashItems.items - } - return new ItemGroupingService().withGroup(items).map((x) => ({ ...x, stash })) - }, - enabled: false - } - }) - : [] - }) - - const { - data: [categoryTagItem, selectableTagsCount, ungroupedItems], - // isGroupedItemsPending, - isGroupedItemsSuccess, - isGroupedItemsLoading, - isGroupedItemsFetching, - isGroupedItemsError, - refetchAll - } = useMemo(() => { - const categoryTagItem: Record = {} - const selectableTags: Record = {} - const ungroupedItems: StashItem[] = [] - groupedItemsResults.forEach((result) => { - result.data?.map((item) => { - const category = LISTING_CATEGORIES.find((e) => e.name === selectedCategory) - if ( - item.group && - (category?.tags?.includes(item.group.primaryGroup.tag) || !selectedCategory) - ) { - if (categoryTagItem[item.group.primaryGroup.tag]) { - categoryTagItem[item.group.primaryGroup.tag].push(item) - } else { - categoryTagItem[item.group.primaryGroup.tag] = [item] - } - } else if (!selectedCategory) { - // No category items; No valuated items; Only stackable items will be shown - ungroupedItems.push(item) - } - - if (item.group) { - selectableTags[item.group.primaryGroup.tag] ??= 0 - selectableTags[item.group.primaryGroup.tag] += 1 - } - }) - }) - - return { - data: [categoryTagItem, selectableTags, ungroupedItems], - // isGroupedItemsPending: groupedItemsResults.some((result) => result.isPending), - isGroupedItemsSuccess: groupedItemsResults.some((result) => result.isSuccess), - isGroupedItemsLoading: groupedItemsResults.some((result) => result.isLoading), - isGroupedItemsFetching: groupedItemsResults.some((result) => result.isFetching), - isGroupedItemsError: groupedItemsResults.some((result) => result.isError), - refetchAll: () => - groupedItemsResults.forEach((result) => { - result.refetch() - }) - } - }, [groupedItemsResults, selectedCategory]) - - useEffect(() => { - // Autoselect logic: - // - If the selected tag has items autoselect the first tag which was found with the most items in it - // - If the next stashes contains the selected tag, then do not change the tag. Even if stashes are reloaded - // - If a stash selected but not loaded the tag will not deselected - // - Auto deselect tag when the selected tag is not available - if (!(isGroupedItemsSuccess && !isGroupedItemsLoading)) return - - const selectedCategoryContainsItems = Object.values(categoryTagItem).some( - (items) => items.length > 0 - ) - - if (selectedCategoryContainsItems && selectedCategory) return - - if (Object.keys(selectableTagsCount).length === 0) { - console.log('Deselect category') - return setSelectedCategory(null) - } - - const tagToSelect = Object.entries(selectableTagsCount).sort((a, b) => b[1] - a[1])[0] - - const category = LISTING_CATEGORIES.find((e) => e.tags.includes(tagToSelect[0])) - if (category) { - console.log('Autoselect category', category.name) - setSelectedCategory(category.name) - } - // Some objects are not stable! We use booleans to determine the change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [league, stashes, isGroupedItemsSuccess, isGroupedItemsLoading, setSelectedCategory]) - - const { data: displayedItems, isValuationPending } = useQueries({ - queries: - categoryTagItem && league - ? Object.keys(categoryTagItem).map((tag) => { - return { - queryKey: ['valuations', league, tag], - queryFn: () => listValuations(league, tag), - gcTime: 20 * 60 * 1000, - staleTime: 20 * 60 * 1000, - enabled: !!league && !!tag - } - }) - : [], // if users is undefined, an empty array will be returned - combine: (valuationResults) => { - const valuationShards = valuationResults - .filter((x) => x.data && !x.isPending) - .map((x) => x.data!) - - const valuationItems = Object.entries(categoryTagItem) - .map(([tag, items]) => { - const valuations = valuationShards.find((shard) => shard.meta.tag === tag)?.valuations - return items.map((item): GroupedItem | ValuatedItem => { - if (valuations && item.group && valuations[item.group.primaryGroup.hash]) { - return { - ...item, - valuation: valuations[item.group.primaryGroup.hash] - } - } - return item - }) - }) - .flatMap((x) => x) - - // Merge all items to stashtabs - const mergedStashItems: { stashTab: IStashTab; valuation: ValuatedItem[] }[] = [] - valuationItems.forEach((item) => { - if (item) { - const stashTab = mergedStashItems.find((x) => x.stashTab.id === item.stash.id) - if (stashTab) { - stashTab.valuation.push(item) - } else { - mergedStashItems.push({ stashTab: item.stash, valuation: [item] }) - } - } - }) - // Not grouped items - Which are not selectable and an overprice can not be set - ungroupedItems.forEach((item) => { - const stashTab = mergedStashItems.find((x) => x.stashTab.id === item.stash.id) - if (stashTab) { - stashTab.valuation.push(item) - } else { - mergedStashItems.push({ stashTab: item.stash, valuation: [item] }) - } - }) - const currentState = useListingToolStore.getState() - const selectedMultiplier = currentState.localMultiplier / 100 - const overprices = currentState.overprices - - let displayedItems = mergedStashItems - .map((valuatedStash) => { - const compactStash = createCompactTab(valuatedStash.stashTab) - let pricedItems = mapItemsToPricedItems( - valuatedStash.valuation, - compactStash, - DEFAULT_VALUATION_INDEX - ) - pricedItems = filterPricedItems(pricedItems, selectedCategory) - const pricedStackedItems = mergeItems(pricedItems) - return mapItemsToDisplayedItems(pricedStackedItems, selectedMultiplier, overprices) - }) - .flatMap((x) => x) - - displayedItems = mergeItemStacks(displayedItems) - - const isValuationError = - isGroupedItemsError || valuationResults.some((result) => result.isError) - const isValuationFetching = - isGroupedItemsFetching || valuationResults.some((result) => result.isFetching) - return { - data: isValuationError || isValuationFetching ? [] : displayedItems, - // We have stash indicators for error handling or informations - // data: displayedItems, - isValuationPending: valuationResults.some((result) => result.isPending), - isValuationFetching, - isValuationError - } - } - }) - - useEffect(() => { - setInitialItems(displayedItems, selectedCategory) - }, [displayedItems, selectedCategory, setInitialItems]) - - useEffect(() => { - setRefetchAll(refetchAll) - }, [refetchAll, setRefetchAll]) - - useEffect(() => { - setStashListFetching(isGroupedItemsFetching || isValuationPending) - }, [isGroupedItemsFetching, isValuationPending, setStashListFetching]) - - useEffect(() => { - const selectableCategories = LISTING_CATEGORIES.filter((category) => - Object.keys(selectableTagsCount)?.some((tag) => category.tags.includes(tag)) - ) - setSelectableCategories(selectableCategories) - }, [selectableTagsCount, setSelectableCategories]) - - return null -} - -export default memo(ListingToolHandler) diff --git a/src/green-app/src/app/listing-tool/listing-tool-table-columns.tsx b/src/green-app/src/app/listing-tool/listing-tool-table-columns.tsx deleted file mode 100644 index 9f53a425..00000000 --- a/src/green-app/src/app/listing-tool/listing-tool-table-columns.tsx +++ /dev/null @@ -1,569 +0,0 @@ -'use client' - -import CurrencyDisplay from '@/components/currency-display' -import DebouncedInput from '@/components/debounced-input' -import { Badge } from '@/components/ui/badge' -import { Checkbox } from '@/components/ui/checkbox' -import { CurrencySwitch, formatValue, round } from '@/lib/currency' -import { SageItemGroup } from '@/lib/item-grouping-service' -import { parseTabNames, parseUnsafeHashProps } from '@/lib/item-util' -import { cn } from '@/lib/utils' -import { IDisplayedItem } from '@/types/echo-api/priced-item' -import { SageValuation } from '@/types/echo-api/valuation' -import { ColumnDef, Row } from '@tanstack/react-table' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import utc from 'dayjs/plugin/utc' -import Image from 'next/image' -import { ChangeEvent, useMemo } from 'react' -import { Area, AreaChart, ResponsiveContainer } from 'recharts' -import { TableColumnHeader } from '../../components/column-header' -dayjs.extend(relativeTime) -dayjs.extend(utc) - -type DisplayedItem = keyof IDisplayedItem | keyof SageItemGroup | 'cumulative' - -export function checkColumn(): ColumnDef { - const key = 'selected' - - return { - header: ({ table }) => { - return ( -
- table.toggleAllRowsSelected( - !(table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && 'indeterminate')) - ) - } - > - table.toggleAllRowsSelected(!!value)} - aria-label="Select all" - /> -
- ) - }, - id: key, - enableResizing: false, - enableSorting: true, - enableGlobalFilter: false, - enableMultiSort: true, - size: 40, - meta: { - className: 'min-w-[40px] max-w-fit p-0', - removePadding: true - }, - sortDescFirst: true, - cell: ({ row }) => { - return ( -
{ - if (row.original.group) { - row.toggleSelected(!row.getIsSelected()) - } - }} - > - row.toggleSelected(!!value)} - aria-label="Select row" - disabled={!row.original.group} - /> -
- ) - } - } -} - -export function nameColumn(): ColumnDef { - const key: DisplayedItem = 'displayName' - const header = 'Name' - - return { - header: ({ column }) => , - accessorKey: key, - enableSorting: true, - enableGlobalFilter: true, - size: 230, - minSize: 90, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.getValue(key) - - return ( -
- - -
- ) - } - } -} - -export function tagColumn(): ColumnDef { - const key: DisplayedItem = 'tag' - const header = 'Tag' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (val) => val.group?.tag, - enableSorting: true, - enableGlobalFilter: true, - size: 65, - minSize: 65, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function propsColumn(): ColumnDef { - const key: DisplayedItem = 'unsafeHashProperties' - const header = 'Props' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (val) => parseUnsafeHashProps(val.group?.unsafeHashProperties), - enableSorting: true, - enableGlobalFilter: true, - size: 200, - minSize: 100, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function tabsColumn(): ColumnDef { - const key: DisplayedItem = 'tabs' - const header = 'Tab' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (val) => parseTabNames(val.tabs), - enableSorting: true, - enableGlobalFilter: true, - size: 75, - minSize: 50, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function quantityColumn(options: { diff?: boolean }): ColumnDef { - const { diff } = options - - const key: DisplayedItem = 'stackSize' - const header = 'Quantity' - - return { - header: ({ column }) => , - accessorKey: key, - enableSorting: true, - enableGlobalFilter: false, - enableResizing: false, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function historyColumn(): ColumnDef { - const key: DisplayedItem = 'valuation' - const header = 'Price last 2 days' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (pricedItem) => { - const valuation = pricedItem.valuation - if (!valuation) return 0 - // Remove indexes - const history = valuation.history.primaryValueHourly.slice() - if (history.length < 2) return 0 - let i = history.length - let indexToUse = history.length - while (i--) { - if (history[i]) { - indexToUse = i - break - } - } - if (indexToUse === 0) return 0 - - return (history[indexToUse] / history[0] - 1) * 100 - }, - enableSorting: true, - enableGlobalFilter: false, - size: 200, - minSize: 100, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.original.valuation - const totalChange = row.getValue(key) - return - } - } -} - -export function itemValue(options: { - accessorKey: DisplayedItem - header: string - cumulative?: boolean - showChange?: boolean - toCurrency?: 'chaos' | 'divine' | 'both' - enableSorting?: boolean - accessorFn?: (value: IDisplayedItem) => string | number -}): ColumnDef { - const { header, accessorKey, accessorFn, cumulative, showChange, toCurrency, enableSorting } = - options - - return { - header: ({ column }) => , - accessorKey, - accessorFn: accessorFn, - enableSorting: enableSorting ?? false, - enableGlobalFilter: false, - enableResizing: false, - sortingFn: (rowA: Row, rowB: Row, columnId: string) => { - const val1 = rowA.getValue(columnId) - const val2 = rowB.getValue(columnId) - if (typeof val1 === 'number' && typeof val2 === 'number') { - return val1 - val2 - } else if (typeof val1 === 'number') { - return val1 - 0 - } else { - return 0 - (val2 as number) - } - }, - meta: { - headerWording: header - }, - cell: ({ row, table }) => { - let value = 0 - if (cumulative) { - const sortedRows = table.getSortedRowModel().rows - for (let i = 0; i < sortedRows.length; i++) { - const total = sortedRows[i].original.calculatedTotal - if (total !== undefined) { - value += total - } - if (sortedRows[i].id === row.id) { - break - } - } - } else if (accessorKey) { - value = row.getValue(accessorKey) - } - - return - } - } -} - -type ItemIconCellProps = { - value: string - // frameType: number -} - -const ItemIconCell = ({ value }: ItemIconCellProps) => { - // const rarityColor = rarityColors[getRarity(frameType)] - - return ( -
- {/*
*/} - {''} -
- ) -} - -type ItemNameCellProps = { - value: string -} - -const ItemNameCell = ({ value }: ItemNameCellProps) => { - return ( -
{ - e.currentTarget.scrollLeft = 0 - }} - > - {value} -
- ) -} - -type ItemTagCellProps = { - value: string -} - -const ItemTagCell = ({ value }: ItemTagCellProps) => { - return {value} -} - -type ItemPropsCellProps = { - value: string -} - -const ItemPropsCell = ({ value }: ItemPropsCellProps) => { - const hashProps = useMemo(() => { - if (!value) return [] - return value.split(';;;').map((v) => { - const keyVal = v.split(';;') - return { name: keyVal[0], value: keyVal[1] } - }) - }, [value]) - - return ( -
(e.currentTarget.scrollLeft = 0)} - > - {hashProps.map(({ name, value }) => ( - - {value} - - ))} -
- ) -} - -type ItemTabsCellProps = { - value: string -} - -const ItemTabsCell = ({ value }: ItemTabsCellProps) => { - return {value} -} - -type ItemQuantityCellProps = { - quantity: number - diff?: boolean -} - -const ItemQuantityCell = ({ quantity, diff }: ItemQuantityCellProps) => { - return ( -
0 && `text-green-400`, - diff && quantity < 0 && `text-red-400` - )} - > - {diff && quantity > 0 ? '+ ' : ''} - {quantity} -
- ) -} - -type ItemValueCellProps = { - value: number | string - editable?: boolean - showChange?: boolean - toCurrency?: CurrencySwitch -} - -const ItemValueCell = ({ value, showChange, toCurrency }: ItemValueCellProps) => { - return ( -
- {typeof value === 'number' ? ( - - ) : ( - value - )} -
- ) -} - -export function quantityInputColumn(): ColumnDef { - const key = 'selectedPrice' - const header = 'Override' - - return { - header: ({ column }) => , - accessorKey: key, - enableSorting: true, - enableGlobalFilter: false, - enableResizing: false, - meta: { - headerWording: header - }, - cell: ({ row, table, column }) => { - const initialValue = row.getValue(key) - - const onInnerChange = (e: ChangeEvent) => { - const newValue = parseFloat(e.target.value) - if (Number.isNaN(newValue) || newValue < 0) return '' - return round(newValue, 4) - } - - const placeHolder = `${row.original.originalPrice ?? '?'}c` - - const updateTableData = (value: string | number) => { - if (typeof value === 'number') { - table.options.meta?.updateData(row.index, column.id, value) - } else if (!Number.isNaN(parseFloat(value))) { - table.options.meta?.updateData(row.index, column.id, parseFloat(value)) - } else { - table.options.meta?.updateData(row.index, column.id, value) - } - } - - return ( - updateTableData(value)} - onBlur={(value) => updateTableData(value)} - debounce={250} - /> - ) - } - } -} - -type SparklineCellProps = { - valuation?: SageValuation - totalChange: number -} - -const SparklineCell = ({ valuation, totalChange }: SparklineCellProps) => { - const data = useMemo(() => { - if (!valuation) return - - return valuation.history.primaryValueHourly.map((value) => { - const format = formatValue(value, 'chaos') - return { - name: 'history', - value: format.value - } - }) - }, [valuation]) - - return ( - <> - {data && ( -
- - - - - - - - - - - - -
0 && ` text-green-400`, - totalChange < 0 && ` text-red-400` - )} - > - {totalChange.toLocaleString(undefined, { - maximumFractionDigits: 1 - })}{' '} - % -
-
- )} - - ) -} - -export const listingToolTableEditModeColumns = (): ColumnDef[] => [ - checkColumn(), - nameColumn(), - propsColumn(), - // itemTag(), - tabsColumn(), - quantityColumn({}), - historyColumn(), - quantityInputColumn(), - itemValue({ - accessorKey: 'calculatedPrice', - accessorFn: (item) => (item.calculatedPrice !== undefined ? item.calculatedPrice : '?'), - header: 'Value', - enableSorting: true - }), - itemValue({ - accessorKey: 'calculatedTotal', - header: 'Total Value', - enableSorting: true, - accessorFn: (item) => item.calculatedTotal - }) - // itemValue({ - // accessorKey: 'cumulative', - // header: 'cumulative', - // cumulative: true - // }) -] diff --git a/src/green-app/src/app/listing-tool/my-offerings-card.tsx b/src/green-app/src/app/listing-tool/my-offerings-card.tsx deleted file mode 100644 index 40bbdb43..00000000 --- a/src/green-app/src/app/listing-tool/my-offerings-card.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import CurrencyDisplay from '@/components/currency-display' -import { currentUserAtom } from '@/components/providers' -import { Button } from '@/components/ui/button' -import { Separator } from '@/components/ui/separator' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { - SageDatabaseOfferingTypeExt, - deleteListing, - listMyListings, - listStashes -} from '@/lib/http-util' -import { LISTING_CATEGORIES } from '@/lib/listing-categories' -import { IStashTab } from '@/types/echo-api/stash' -import { SageOfferingType } from '@/types/sage-listing-type' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import utc from 'dayjs/plugin/utc' -import { useAtomValue } from 'jotai' -import { - ArrowLeftToLineIcon, - CircleUserIcon, - LayoutListIcon, - MoreHorizontalIcon, - PackageIcon, - Trash2Icon -} from 'lucide-react' -import Image from 'next/image' -import { useEffect, useMemo, useState } from 'react' -dayjs.extend(relativeTime) -dayjs.extend(utc) - -type MyOfferingsCardProps = { - league: string | null - setCategory: (category: string | null) => void - setStashes: (stashes: IStashTab[]) => void -} - -export function MyOfferingsCard({ league, setCategory, setStashes }: MyOfferingsCardProps) { - const queryClient = useQueryClient() - const currentUser = useAtomValue(currentUserAtom) - const [listings, setListings] = useState() - - const { data: stashes } = useQuery({ - queryKey: ['stashes', league], - queryFn: () => { - if (!league) return [] as IStashTab[] - return listStashes(league) - }, - staleTime: 5 * 60 * 1000, - enabled: !!currentUser?.profile?.uuid && !!league - }) - - const { data: allListings, isLoading } = useQuery({ - queryKey: ['my-listings'], - queryFn: () => listMyListings().then((res) => res.filter((l) => !l.deleted)) - }) - - useEffect(() => { - let now = dayjs.utc().valueOf() - setListings(allListings?.filter((l) => now - l.meta.timestampMs < 30 * 60 * 1000)) - const interval = setInterval(() => { - now = dayjs.utc().valueOf() - setListings(allListings?.filter((l) => now - l.meta.timestampMs < 30 * 60 * 1000)) - }, 5000) - - return () => clearInterval(interval) - }, [allListings]) - - const shownListings = useMemo( - () => listings?.filter((l) => l.meta.league === league), - [listings, league] - ) - - // Optimistic delete; See: https://tanstack.com/query/v4/docs/framework/react/guides/optimistic-updates - const deleteMutation = useMutation({ - mutationFn: ({ league, category, uuid }: { league: string; category: string; uuid: string }) => - deleteListing(league, category, "test", uuid), - onMutate: async (deleted) => { - await queryClient.cancelQueries({ queryKey: ['my-listings'] }) - const previousListings = queryClient.getQueryData(['my-listings']) - queryClient.setQueryData(['my-listings'], (old: SageDatabaseOfferingTypeExt[]) => - old.filter( - (x) => !(x.meta.league === deleted.league && x.meta.category === deleted.category) - ) - ) - return { previousListings } - }, - onError: (err, deletedListing, context) => { - queryClient.setQueryData(['my-listings'], context?.previousListings) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['my-listings'] }) - } - }) - - return ( -
-

My Offerings

-
-
- {(shownListings?.length || 0) > 0 ? ( - shownListings?.map((listing) => { - const meta = listing.meta - - const category = LISTING_CATEGORIES.find((cat) => cat.name === meta.category) - - return ( -
-
-
- {category && ( - {category.name} - )} -
{meta.category}
-
-
- -
-
-
-
- {meta.listingMode === 'bulk' ? ( - <> - - Whole Offering - - ) : ( - <> - - Individual - - )} - {/* {meta.tabs.map((tab, i) => ( - {tab} - ))} */} -
-
{dayjs.utc(meta.timestampMs).fromNow()}
-
-
- {/*
*/} -
- - {meta.ign} - {/* {' - '} */} - {/* {meta.league} */} -
-
- - - - - - - This selects only the stashtabs and category. -
- All other settings are still applied: -
    -
  • Multiplier per category
  • -
  • Overrides/Overprices
  • -
  • Unselected items
  • -
-
-
- - {/* - */} -
-
-
-
- ) - }) - ) : ( -
No offerings.
- )} -
-
-
- ) -} diff --git a/src/green-app/src/app/listings/listing-table-columns.tsx b/src/green-app/src/app/listings/listing-table-columns.tsx deleted file mode 100644 index 701d6e24..00000000 --- a/src/green-app/src/app/listings/listing-table-columns.tsx +++ /dev/null @@ -1,562 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -'use client' - -import CurrencyDisplay from '@/components/currency-display' -import DebouncedInput from '@/components/debounced-input' -import { Badge } from '@/components/ui/badge' -import { Checkbox } from '@/components/ui/checkbox' -import { CurrencySwitch, formatValue, round } from '@/lib/currency' -import { parseUnsafeHashProps } from '@/lib/item-util' -import { cn } from '@/lib/utils' -import { SageValuation } from '@/types/echo-api/valuation' -import { ListingMode, SageListingItemType } from '@/types/sage-listing-type' -import { ColumnDef, Row } from '@tanstack/react-table' -import dayjs from 'dayjs' -import relativeTime from 'dayjs/plugin/relativeTime' -import utc from 'dayjs/plugin/utc' -import Image from 'next/image' -import { ChangeEvent, useMemo } from 'react' -import { Area, AreaChart, ResponsiveContainer } from 'recharts' -import { TableColumnHeader } from '../../components/column-header' -dayjs.extend(relativeTime) -dayjs.extend(utc) - -type ColumnDefProps = { - listingMode: ListingMode -} - -export function checkColumn(): ColumnDef { - const key = 'selected' - - return { - header: ({ table }) => { - return ( -
- table.toggleAllRowsSelected( - !(table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && 'indeterminate')) - ) - } - > - table.toggleAllRowsSelected(!!value)} - aria-label="Select all" - /> -
- ) - }, - id: key, - enableResizing: false, - enableSorting: true, - enableGlobalFilter: false, - enableMultiSort: true, - size: 40, - meta: { - className: 'min-w-[40px] max-w-fit p-0', - removePadding: true - }, - sortDescFirst: true, - cell: ({ row }) => { - return ( -
{ - row.toggleSelected(!row.getIsSelected()) - }} - > - row.toggleSelected(!!value)} - aria-label="Select row" - /> -
- ) - } - } -} - -export function nameColumn(): ColumnDef { - const key = 'displayName' - const header = 'Name' - - return { - header: ({ column }) => , - accessorKey: key, - enableSorting: true, - enableGlobalFilter: true, - size: 230, - minSize: 90, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.getValue(key) - - return ( -
- - -
- ) - } - } -} - -export function propsColumn(): ColumnDef { - const key = 'unsafeHashProperties' - const header = 'Props' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (val) => parseUnsafeHashProps(val.summary.unsafeHashProperties), - enableSorting: true, - enableGlobalFilter: true, - size: 200, - minSize: 100, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function quantityColumn(options: { diff?: boolean }): ColumnDef { - const { diff } = options - - const key = 'quantity' - const header = 'Quantity' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (item) => { - return item.quantity - }, - enableSorting: true, - enableGlobalFilter: false, - enableResizing: false, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return - } - } -} - -export function historyColumn(): ColumnDef { - const key = 'valuation' - const header = 'Price last 2 days' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (pricedItem) => { - const valuation = pricedItem.valuation - if (!valuation) return 0 - // Remove indexes - const history = valuation.history.primaryValueHourly.slice() - if (history.length < 2) return 0 - let i = history.length - let indexToUse = history.length - while (i--) { - if (history[i]) { - indexToUse = i - break - } - } - if (indexToUse === 0) return 0 - - return (history[indexToUse] / history[0] - 1) * 100 - }, - enableSorting: true, - enableGlobalFilter: false, - size: 200, - minSize: 100, - meta: { - headerWording: header, - staticResizing: true - }, - cell: ({ row }) => { - const value = row.original.valuation - const totalChange = row.getValue(key) - return - } - } -} - -export function itemValue(options: { - accessorKey: string - header: string - cumulative?: boolean - showChange?: boolean - toCurrency?: 'chaos' | 'divine' | 'both' - enableSorting?: boolean - accessorFn?: (value: SageListingItemType) => string | number -}): ColumnDef { - const { header, accessorKey, accessorFn, cumulative, showChange, toCurrency, enableSorting } = - options - - return { - header: ({ column }) => , - accessorKey, - accessorFn: accessorFn, - enableSorting: enableSorting ?? false, - enableGlobalFilter: false, - enableResizing: false, - sortingFn: ( - rowA: Row, - rowB: Row, - columnId: string - ) => { - const val1 = rowA.getValue(columnId) - const val2 = rowB.getValue(columnId) - if (typeof val1 === 'number' && typeof val2 === 'number') { - return val1 - val2 - } else if (typeof val1 === 'number') { - return val1 - 0 - } else { - return 0 - (val2 as number) - } - }, - meta: { - headerWording: header - }, - cell: ({ row, table }) => { - let value = 0 - // if (cumulative) { - // const sortedRows = table.getSortedRowModel().rows - // for (let i = 0; i < sortedRows.length; i++) { - // const total = sortedRows[i].original.calculatedTotal - // if (total !== undefined) { - // value += total - // } - // if (sortedRows[i].id === row.id) { - // break - // } - // } - // } else if (accessorKey) { - value = row.getValue(accessorKey) - // } - - return - } - } -} - -export function multiplierColumn(): ColumnDef { - const key = 'multiplier' - const header = 'Multiplier' - - return { - header: ({ column }) => , - accessorKey: key, - accessorFn: (item) => { - if (item.primaryValuation === 0 || item.price === 0) return '- ' - return ((item.price / item.primaryValuation) * 100).toLocaleString(undefined, { - minimumFractionDigits: 0, - maximumFractionDigits: 2 - }) - }, - enableSorting: true, - enableGlobalFilter: false, - enableResizing: false, - meta: { - headerWording: header - }, - cell: ({ row }) => { - const value = row.getValue(key) - return
{value}%
- } - } -} - -type ItemNameCellProps = { - value: string -} - -const ItemNameCell = ({ value }: ItemNameCellProps) => { - return ( -
{ - e.currentTarget.scrollLeft = 0 - }} - > - {value} -
- ) -} - -type ItemIconCellProps = { - value: string - // frameType: number -} - -const ItemIconCell = ({ value }: ItemIconCellProps) => { - // const rarityColor = rarityColors[getRarity(frameType)] - - return ( -
- {/*
*/} - {''} -
- ) -} - -type ItemPropsCellProps = { - value: string -} - -const ItemPropsCell = ({ value }: ItemPropsCellProps) => { - const hashProps = useMemo(() => { - if (!value) return [] - return value.split(';;;').map((v) => { - const keyVal = v.split(';;') - return { name: keyVal[0], value: keyVal[1] } - }) - }, [value]) - - return ( -
(e.currentTarget.scrollLeft = 0)} - > - {hashProps.map(({ name, value }) => ( - - {value} - - ))} -
- ) -} - -type ItemQuantityCellProps = { - quantity: number - diff?: boolean -} - -const ItemQuantityCell = ({ quantity, diff }: ItemQuantityCellProps) => { - return ( -
0 && `text-green-400`, - diff && quantity < 0 && `text-red-400` - )} - > - {diff && quantity > 0 ? '+ ' : ''} - {quantity} -
- ) -} - -type ItemValueCellProps = { - value: number | string - editable?: boolean - showChange?: boolean - toCurrency?: CurrencySwitch -} - -const ItemValueCell = ({ value, showChange, toCurrency }: ItemValueCellProps) => { - return ( -
- {typeof value === 'number' ? ( - - ) : ( - value - )} -
- ) -} - -export function quantityInputColumn(): ColumnDef { - const key = 'selectedQuantity' - const header = 'Asking Quantity' - - return { - header: ({ column }) => , - accessorKey: key, - enableSorting: true, - enableGlobalFilter: false, - enableResizing: false, - meta: { - headerWording: header - }, - cell: ({ row, table, column }) => { - const initialValue = row.getValue(key) - - const onInnerChange = (e: ChangeEvent) => { - const newValue = parseFloat(e.target.value) - if (Number.isNaN(newValue) || newValue < 0) return 0 - else if (newValue > row.original.quantity) return round(row.original.quantity) - return round(newValue, 4) - } - - const placeHolder = `${row.original.quantity ?? '?'}` - - const updateTableData = (value: string | number) => { - if (typeof value === 'number') { - table.options.meta?.updateData(row.index, column.id, value) - } else if (!Number.isNaN(parseFloat(value))) { - table.options.meta?.updateData(row.index, column.id, parseFloat(value)) - } else { - table.options.meta?.updateData(row.index, column.id, value) - } - } - - return ( - updateTableData(value)} - onBlur={(value) => updateTableData(value)} - debounce={250} - /> - ) - } - } -} - -type SparklineCellProps = { - valuation?: SageValuation - totalChange: number -} - -const SparklineCell = ({ valuation, totalChange }: SparklineCellProps) => { - const data = useMemo(() => { - if (!valuation) return - - return valuation.history.primaryValueHourly.map((value) => { - const format = formatValue(value, 'chaos') - return { - name: 'history', - value: format.value - } - }) - }, [valuation]) - - return ( - <> - {data && ( -
- - - - - - - - - - - - -
0 && ` text-green-400`, - totalChange < 0 && ` text-red-400` - )} - > - {totalChange.toLocaleString(undefined, { - maximumFractionDigits: 1 - })}{' '} - % -
-
- )} - - ) -} - -type ColumnsDefProps = ColumnDefProps & {} - -export const listingTableBulkModeColumns = ({ - listingMode -}: ColumnsDefProps): ColumnDef[] => [ - nameColumn(), - propsColumn(), - quantityColumn({}), - historyColumn(), - itemValue({ - accessorKey: 'price', - accessorFn: (item) => item.price, - header: 'Value', - enableSorting: true - }), - itemValue({ - accessorKey: 'calculatedTotal', - header: 'Total Value', - enableSorting: true, - accessorFn: (item) => item.price * item.selectedQuantity - }), - multiplierColumn() -] - -export const listingTradeSingleModeColumns = ({ - listingMode -}: ColumnsDefProps): ColumnDef[] => [ - checkColumn(), - nameColumn(), - propsColumn(), - quantityColumn({}), - historyColumn(), - itemValue({ - accessorKey: 'price', - accessorFn: (item) => item.price, - header: 'Value', - enableSorting: true - }), - itemValue({ - accessorKey: 'calculatedTotal', - header: 'Total Value', - enableSorting: true, - accessorFn: (item) => item.price * item.selectedQuantity - }), - multiplierColumn(), - quantityInputColumn() -] diff --git a/src/green-app/src/app/listings/listing-table.tsx b/src/green-app/src/app/listings/listing-table.tsx deleted file mode 100644 index 04ff2698..00000000 --- a/src/green-app/src/app/listings/listing-table.tsx +++ /dev/null @@ -1,257 +0,0 @@ -/* eslint-disable no-extra-boolean-cast */ -'use client' - -import DebouncedInput from '@/components/debounced-input' -import { TablePagination } from '@/components/table-pagination' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from '@/components/ui/table' -import { useSkipper } from '@/hooks/useSkipper' -import { cn } from '@/lib/utils' -import { SageListingItemType, SageListingType } from '@/types/sage-listing-type' -import { MagnifyingGlassIcon } from '@radix-ui/react-icons' -import { - ColumnDef, - FilterFnOption, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable -} from '@tanstack/react-table' -import React, { memo, useMemo, useState } from 'react' -import { useShallow } from 'zustand/react/shallow' -import { useListingsStore } from './listingsStore' -import { BasicSelect } from '@/components/poe/BasicSelect' -import { atom, useAtom } from 'jotai' - -interface DataTableProps { - columns: ColumnDef[] - className?: string - globalFilterFn?: FilterFnOption -} - -type ShowItemMode = 'Show all' | 'Show selected' | 'Show unselected' -const showItemsModeAtom = atom('Show all') - -// Tutorial: https://ui.shadcn.com/docs/components/data-table -const ListingTable = ({ columns, className, globalFilterFn }: DataTableProps) => { - const [globalFilter, setGlobalFilter] = useState('') - const [showItemsMode, setShowItemsMode] = useAtom(showItemsModeAtom) - - const listing = useListingsStore( - useShallow((state) => { - return state.listingsMap[state.category || '']?.find( - (l) => l.uuid === state.selectedListingId - ) - }) - ) - - const selectedItems = useListingsStore((state) => - state.selectedListingId ? state.selectedItemsMap[state.selectedListingId] : {} - ) - - const filteredItems = useMemo((): SageListingItemType[] => { - if (!listing) return [] - if (listing.meta.listingMode === 'bulk' || showItemsMode === 'Show all') return listing.items - return listing.items.filter((item) => { - const selected = !!selectedItems[item.hash] - if (showItemsMode === 'Show selected') return selected - return !selected - }) - }, [listing, showItemsMode, selectedItems]) - - const setSelectedItems = useListingsStore((state) => state.setSelectedItems) - const updateData = useListingsStore((state) => state.updateData) - - const [autoResetPageIndex, skipAutoResetPageIndex] = useSkipper() - - const table = useReactTable({ - data: filteredItems, - columns, - getRowId: (row) => row.hash, - enableColumnResizing: true, - enableMultiSort: listing?.meta.listingMode !== 'bulk', - autoResetPageIndex, - columnResizeMode: 'onChange', - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setSelectedItems, - globalFilterFn: globalFilterFn, - state: { - rowSelection: selectedItems, - globalFilter: globalFilter - }, - initialState: { - pagination: { - pageSize: 10 - }, - sorting: [ - ...(listing?.meta.listingMode === 'bulk' - ? [ - { - desc: true, - id: 'valuation' - } - ] - : [ - { - desc: true, - id: 'selected' - }, - { - desc: true, - id: 'valuation' - } - ]) - ] - }, - meta: { - // https://muhimasri.com/blogs/react-editable-table/ - updateData: (...params) => { - skipAutoResetPageIndex() - updateData(...params) - } - } - }) - - const columnSizeVars = React.useMemo(() => { - const headers = table.getFlatHeaders() - const colSizes: { [key: string]: number } = {} - for (let i = 0; i < headers.length; i++) { - const header = headers[i]! - colSizes[`--sub-header-${header.id}-size`] = header.getSize() - colSizes[`--sub-col-${header.column.id}-size`] = header.column.getSize() - } - return colSizes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [table.getState().columnSizing]) - - return ( -
-
- setGlobalFilter(String(value))} - onBlur={(value) => setGlobalFilter(String(value))} - className="pl-8 max-w-60" - placeholder={'Search ...'} - startIcon={ -
- -
- } - /> - {listing?.meta.listingMode === 'single' && ( -
- -
- )} -
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header, i) => { - const meta = header.column.columnDef.meta - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - {header.column.getCanResize() && ( -
- )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const meta = cell.column.columnDef.meta - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) - })} - - )) - ) : ( - - - No results. - - - )} - -
-
- -
- ) -} - -export default memo(ListingTable) diff --git a/src/green-app/src/app/listings/listings-table.tsx b/src/green-app/src/app/listings/listings-table.tsx deleted file mode 100644 index 52b00684..00000000 --- a/src/green-app/src/app/listings/listings-table.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/* eslint-disable no-extra-boolean-cast */ -'use client' - -import DebouncedInput from '@/components/debounced-input' -import { TablePagination } from '@/components/table-pagination' -import { Dialog, DialogContent } from '@/components/ui/dialog' -import { Label } from '@/components/ui/label' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from '@/components/ui/table' -import { ListingFilterUtil } from '@/lib/listing-filter-util' -import { cn } from '@/lib/utils' -import { SageListingType } from '@/types/sage-listing-type' -import { DialogPortal } from '@radix-ui/react-dialog' -import { MagnifyingGlassIcon } from '@radix-ui/react-icons' -import * as SliderPrimitive from '@radix-ui/react-slider' -import { - ColumnDef, - FilterFnOption, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable -} from '@tanstack/react-table' -import dayjs from 'dayjs' -import utc from 'dayjs/plugin/utc' -import React, { memo, useMemo, useState } from 'react' -import { useShallow } from 'zustand/react/shallow' -import ListingDialogContent from './listing-dialog-content' -import { useListingsStore } from './listingsStore' -import { atom, useAtom } from 'jotai' -import { BasicSelect } from '@/components/poe/BasicSelect' -dayjs.extend(utc) - -type SellModeOptions = 'Show all modes' | 'Show whole listings' | 'Show individual listings' -const showSellModeAtom = atom('Show all modes') - -interface DataTableProps { - className?: string - columns: ColumnDef[] - globalFilterFn?: FilterFnOption -} - -// Tutorial: https://ui.shadcn.com/docs/components/data-table -const ListingsTable = ({ columns, globalFilterFn, className }: DataTableProps) => { - // const { t } = useTranslation(); - const [showSellMode, setShowSellMode] = useAtom(showSellModeAtom) - const [dialogOpen, setDialogOpen] = useListingsStore( - useShallow((state) => [state.dialogOpen, state.setDialogOpen]) - ) - const [multiplierRange, setMultiplierRange] = useListingsStore( - useShallow((state) => [state.multiplierRange, state.setMultiplierRange]) - ) - - const [globalFilter, setGlobalFilter] = useState('') - const modifiedListings = useListingsStore( - useShallow((state) => { - if (!state.category) return [] - const now = dayjs.utc().valueOf() - return state.listingsMap[state.category].filter( - (l) => - l.meta.league === state.league && - state.filteredByGroupListings[l.uuid] && - now - l.meta.timestampMs < 30 * 60 * 1000 && - state.multiplierRange[0] <= l.meta.multiplier && - l.meta.multiplier <= state.multiplierRange[1] - ) - }) - ) - - const filteredListings = useMemo((): SageListingType[] => { - if (showSellMode === 'Show all modes') return modifiedListings - return modifiedListings.filter((l) => { - if (showSellMode === 'Show whole listings') return l.meta.listingMode === 'bulk' - else return l.meta.listingMode === 'single' - }) - }, [modifiedListings, showSellMode]) - - const table = useReactTable({ - data: filteredListings, - columns, - getRowId: (row) => row.uuid, - enableColumnResizing: true, - enableMultiSort: true, - columnResizeMode: 'onChange', - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onGlobalFilterChange: setGlobalFilter, - globalFilterFn: globalFilterFn, - state: { - globalFilter: globalFilter - }, - initialState: { - pagination: { - pageSize: 25 - }, - sorting: [ - { - desc: true, - id: 'created' - }, - { - desc: false, - id: 'multiplier' - } - ] - } - }) - - const columnSizeVars = React.useMemo(() => { - const headers = table.getFlatHeaders() - const colSizes: { [key: string]: number } = {} - for (let i = 0; i < headers.length; i++) { - const header = headers[i]! - colSizes[`--header-${header.id}-size`] = header.getSize() - colSizes[`--col-${header.column.id}-size`] = header.column.getSize() - } - return colSizes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [table.getState().columnSizing]) - - return ( -
- -
- setGlobalFilter(String(value))} - onBlur={(value) => setGlobalFilter(String(value))} - className="pl-8 max-w-48" - placeholder={'Search ...'} - startIcon={ -
- -
- } - /> -
- -
-
- - setMultiplierRange(e)} - > - - - - - - -
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header, i) => { - const meta = header.column.columnDef.meta - return ( - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - {header.column.getCanResize() && ( -
- )} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length > 0 ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const meta = cell.column.columnDef.meta - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) - })} - - )) - ) : ( - - - No results. - - - )} - -
-
- - - e.preventDefault()} - > - - - -
-
- ) -} - -export default memo(ListingsTable) diff --git a/src/green-app/src/assets/poestack.png b/src/green-app/src/assets/poestack.png new file mode 100644 index 00000000..bd5bf91c Binary files /dev/null and b/src/green-app/src/assets/poestack.png differ diff --git a/src/green-app/src/assets/toastify.css b/src/green-app/src/assets/toastify.css new file mode 100644 index 00000000..c96a2091 --- /dev/null +++ b/src/green-app/src/assets/toastify.css @@ -0,0 +1,55 @@ + +:root { + --toastify-color-light: #fff; + --toastify-color-dark: hsl(var(--background)); + --toastify-color-info: hsl(var(--foreground)); + --toastify-color-success: #07bc0c; + --toastify-color-warning: #f1c40f; + --toastify-color-error: #e74c3c; + --toastify-color-transparent: rgba(255, 255, 255, 0.7); + + --toastify-icon-color-info: var(--toastify-color-info); + --toastify-icon-color-success: var(--toastify-color-success); + --toastify-icon-color-warning: var(--toastify-color-warning); + --toastify-icon-color-error: var(--toastify-color-error); + + --toastify-toast-width: 320px; + --toastify-toast-background: #fff; + --toastify-toast-min-height: 64px; + --toastify-toast-max-height: 800px; + --toastify-font-family: sans-serif; + --toastify-z-index: 60; + + --toastify-text-color-light: #757575; + --toastify-text-color-dark: hsl(var(--foreground)); + + /* //Used only for colored theme */ + --toastify-text-color-info: hsl(var(--foreground)); + --toastify-text-color-success: hsl(var(--foreground)); + --toastify-text-color-warning: hsl(var(--foreground)); + --toastify-text-color-error: hsl(var(--foreground)); + + --toastify-spinner-color: #616161; + --toastify-spinner-color-empty-area: hsl(var(--foreground)); + + /* // Used when no type is provided + // toast("**hello**") */ + --toastify-color-progress-light: linear-gradient( + to right, + #4cd964, + #5ac8fa, + #007aff, + #34aadc, + #5856d6, + #ff2d55 + ); + /* // Used when no type is provided */ + --toastify-color-progress-dark: #bb86fc; + --toastify-color-progress-info: var(--toastify-color-info); + --toastify-color-progress-success: var(--toastify-color-success); + --toastify-color-progress-warning: var(--toastify-color-warning); + --toastify-color-progress-error: var(--toastify-color-error); + + /* // used to control the opacity of the progress trail */ + --toastify-color-progress-bgo: .2; + } \ No newline at end of file diff --git a/src/green-app/src/components/poe/BasicSelect.tsx b/src/green-app/src/components/basic-select.tsx similarity index 53% rename from src/green-app/src/components/poe/BasicSelect.tsx rename to src/green-app/src/components/basic-select.tsx index 51e23e69..45aae46e 100644 --- a/src/green-app/src/components/poe/BasicSelect.tsx +++ b/src/green-app/src/components/basic-select.tsx @@ -7,8 +7,9 @@ import { } from '@/components/ui/select' import { cn } from '@/lib/utils' import { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' -type BasicSelectProps = { +type BasicSelectProps = { options: TData[] | ReactNode onSelect: (select: TValue) => void defaultOption?: string | undefined @@ -17,9 +18,10 @@ type BasicSelectProps = { defaultOpen?: boolean value?: string itemClassName?: string + translate?: boolean } -export function BasicSelect({ +export function BasicSelect({ open, value, onSelect, @@ -27,8 +29,10 @@ export function BasicSelect({ defaultOption, defaultOpen, onOpenChange, - itemClassName + itemClassName, + translate }: BasicSelectProps) { + const { t } = useTranslation() return ( - + {data?.map((c) => ( diff --git a/src/green-app/src/components/listing-category-select.tsx b/src/green-app/src/components/listing-category-select.tsx new file mode 100644 index 00000000..538ad446 --- /dev/null +++ b/src/green-app/src/components/listing-category-select.tsx @@ -0,0 +1,207 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import { LISTING_CATEGORIES, ListingCategory, ListingSubCategory } from '@/lib/listing-categories' +import { cn } from '@/lib/utils' +import Image from 'next/image' +import { useMemo, useState } from 'react' +import { ComboboxItem, ComboboxTrigger } from './ui/combobox' +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandList } from './ui/command' +import { Popover, PopoverContent, PopoverTrigger } from './ui/popover' +import { useTranslation } from 'react-i18next' + +type SelectableCategory = ListingSubCategory & { + selectable: boolean +} + +type OnSelectProps = { + className?: string + selectableCategories?: ListingCategory[] + selectableSubCategories?: ListingSubCategory[] + category: string | null + subCategory: string | null + isSubCategory: boolean + control: 'select' | 'combobox' + onCategorySelect: (category: string | null) => void + onSubCategorySelect: (category: string | null) => void +} + +export function ListingCategorySelect({ + className, + selectableCategories, + selectableSubCategories, + isSubCategory, + category, + subCategory, + control, + onCategorySelect, + onSubCategorySelect +}: OnSelectProps) { + const { t } = useTranslation() + const [popoverOpen, setPopoverOpen] = useState(false) + + const categoryItem = useMemo(() => { + return LISTING_CATEGORIES.find((c) => c.name === category) + }, [category]) + + const categories = useMemo(() => { + let extSelectableCategories: SelectableCategory[] = [] + if (isSubCategory) { + if (categoryItem) { + extSelectableCategories = categoryItem.subCategories + .map((subCat) => { + return { + selectable: + !selectableSubCategories || + selectableSubCategories?.some((selsubCat) => selsubCat.name === subCat.name), + ...subCat + } + }) + .sort((a, b) => { + if (a.selectable && b.selectable) return a.name.localeCompare(b.name) + if (!a.selectable && !b.selectable) return a.name.localeCompare(b.name) + if (a.selectable && !b.selectable) return -1 + return 1 + }) + } + } else { + extSelectableCategories = LISTING_CATEGORIES.map((category) => { + return { + selectable: + !selectableCategories || selectableCategories?.some((c) => c.name === category.name), + ...category + } + }).sort((a, b) => { + if (a.selectable && b.selectable) return a.name.localeCompare(b.name) + if (!a.selectable && !b.selectable) return a.name.localeCompare(b.name) + if (a.selectable && !b.selectable) return -1 + return 1 + }) + } + extSelectableCategories.unshift({ + name: '...', + tags: [], + icon: '', + selectable: true + }) + return extSelectableCategories + }, [isSubCategory, categoryItem, selectableCategories, selectableSubCategories]) + + const selectedCategory = isSubCategory ? subCategory : category + const selectedCategoryItem = useMemo(() => { + return isSubCategory + ? categoryItem?.subCategories.find((c) => c.name === subCategory) + : categoryItem + }, [categoryItem, isSubCategory, subCategory]) + + const handleCategorySelect = (value: string | null) => { + if (isSubCategory ? value === subCategory : value === category) return + if (isSubCategory) { + onSubCategorySelect(value) + } else { + onCategorySelect(value) + onSubCategorySelect(null) + } + } + + if (isSubCategory && categories.length === 1) { + return null + } + + return ( +
+ {control === 'select' ? ( + + ) : ( + + + + {selectedCategory ? ( +
+ {selectedCategoryItem && ( + {selectedCategoryItem.name} + )} +
{t(`categories.${selectedCategory}` as any)}
+
+ ) : ( + <>{isSubCategory ? t('label.selectSubCategoryPh') : t('label.selectCategoryPh')} + )} +
+
+ + + + {t('label.noResults')} + + + {categories?.map((c) => ( + { + setPopoverOpen(false) + handleCategorySelect(value !== '...' ? value : null) + }} + selected={c.name === selectedCategory} + > + {c.name === '...' ? ( +
{c.name}
+ ) : ( +
+ {c.name} +
{t(`categories.${c.name}` as any)}
+
+ )} +
+ ))} +
+
+
+
+
+ )} +
+ ) +} diff --git a/src/green-app/src/components/navbar.tsx b/src/green-app/src/components/navbar.tsx new file mode 100644 index 00000000..4ed07f16 --- /dev/null +++ b/src/green-app/src/components/navbar.tsx @@ -0,0 +1,25 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { Link } from './ui/link' +import { useTranslation } from 'react-i18next' + +const Navbar = () => { + const { t } = useTranslation() + const pathname = usePathname() + + return ( + <> + + + ) +} + +export default Navbar diff --git a/src/green-app/src/components/notificationCenter/notification-center.tsx b/src/green-app/src/components/notificationCenter/notification-center.tsx new file mode 100644 index 00000000..cba392bb --- /dev/null +++ b/src/green-app/src/components/notificationCenter/notification-center.tsx @@ -0,0 +1,367 @@ +'use client' + +import { cn } from '@/lib/utils' +import { Notification, useNotificationStore } from '@/store/notificationStore' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' +import { AnimatePresence, motion } from 'framer-motion' +import { ArchiveIcon, BellIcon, CheckIcon, PackageSearchIcon, SettingsIcon } from 'lucide-react' +import Link from 'next/link' +import { ReactNode, useState } from 'react' +import { Icons, Id } from 'react-toastify' +import { useShallow } from 'zustand/react/shallow' +import { ToastData } from '../notifier' +import { TimeTracker } from '../time-tracker' +import { Badge } from '../ui/badge' +import { Button } from '../ui/button' +import { Dialog, DialogContent, DialogTrigger } from '../ui/dialog' +import { Label } from '../ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' +import { Switch } from '../ui/switch' +import PulsatingDot from './pulsating-dot' +import SettingsDialogContent from './settings-dialog-content' +import { useTranslation } from 'react-i18next' +dayjs.extend(relativeTime) + +const variants = { + // used to stagger item animation when switching from closed to open and vice versa + content: { + open: { + transition: { staggerChildren: 0.07, delayChildren: 0.2 } + }, + closed: { + transition: { staggerChildren: 0.05, staggerDirection: -1 } + } + }, + item: { + open: { + y: 0, + opacity: 1, + transition: { + y: { stiffness: 1000, velocity: -100 } + } + }, + closed: { + y: 50, + opacity: 0, + transition: { + y: { stiffness: 1000 } + } + } + } +} + +const NotificationCenter = () => { + const { t } = useTranslation() + const { notifications, clear, markAllAsRead, markAsRead, remove, dismissAll, blockDisplay } = + useNotificationStore( + useShallow((state) => ({ + notifications: state.notifications, + clear: state.clear, + markAllAsRead: state.markAllAsRead, + markAsRead: state.markAsRead, + remove: state.remove, + dismissAll: state.dismissAll, + blockDisplay: state.blockDisplay + })) + ) + + const unreadCount = useNotificationStore((state) => { + return state.notifications.filter((n) => { + if (state.toastsToDisplay.some((ttd) => ttd === n.id)) return false + return !n.read + }).length + }) + + const [showUnreadOnly, setShowUnreadOnly] = useState(true) + const [settingsDialogOpen, setSettingsDialogOpen] = useState(false) + const [popoverOpen, setPopoverOpen] = useState(false) + + const handlePopoverOpenChange = (open: boolean) => { + if (open) { + // Race conditions - dismiss has priority! + setTimeout(() => dismissAll(true), 0) + } else { + blockDisplay(false) + } + setPopoverOpen(open) + } + + return ( + + + + + + +
+
+

+ {t('title.notifications')} +

+
+
setShowUnreadOnly((prev) => !prev)} + > + + +
+ + + +
+
+ + + {(!notifications.length || (unreadCount === 0 && showUnreadOnly)) && ( +

{t('label.noNotificationResults')}

+ )} + + {(showUnreadOnly ? notifications.filter((v) => !v.read) : notifications).map( + (notification) => { + if ( + notification.options?.data && + 'type' in notification.options.data && + notification.options.data.type === 'offering-buy' + ) { + return ( + + ) + } else { + return ( + + ) + } + } + )} + +
+
+
+ + +
+
+
+
+ + setSettingsDialogOpen(false)} /> + +
+ ) +} + +type ListingNotificationItemProps = { + notification: Notification + toastData: ToastData + showUnreadOnly: boolean + markAsRead: (id: Id | Id[]) => void + remove: (id: Id | Id[]) => void +} + +const ListingNotificationItem = ({ + notification, + toastData, + showUnreadOnly, + markAsRead, + remove +}: ListingNotificationItemProps) => { + const openTradeOverviewInNewWindow = useNotificationStore( + (state) => state.openTradeOverviewInNewWindow + ) + + const actionReadButton = notification.read ? ( +
+ +
+ ) : ( + + ) + + return ( + + +
+
+
+ {Icons.info({ + theme: 'dark', + type: 'info' + })} +
+
+ {toastData.toastBody} + +
+
+ {showUnreadOnly ? ( +
+ {actionReadButton} + +
+ ) : ( +
+ +
+ {actionReadButton} + +
+
+ )} +
+
+
+ ) +} + +type BasicNotificationItemProps = { + notification: Notification + showUnreadOnly: boolean + markAsRead: (id: Id | Id[]) => void + remove: (id: Id | Id[]) => void +} + +const BasicNotificationItem = ({ + notification, + showUnreadOnly, + markAsRead, + remove +}: BasicNotificationItemProps) => { + return ( + + +
+
+
+ {notification.type === 'info' + ? Icons.info({ + theme: 'dark', + type: 'info' + }) + : notification.type === 'success' + ? Icons.success({ + theme: 'dark', + type: 'success' + }) + : notification.type === 'warning' + ? Icons.warning({ + theme: 'dark', + type: 'warning' + }) + : notification.type === 'error' + ? Icons.error({ + theme: 'dark', + type: 'error' + }) + : Icons.info({ + theme: 'dark', + type: 'default' + })} +
+
+ {notification.content as ReactNode} + +
+
+
+ {notification.read ? ( +
+ +
+ ) : ( + + )} + {!showUnreadOnly && ( + + )} +
+
+
+
+ ) +} + +export default NotificationCenter diff --git a/src/green-app/src/components/notificationCenter/pulsating-dot.tsx b/src/green-app/src/components/notificationCenter/pulsating-dot.tsx new file mode 100644 index 00000000..e6eff437 --- /dev/null +++ b/src/green-app/src/components/notificationCenter/pulsating-dot.tsx @@ -0,0 +1,18 @@ +const PulsatingDot = () => { + return ( +
+
+
+ ) +} + +export default PulsatingDot diff --git a/src/green-app/src/components/notificationCenter/settings-dialog-content.tsx b/src/green-app/src/components/notificationCenter/settings-dialog-content.tsx new file mode 100644 index 00000000..47cc6479 --- /dev/null +++ b/src/green-app/src/components/notificationCenter/settings-dialog-content.tsx @@ -0,0 +1,122 @@ +'use client' + +import { useNotificationStore } from '@/store/notificationStore' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { useShallow } from 'zustand/react/shallow' +import { Button } from '../ui/button' +import { DialogClose, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '../ui/form' +import { Input } from '../ui/input' +import { Switch } from '../ui/switch' +import { useTranslation } from 'react-i18next' + +const formSchema = z.object({ + maxActiveToasts: z.coerce.number().min(0), + openTradeOverviewInNewWindow: z.boolean() +}) + +type SettingsDialogContentProps = { + onClose: () => void +} + +const SettingsDialogContent = ({ onClose }: SettingsDialogContentProps) => { + const { t } = useTranslation() + const { + maxActiveToasts, + setMaxActiveToasts, + openTradeOverviewInNewWindow, + setOpenTradeOverviewInNewWindow + } = useNotificationStore( + useShallow((state) => ({ + maxActiveToasts: state.maxActiveToasts, + setMaxActiveToasts: state.setMaxActiveToasts, + openTradeOverviewInNewWindow: state.openTradeOverviewInNewWindow, + setOpenTradeOverviewInNewWindow: state.setOpenTradeOverviewInNewWindow + })) + ) + + const form = useForm>({ + resolver: zodResolver(formSchema), + reValidateMode: 'onChange', + mode: 'all', + delayError: 500, // Do not show error when canceled + defaultValues: { + maxActiveToasts, + openTradeOverviewInNewWindow + } + }) + + function onSubmit(values: z.infer) { + // ✅ This will be type-safe and validated. + setMaxActiveToasts(values.maxActiveToasts) + setOpenTradeOverviewInNewWindow(values.openTradeOverviewInNewWindow) + onClose() + } + + return ( + <> +
+ + + {t('title.notificationSettings')} + +
+ ( + + {t('label.maxNotification')} + + + + {t('body.maxNotification')} + + + )} + /> + ( + + {t('label.tradeInNewWindow')} + + + + {t('body.tradeInNewWindow')} + + + )} + /> +
+ + + + + + +
+ + + ) +} + +export default SettingsDialogContent diff --git a/src/green-app/src/components/notifier.tsx b/src/green-app/src/components/notifier.tsx index 7fa9c512..6b8b51de 100644 --- a/src/green-app/src/components/notifier.tsx +++ b/src/green-app/src/components/notifier.tsx @@ -1,80 +1,94 @@ 'use client' import { currentUserAtom } from '@/components/providers' -import { NotificationBody, NotificationItem } from '@/hooks/useWhisperHashCopied' +import { useToast } from '@/hooks/useToast' +import { NotificationBody, NotificationItem } from '@/hooks/useWhisperHash' import { + Notification, listMyListings, listNotifications, listSummaries, - listValuations, - Notification + listValuations } from '@/lib/http-util' import { LISTING_CATEGORIES } from '@/lib/listing-categories' +import { calculateListingFromOfferingListing } from '@/lib/listing-util' +import { useNotificationStore } from '@/store/notificationStore' import { SageItemGroupSummaryShard } from '@/types/echo-api/item-group' import { SageValuationShard } from '@/types/echo-api/valuation' -import { SageNotificationListingType, SageOfferingType } from '@/types/sage-listing-type' +import { + SageListingType, + SageOfferingType, + SageSelectedDatabaseOfferingItemType +} from '@/types/sage-listing-type' import { useQueries, useQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { useAtomValue } from 'jotai' -import { memo, useEffect, useMemo, useRef, useState } from 'react' -import { toast } from 'react-toastify' +import { PackageSearchIcon } from 'lucide-react' +import Image from 'next/image' +import Link from 'next/link' +import { ReactNode, memo, useEffect, useMemo, useRef, useState } from 'react' +import CurrencyDisplay from './currency-display' +import { Button } from './ui/button' +import { Tooltip, TooltipProvider, TooltipTrigger } from './ui/tooltip' +import { useTranslation } from 'react-i18next' type ParsedNotification = Omit & { - body: string | SageOfferingType - requestedItems?: NotificationItem[] + body: string | { listing: SageOfferingType; requestedItems?: NotificationItem[]; ign: string } +} + +export type ToastData = { + listing: SageListingType + created: number + type: string + buyer: string + ign: string + toastBody: ReactNode } interface NotificationHandlerProps {} // Tutorial: https://ui.shadcn.com/docs/components/data-table const Notifier = () => { - const [fetchTimeStamp, setFetchTimestamp] = useState(0) + const { t } = useTranslation(['common', 'notification']) + const openTradeOverviewInNewWindow = useNotificationStore( + (state) => state.openTradeOverviewInNewWindow + ) + + const [fetchTimeStamp, setFetchTimestamp] = useState(dayjs.utc().valueOf() - 2000) const currentUser = useAtomValue(currentUserAtom) const listedNotifications = useRef>({}) + const toast = useToast() - const { data: notifications, isError } = useQuery({ + const { data: notifications } = useQuery({ // eslint-disable-next-line @tanstack/query/exhaustive-deps - queryKey: ['notifications', fetchTimeStamp], - queryFn: () => listNotifications(fetchTimeStamp), + queryKey: ['notifications'], + queryFn: async () => { + const notifications = await listNotifications(fetchTimeStamp) + const nextMs = dayjs.utc().valueOf() + setFetchTimestamp(nextMs - 2000) + return notifications + }, // We do not save any cache - this has the effect, that the query starts directly after changing the category gcTime: 0, enabled: !!currentUser, - refetchOnWindowFocus: false, + refetchInterval: 2000, retry: true }) - const { data: allListings, isLoading } = useQuery({ - queryKey: ['my-listings'], - queryFn: () => listMyListings().then((res) => res.filter((l) => !l.deleted)) + const { data: allListings } = useQuery({ + queryKey: [currentUser?.profile?.uuid, 'my-listings'], + queryFn: () => listMyListings().then((res) => res.filter((l) => !l.deleted)), + enabled: !!currentUser?.profile?.uuid }) - const enabled = useRef(isError) - enabled.current = isError || !currentUser - - useEffect(() => { - const interval = setInterval(() => { - if (enabled.current) { - // We do not start the next request until the first is finished - console.warn('Skip request for next timestamp') - return - } - const nextMs = dayjs.utc().valueOf() - setFetchTimestamp(nextMs - 2000) // Request the data 2 sec ago - }, 2000) - - return () => clearInterval(interval) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - const parsedNotifications = useMemo(() => { return notifications?.notifications.map((n): ParsedNotification => { - console.log('Notification received: ', n) if (n.type === 'offering-buy') { try { const body: NotificationBody = JSON.parse(n.body) const listing = allListings?.find((l) => l.uuid === body.uuid) if (listing) { - return { ...n, body: listing, requestedItems: body.items } + return { ...n, body: { listing, requestedItems: body.items, ign: body.ign } } } } catch (error) { console.error('Notification body could not be parsed', error) @@ -89,11 +103,11 @@ const Notifier = () => { const leagues: Record = {} parsedNotifications?.forEach((n) => { if (typeof n.body === 'object') { - const category = n.body.meta.category + const category = n.body.listing.meta.category const categoryTagItem = LISTING_CATEGORIES.find((ca) => ca.name === category) categoryTagItem?.tags.forEach((t) => (tags[t] = true)) - leagues[n.body.meta.league] = true + leagues[n.body.listing.meta.league] = true } }) return [Object.keys(tags), Object.keys(leagues)] @@ -144,18 +158,15 @@ const Notifier = () => { } }) ) - .flatMap((x) => x), + .flat(), combine: (valuationResults) => { - // TODO: How to handle multiple leagues??? const valuationShards = valuationResults .filter((x) => x.data && !x.isPending) .map((x) => x.data!) - let valuations: SageValuationShard['valuations'] = {} + const valuations: Record = {} valuationShards.forEach((e) => { - // TODO: Distinct between leagues - // e.meta.league - valuations = { ...valuations, ...e.valuations } + valuations[e.meta.league] = { ...valuations[e.meta.league], ...e.valuations } }) const isValuationError = valuationResults.some((result) => result.isError) @@ -171,9 +182,11 @@ const Notifier = () => { } }) + const startCalculation = valuations !== undefined && summaries !== undefined + useEffect(() => { - // TODO: parsedNotifications?.forEach((n) => { + if (!summaries || !valuations) return if (listedNotifications.current[n.id]) return listedNotifications.current[n.id] = true @@ -181,48 +194,141 @@ const Notifier = () => { console.warn('Notification type not supported', n) return } - if (typeof n.body !== 'object' || !n.requestedItems) { + if (typeof n.body !== 'object' || !n.body.requestedItems) { console.warn('Notification listing not found', n) return } console.log('Notification received: ', n) let validNotification = true - const offering = n.body - const requestedItems = n.requestedItems + const { listing: offering, requestedItems, ign } = n.body - // TODO: Rebuild the listing - let listing: SageNotificationListingType - - if (n.body.items.length > 0) { + let listing: SageListingType | undefined + if (requestedItems.length > 0) { // Single items - const reqItems = requestedItems.map(([hash, selectedQuantity]) => { - const item = offering?.items.find((item) => item.hash === hash) - if (!item) { - validNotification = false - } - return { - ...item, - selectedQuantity: selectedQuantity - } - }) + const reqItems = requestedItems + .map(([hash, selectedQuantity]): SageSelectedDatabaseOfferingItemType | undefined => { + const item = offering?.items.find((item) => item.hash === hash) + if (!item) { + validNotification = false + return undefined + } + return { + ...item, + selectedQuantity: selectedQuantity + } + }) + .filter((item) => !!item) as SageSelectedDatabaseOfferingItemType[] + + if (validNotification) { + listing = calculateListingFromOfferingListing( + { ...offering, items: reqItems }, + summaries, + valuations[offering.meta.league] + ) + } } else { - // All items + listing = calculateListingFromOfferingListing( + offering, + summaries, + valuations[offering.meta.league] + ) } - // TODO: Generate a short whisper message. Username - Category - Total Value -> Button to show the details - if (validNotification) { - toast.info('', { - toastId: n.id, - autoClose: 10000 - }) + if (validNotification && listing) { + let description: ReactNode + if (requestedItems.length === 0) { + description = t('body.wtbAll', { + category: t(`categories.${listing.meta.subCategory || listing.meta.category}` as any) + }) + } else { + const totalItems = listing.items.reduce((sum, item) => item.selectedQuantity + sum, 0) + description = t('body.wtbPartial', { + count: totalItems, + category: t(`categories.${listing.meta.subCategory || listing.meta.category}` as any) + }) + } + + const toastBody = ( +
+
+ + + @ + + + {ign} + + {description} + + {/* TODO: Show user stats */} + {/* {`User: ${ign}`} */} + + +
+
+ {listing.meta.altIcon} +
{t('label.total')}
+ +
+
+ ) + + const toastData: ToastData = { + listing, + created: n.timestamp, + type: n.type, + buyer: n.senderId, + ign, + toastBody + } + + toast( +
+ {toastBody} + +
, + 'info', + { + toastId: n.id, + data: toastData + } + ) } else { - console.warn('The received notification body is invalid. Not all items found') + toast( + t('notification:warning.description.tradeRequestNotValid', { ign: n.body.ign }), + 'warning', + { + toastId: n.id, + data: listing + } + ) + console.warn(`Be careful ${n.body.ign} wants to buy items which are not offered ...`) } - - // TODO: List username + vouchrank + short message what he wants to buy }) - }, [parsedNotifications]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [startCalculation, parsedNotifications]) return null } diff --git a/src/green-app/src/components/poe/ListingCategorySelect.tsx b/src/green-app/src/components/poe/ListingCategorySelect.tsx deleted file mode 100644 index 694118e0..00000000 --- a/src/green-app/src/components/poe/ListingCategorySelect.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select' -import { LISTING_CATEGORIES, ListingCategory } from '@/lib/listing-categories' -import { cn } from '@/lib/utils' -import Image from 'next/image' -import { useMemo } from 'react' - -type OnSelectProps = { - className?: string - selectableCategories?: ListingCategory[] - category: string | null - onCategorySelect: (category: string | null) => void -} - -export function ListingCategorySelect({ - className, - selectableCategories, - category, - onCategorySelect -}: OnSelectProps) { - const categories = useMemo(() => { - const categories = Object.values(LISTING_CATEGORIES) - .map((category) => { - return { - selectable: - !selectableCategories || selectableCategories?.some((c) => c.name === category.name), - ...category - } - }) - .sort((a, b) => { - if (a.selectable && b.selectable) return a.name.localeCompare(b.name) - if (!a.selectable && !b.selectable) return a.name.localeCompare(b.name) - if (a.selectable && !b.selectable) return -1 - return 1 - }) - categories.unshift({ - name: '...', - tags: [], - icon: '', - selectable: true - }) - return categories - }, [selectableCategories]) - - return ( -
- -
- ) -} diff --git a/src/green-app/src/components/profile-menu.tsx b/src/green-app/src/components/profile-menu.tsx index 553a97bb..dd1cf229 100644 --- a/src/green-app/src/components/profile-menu.tsx +++ b/src/green-app/src/components/profile-menu.tsx @@ -1,25 +1,15 @@ 'use client' import { Button } from '@/components/ui/button' -import { useRouter } from 'next/navigation' -import { useEffect, useState } from 'react' -import { currentUserAtom } from './providers' -import { jwtDecode } from 'jwt-decode' -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger -} from './ui/dropdown-menu' -import { useAtom } from 'jotai' import { SUPPORTED_LEAGUES } from '@/lib/constants' +import { useNotificationStore } from '@/store/notificationStore' +import { useAtom } from 'jotai' +import { jwtDecode } from 'jwt-decode' import { UserRoundIcon } from 'lucide-react' -import Link from 'next/link' -import { useListingsStore } from '@/app/listings/listingsStore' -import { useListingToolStore } from '@/app/listing-tool/listingToolStore' +import { usePathname, useRouter } from 'next/navigation' +import { ChangeEvent, useEffect, useState } from 'react' import { useShallow } from 'zustand/react/shallow' +import NotificationCenter from './notificationCenter/notification-center' +import { currentUserAtom } from './providers' import { AlertDialog, AlertDialogAction, @@ -31,71 +21,127 @@ import { AlertDialogTitle, AlertDialogTrigger } from './ui/alert-dialog' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger +} from './ui/dropdown-menu' +import { Link } from './ui/link' +import { useListingsStore } from '@/app/[locale]/listings/listingsStore' +import { useListingToolStore } from '@/app/[locale]/listing-tool/listingToolStore' +import { useTranslation } from 'react-i18next' +import { i18nConfig } from '../config/i18n.config' export function ProfileMenu() { + const { t, i18n } = useTranslation() + const currentLocale = i18n.language const router = useRouter() + const currentPathname = usePathname() + + const [hydrated, setHydrated] = useState(false) - const [selectedLeague, setListingsLeague] = useListingsStore( + const [open, setOpen] = useState(false) + + const [listingsLeague, setListingsLeague] = useListingsStore( + useShallow((state) => [state.league, state.setLeague]) + ) + const [listingToolLeague, setListingToolLeague] = useListingToolStore( useShallow((state) => [state.league, state.setLeague]) ) - const setListingToolLeague = useListingToolStore((state) => state.setLeague) const resetListinsStore = useListingsStore((state) => state.reset) const resetListingToolStore = useListingToolStore((state) => state.reset) + const [dismissAll, blockDisplay] = useNotificationStore( + useShallow((state) => [state.dismissAll, state.blockDisplay]) + ) + const [currentUser, setCurrentUser] = useAtom(currentUserAtom) useEffect(() => { - // TODO: Test this - if (!SUPPORTED_LEAGUES.includes(selectedLeague)) { - // New league - reset all + // New league - reset all + if (!SUPPORTED_LEAGUES.includes(listingsLeague)) { console.warn('Attention! Stores going to be resetted!') resetListinsStore() + } + if (!SUPPORTED_LEAGUES.includes(listingToolLeague)) { + console.warn('Attention! Stores going to be resetted!') resetListingToolStore() } - }, [resetListingToolStore, resetListinsStore, selectedLeague]) + }, [resetListingToolStore, resetListinsStore, listingsLeague, listingToolLeague]) useEffect(() => { const jwt = localStorage.getItem('doNotShareJwt') if (jwt) { setCurrentUser(jwtDecode(jwt)) } + setHydrated(true) }, [setCurrentUser]) + const handleLanguageChange = (newLocale: string) => { + // set cookie for next-i18n-router + const days = 30 + const date = new Date() + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000) + document.cookie = `NEXT_LOCALE=${newLocale};expires=${date.toUTCString()};path=/` + + // redirect to the new locale path + if (currentLocale === i18nConfig.defaultLocale && !i18nConfig.prefixDefault) { + router.push('/' + newLocale + currentPathname) + } else { + router.push(currentPathname.replace(`/${currentLocale}`, `/${newLocale}`)) + } + + router.refresh() + } + + if (!hydrated) return null + if (currentUser?.profile?.name) { return ( <> + + {t('action.poeTradeDiscord')} + + - - + { + if (open) { + // Race conditions - dismiss has priority! + setTimeout(() => dismissAll(true), 0) + } else { + blockDisplay(false) + } + setOpen(open) + }} + > - {/* */} - My Account + {t('label.account')} - Discord + {t('label.discordTT')} - Link Discord + {t('action.linkDiscord')} - League + {t('label.league')} {SUPPORTED_LEAGUES.map((league) => ( { setListingsLeague(league) setListingToolLeague(league) @@ -133,14 +179,31 @@ export function ProfileMenu() { ))} - + + + {t('label.language')} + + + {i18nConfig.locales.map((l) => ( + handleLanguageChange(l)} + > + {t(`locales.${l}` as any)} + + ))} + + + + + - Hard Reset + {t('label.hardReset')} {/* ⇧⌘Q */} - { @@ -148,28 +211,25 @@ export function ProfileMenu() { setCurrentUser(null) }} > - Log out + {t('action.logout')} {/* ⇧⌘Q */} - Are you absolutely sure? - - If you confirm, all dates will be reset. This can help with bugs. To fix the bug, it - would help to report it. Thanks! - + {t('title.alertDialogQuesting')} + {t('body.hardReset')} - Cancel + {t('action.cancel')} { resetListinsStore() resetListingToolStore() }} > - Reset Data + {t('action.hardReset')} @@ -179,7 +239,7 @@ export function ProfileMenu() { } return ( - <> +
- +
) } diff --git a/src/green-app/src/components/providers.tsx b/src/green-app/src/components/providers.tsx index aa2bc969..013bfca3 100644 --- a/src/green-app/src/components/providers.tsx +++ b/src/green-app/src/components/providers.tsx @@ -1,38 +1,22 @@ 'use client' -import { useDivinePrice } from '@/hooks/useDivinePrice' +import { useToast } from '@/hooks/useToast' import { UserInfo } from '@/types/userInfo' -import { QueryClient } from '@tanstack/react-query' +import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' +import { QueryCache, QueryClient } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { Provider, atom, createStore } from 'jotai' import { ReactNode, useState } from 'react' -import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' -import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' -import { ToastContainer, toast } from 'react-toastify' +import { ToastContainer } from 'react-toastify' +import ErrorBoundaryContainer from './error-boundary-container' import Notifier from './notifier' - -// type DivineLeagues = { -// divinePrice: Record -// league: string | null -// setDivinePrice: (divinePrice: number, league: string) => void -// } - -// export const useDivineStore = create((set) => ({ -// divinePrice: {}, -// league: null, -// setDivinePrice: (divinePrice, league) => -// set((state) => ({ divinePrice: { ...state.divinePrice, [league]: divinePrice } })) -// })) +import { useTranslation } from 'react-i18next' const leagueDivineStore = createStore() export const currentDivinePriceAtom = atom(0) leagueDivineStore.set(currentDivinePriceAtom, 0) -// TODO: Remove later -const unsub = leagueDivineStore.sub(currentDivinePriceAtom, () => { - console.log('divinePrice value is changed to', leagueDivineStore.get(currentDivinePriceAtom)) -}) - export const currentUserAtom = atom(null) export const atomStore = createStore() @@ -42,7 +26,23 @@ type ProvidersProps = { } export function Providers({ children }: ProvidersProps) { - const [queryClient] = useState(() => new QueryClient()) + const { t } = useTranslation('notification') + const toast = useToast() + const [queryClient] = useState( + () => + new QueryClient({ + queryCache: new QueryCache({ + onError: (error, query) => { + // 🎉 only show error toasts if we already have data in the cache + // which indicates a failed background update + console.error(error) + if (query.state.data !== undefined) { + toast(t('error.unknown_error', { message: error.message }), 'error') + } + } + }) + }) + ) const [persister] = useState(() => createSyncStoragePersister({ @@ -53,8 +53,12 @@ export function Providers({ children }: ProvidersProps) { return ( - {children} - + {process.env.NODE_ENV !== 'production' ? ( + children + ) : ( + {children} + )} + @@ -62,19 +66,13 @@ export function Providers({ children }: ProvidersProps) { ) } -// export const DivineProvider = ({ -// children, -// league -// }: ProvidersProps & { league?: string | null }) => { -// return ( -// -// {league ? {children} : children} -// -// ) -// } - -// export const DivineSetter = ({ children, league }: ProvidersProps & { league: string | null }) => { -// useDivinePrice(league) - -// return children -// } +const ToastProvider = () => { + return ( + + ) +} diff --git a/src/green-app/src/components/stash-select.tsx b/src/green-app/src/components/stash-select.tsx index e88c2ddf..6975ad60 100644 --- a/src/green-app/src/components/stash-select.tsx +++ b/src/green-app/src/components/stash-select.tsx @@ -25,6 +25,9 @@ import { Popover, PopoverContent, PopoverTrigger } from './ui/popover' import { Separator } from './ui/separator' import { Skeleton } from './ui/skeleton' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip' +import { TimeTracker } from './time-tracker' +import { UserInfo } from '@/types/userInfo' +import { useTranslation } from 'react-i18next' dayjs.extend(relativeTime) // Inspiration: @@ -49,6 +52,7 @@ const StashSelect = ({ onSelect, onLoadStashTabsClicked }: StashSelectProps) => { + const { t } = useTranslation() const currentUser = useAtomValue(currentUserAtom) const { @@ -57,7 +61,7 @@ const StashSelect = ({ isLoading: isStashListLoading, refetch } = useQuery({ - queryKey: ['stashes', league], + queryKey: [currentUser?.profile?.uuid, 'stashes', league], queryFn: () => { if (!league) return [] as IStashTab[] return listStashes(league) @@ -91,7 +95,7 @@ const StashSelect = ({ }} >
- +
- No results. + {t('label.noResults')} @@ -125,6 +129,7 @@ const StashSelect = ({ return ( - Load {selected.length} of {Object.keys(stashesMap).length} Tabs + {t('action.loadTabs', { + selected: selected.length, + total: Object.keys(stashesMap).length + })}
{/*
@@ -181,19 +190,20 @@ const StashSelect = ({ } type StashItemProps = { + currentUser: UserInfo | null league: string | null stash: IStashTab selected: IStashTab[] onSelect: (stashes: IStashTab[]) => void } -const StashItem = ({ league, stash, selected, onSelect }: StashItemProps) => { +const StashItem = ({ currentUser, league, stash, selected, onSelect }: StashItemProps) => { + const { t } = useTranslation() const isSelected = useMemo(() => selected.some((x) => x.id === stash.id), [selected, stash.id]) const [open, setOpen] = useState(false) - const [relativeTime, setRelativeTime] = useState('') const { isSuccess, isError, isFetching, dataUpdatedAt } = useQuery({ - queryKey: ['stash', league, stash.id], + queryKey: [currentUser?.profile?.uuid, 'stash', league, stash.id], enabled: false }) @@ -205,9 +215,6 @@ const StashItem = ({ league, stash, selected, onSelect }: StashItemProps) => { delayDuration={500} open={open} onOpenChange={(open) => { - if (dataUpdatedAt !== 0) { - setRelativeTime(dayjs(dataUpdatedAt).fromNow()) - } setOpen(open) }} > @@ -215,11 +222,9 @@ const StashItem = ({ league, stash, selected, onSelect }: StashItemProps) => { key={stash.id} value={stash.id} className={cn( - 'hover:!bg-accent hover:!text-accent-foreground aria-selected:bg-inherit aria-selected:text-inherit px-0 py-0', - isFetching ? 'cursor-progress' : 'cursor-pointer', - isUnsupported && 'cursor-not-allowed' + "hover:!bg-accent hover:!text-accent-foreground aria-[selected='true']:bg-inherit aria-[selected='true']:text-inherit px-0 py-0", + isFetching ? 'cursor-progress' : 'cursor-pointer' )} - data-selected="true" disabled={isFetching || isUnsupported} onSelect={() => { onSelect( @@ -258,7 +263,16 @@ const StashItem = ({ league, stash, selected, onSelect }: StashItemProps) => { {!isUnsupported && ( - + )} {isFetching && ( @@ -288,6 +302,7 @@ function SubCommand({ onRefreshStashtabsClicked, onUnselectStashtabsClicked }: SubCommandProps) { + const { t } = useTranslation() const [open, setOpen] = useState(false) return ( @@ -310,18 +325,21 @@ function SubCommand({ > - + { onRefreshStashtabsClicked() setOpen(false) }} - className="hover:!bg-accent hover:!text-accent-foreground aria-selected:bg-inherit aria-selected:text-inherit cursor-pointer" + className={cn( + "hover:!bg-accent hover:!text-accent-foreground aria-[selected='true']:bg-inherit aria-[selected='true']:text-inherit cursor-pointer", + isStashListFetching && 'cursor-not-allowed' + )} disabled={isStashListFetching} >
- Refresh Stashtabs + {t('action.refreshStashTabs')}
- Unselect Stashtabs + {t('action.unselectStashTabs')}
diff --git a/src/green-app/src/components/poe/StashTabMultiSelect.tsx b/src/green-app/src/components/stashtab-multi-select.tsx similarity index 91% rename from src/green-app/src/components/poe/StashTabMultiSelect.tsx rename to src/green-app/src/components/stashtab-multi-select.tsx index a4ed7ecd..c2c3257f 100644 --- a/src/green-app/src/components/poe/StashTabMultiSelect.tsx +++ b/src/green-app/src/components/stashtab-multi-select.tsx @@ -2,9 +2,9 @@ import * as React from 'react' import { Check, ChevronsUpDown, X } from 'lucide-react' import { cn } from '@/lib/utils' -import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' -import { Button } from '../ui/button' -import { Badge } from '../ui/badge' +import { Popover, PopoverContent, PopoverTrigger } from './ui/popover' +import { Button } from './ui/button' +import { Badge } from './ui/badge' import { Command, CommandEmpty, @@ -12,9 +12,9 @@ import { CommandInput, CommandItem, CommandList -} from '../ui/command' +} from './ui/command' import { IStashTab } from '@/types/echo-api/stash' -// import { useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' export type OptionType = IStashTab @@ -28,7 +28,7 @@ interface MultiSelectProps { const StashTabMultiSelect = React.forwardRef( ({ options, selected, onChange, className, ...props }, ref) => { - // const { t } = useTranslation() + const { t } = useTranslation() const [open, setOpen] = React.useState(false) const handleUnselect = (item: IStashTab) => { @@ -97,8 +97,8 @@ const StashTabMultiSelect = React.forwardRef - - No item found. + + {t('label.noResults')} {options.map((option) => { diff --git a/src/green-app/src/components/table-cells/item-icon-cell.tsx b/src/green-app/src/components/table-cells/item-icon-cell.tsx new file mode 100644 index 00000000..648ebdd5 --- /dev/null +++ b/src/green-app/src/components/table-cells/item-icon-cell.tsx @@ -0,0 +1,38 @@ +import { getRarity, rarityColors } from '@/lib/item-util' +import Image from 'next/image' + +type ItemIconCellProps = { + value: string + frameType?: number + showRarityIndicator?: boolean +} + +export const ItemIconCell = ({ value, frameType, showRarityIndicator }: ItemIconCellProps) => { + return ( +
+ {showRarityIndicator && frameType !== undefined && ( +
+ )} + {''} +
+ ) +} diff --git a/src/green-app/src/components/table-cells/item-name-cell.tsx b/src/green-app/src/components/table-cells/item-name-cell.tsx new file mode 100644 index 00000000..ab30f800 --- /dev/null +++ b/src/green-app/src/components/table-cells/item-name-cell.tsx @@ -0,0 +1,16 @@ +type ItemNameCellProps = { + value: string +} + +export const ItemNameCell = ({ value }: ItemNameCellProps) => { + return ( +
{ + e.currentTarget.scrollLeft = 0 + }} + > + {value} +
+ ) +} diff --git a/src/green-app/src/components/table-cells/item-price-cell.tsx b/src/green-app/src/components/table-cells/item-price-cell.tsx new file mode 100644 index 00000000..72e8a7e3 --- /dev/null +++ b/src/green-app/src/components/table-cells/item-price-cell.tsx @@ -0,0 +1,26 @@ +import { CurrencySwitch } from '@/lib/currency' +import CurrencyDisplay from '../currency-display' + +type ItemValueCellProps = { + value: number | string + editable?: boolean + showChange?: boolean + toCurrency?: CurrencySwitch +} + +export const ItemValueCell = ({ value, showChange, toCurrency }: ItemValueCellProps) => { + return ( +
+ {typeof value === 'number' ? ( + + ) : ( + value + )} +
+ ) +} diff --git a/src/green-app/src/components/table-cells/item-props-cell.tsx b/src/green-app/src/components/table-cells/item-props-cell.tsx new file mode 100644 index 00000000..dad7b678 --- /dev/null +++ b/src/green-app/src/components/table-cells/item-props-cell.tsx @@ -0,0 +1,29 @@ +import { useMemo } from 'react' +import { Badge } from '../ui/badge' + +type ItemPropsCellProps = { + value: string +} + +export const ItemPropsCell = ({ value }: ItemPropsCellProps) => { + const hashProps = useMemo(() => { + if (!value) return [] + return value.split(';;;').map((v) => { + const keyVal = v.split(';;') + return { name: keyVal[0], value: keyVal[1] } + }) + }, [value]) + + return ( +
(e.currentTarget.scrollLeft = 0)} + > + {hashProps.map(({ name, value }) => ( + + {value} + + ))} +
+ ) +} diff --git a/src/green-app/src/components/table-cells/item-quantity-cell.tsx b/src/green-app/src/components/table-cells/item-quantity-cell.tsx new file mode 100644 index 00000000..a3f0a684 --- /dev/null +++ b/src/green-app/src/components/table-cells/item-quantity-cell.tsx @@ -0,0 +1,22 @@ +import { cn } from '@/lib/utils' + +type ItemQuantityCellProps = { + quantity: number + diff?: boolean +} + +export const ItemQuantityCell = ({ quantity, diff }: ItemQuantityCellProps) => { + return ( +
0 && `text-green-400`, + diff && quantity < 0 && `text-red-400` + )} + > + {diff && quantity > 0 ? '+ ' : ''} + {quantity} +
+ ) +} diff --git a/src/green-app/src/components/table-cells/item-sparkline-cell.tsx b/src/green-app/src/components/table-cells/item-sparkline-cell.tsx new file mode 100644 index 00000000..6d98baee --- /dev/null +++ b/src/green-app/src/components/table-cells/item-sparkline-cell.tsx @@ -0,0 +1,73 @@ +import { formatValue } from '@/lib/currency' +import { cn } from '@/lib/utils' +import { SageValuation } from '@/types/echo-api/valuation' +import { useMemo } from 'react' +import { Area, AreaChart, ResponsiveContainer } from 'recharts' + +type SparklineCellProps = { + valuation?: SageValuation + totalChange: number | string + mode: '2 days' | '7 days' + animation?: boolean +} + +export const SparklineCell = ({ valuation, totalChange, mode, animation }: SparklineCellProps) => { + const data = useMemo(() => { + if (!valuation) return + + const history = + mode === '2 days' ? valuation.history.primaryValueHourly : valuation.history.primaryValueDaily + + if (history.length < 2) return [] + + return history.map((value) => { + const format = formatValue(value, 'chaos') + return { + name: 'history', + value: format.value + } + }) + }, [mode, valuation]) + + return ( + <> + {data && ( +
+ + + + + + + + + + + + +
0 && ` text-green-400`, + typeof totalChange === 'number' && totalChange < 0 && ` text-red-400` + )} + > + {typeof totalChange === 'number' + ? totalChange.toLocaleString(undefined, { + maximumFractionDigits: 1 + }) + ' ' + : totalChange} + % +
+
+ )} + + ) +} diff --git a/src/green-app/src/components/table-column-toggle.tsx b/src/green-app/src/components/table-column-toggle.tsx new file mode 100644 index 00000000..8e7981d2 --- /dev/null +++ b/src/green-app/src/components/table-column-toggle.tsx @@ -0,0 +1,149 @@ +// import { MixerHorizontalIcon } from '@radix-ui/react-icons' +import { ColumnDef, ColumnOrderState, VisibilityState } from '@tanstack/react-table' +import { ChevronDown, ChevronDownIcon, ChevronUpIcon, ListRestartIcon } from 'lucide-react' +import React, { useMemo, useState } from 'react' +import { Button } from './ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from './ui/dropdown-menu' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip' +import { useTranslation } from 'react-i18next' + +interface TableColumnToggleProps { + columns: ColumnDef[] + columnVisibility: VisibilityState + columnOrder: ColumnOrderState + onColumnVisibility: React.Dispatch> + onColumnOrder: React.Dispatch> + resetTable: () => void +} + +function TableColumnToggle({ + columns, + columnVisibility, + columnOrder, + onColumnVisibility, + onColumnOrder, + resetTable +}: TableColumnToggleProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const columnsValid = useMemo(() => columns.every((c) => c.id), [columns]) + const sortedColumns = useMemo(() => { + if (columnOrder.length > 0) { + const clonedColumns = [...columns] + const sortedColumns = columnOrder.map((co) => { + const idx = clonedColumns.findIndex((c) => c.id === co) + if (idx > -1) { + return clonedColumns.splice(idx, 1)[0] + } + }) + sortedColumns.push(...clonedColumns) + + if (sortedColumns.every((c) => c)) { + return sortedColumns as ColumnDef[] + } + return columns + } + return columns + }, [columnOrder, columns]) + + if (!columnsValid) return null + + const handleColumnOrderChange = (columnId: string, up: boolean) => { + const columnList = columnOrder.length > 0 ? [...columnOrder] : columns.map((c) => c.id!) + if (columnList.length < 2) return + const prevIdx = columnList.indexOf(columnId) + if (prevIdx === -1) return + const nextIdx = up ? prevIdx + 1 : prevIdx - 1 + if (!columnList[nextIdx]) return + + const temp = columnList[nextIdx] + columnList[nextIdx] = columnList[prevIdx] + columnList[prevIdx] = temp + + onColumnOrder(columnList) + } + + return ( + + + + + + + {t('label.columnSettings')} + + + + + + {t('label.columnSettingsResetTT')} + + + + + {sortedColumns.map((column, i) => { + return ( + { + onColumnVisibility((state) => ({ ...state, [column.id!]: !!value })) + }} + > + + {t(`columnTitle.${column.meta?.headerWording}` as any)} + +
+ + +
+
+ ) + })} +
+
+ ) +} + +export default TableColumnToggle diff --git a/src/green-app/src/components/table-columns/check-column.tsx b/src/green-app/src/components/table-columns/check-column.tsx new file mode 100644 index 00000000..7ab25b1a --- /dev/null +++ b/src/green-app/src/components/table-columns/check-column.tsx @@ -0,0 +1,78 @@ +import { ColumnDef } from '@tanstack/react-table' +import { SageItemGroup } from 'sage-common' +import { Checkbox } from '../ui/checkbox' + +type ColumnProps = Partial> & {} + +export function checkColumn( + props?: ColumnProps +): ColumnDef { + const id = 'selected' + const header = 'selection' + + return { + header: ({ table }) => { + return ( +
+ table.toggleAllRowsSelected( + !(table.getIsAllRowsSelected() || (table.getIsSomeRowsSelected() && 'indeterminate')) + ) + } + > + table.toggleAllRowsSelected(!!value)} + aria-label="Select all" + /> +
+ ) + }, + id: id, + enableSorting: true, + enableGlobalFilter: false, + enableResizing: false, + size: 40, + meta: { + headerWording: header, + className: 'min-w-[40px] max-w-fit p-0', + removePadding: true + }, + sortDescFirst: true, + cell: ({ row }) => { + if ('group' in row.original) { + return ( +
{ + if ('group' in row.original && row.original.group) { + row.toggleSelected(!row.getIsSelected()) + } + }} + > + +
+ ) + } + return ( +
{ + row.toggleSelected(!row.getIsSelected()) + }} + > + +
+ ) + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/history-column.tsx b/src/green-app/src/components/table-columns/history-column.tsx new file mode 100644 index 00000000..a1f8420d --- /dev/null +++ b/src/green-app/src/components/table-columns/history-column.tsx @@ -0,0 +1,67 @@ +import { SageValuation } from '@/types/echo-api/valuation' +import { TableColumnHeader } from '../column-header' +import { ColumnDef } from '@tanstack/react-table' +import { SparklineCell } from '../table-cells/item-sparkline-cell' + +type ColumnProps = Partial> & { + mode: '2 days' | '7 days' + animation?: boolean +} + +export function historyColumn( + { mode, animation, ...props }: ColumnProps = { mode: '2 days' } +): ColumnDef { + const key = mode === '2 days' ? '2_day_history' : '7_day_history' + const header = mode === '2 days' ? '2_day_history' : '7_day_history' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + accessorFn: (pricedItem) => { + const valuation = pricedItem.valuation + if (!valuation) return '- ' + // Remove indexes + const history = + mode === '2 days' + ? valuation.history.primaryValueHourly + : valuation.history.primaryValueDaily + if (history.length < 2) return '- ' + let i = history.length + let indexToUse = history.length + while (i--) { + if (history[i]) { + indexToUse = i + break + } + } + if (indexToUse === 0) return '- ' + + if (!history[indexToUse]) return '- ' + if (!history[0]) return '- ' + + return (history[indexToUse] / history[0] - 1) * 100 + }, + enableSorting: true, + enableGlobalFilter: false, + size: 200, + minSize: 100, + meta: { + headerWording: header, + staticResizing: true + }, + cell: ({ row }) => { + const value = row.original.valuation + const totalChange = row.getValue(key) + return ( + + ) + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/multiplier-column.tsx b/src/green-app/src/components/table-columns/multiplier-column.tsx new file mode 100644 index 00000000..fd7c7d9b --- /dev/null +++ b/src/green-app/src/components/table-columns/multiplier-column.tsx @@ -0,0 +1,28 @@ +import { AccessorFn, ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' + +type ColumnProps = Omit, 'accessorKey' | 'accessorFn'> & { + accessorKey?: string + accessorFn?: AccessorFn +} + +export function multiplierColumn(props: ColumnProps): ColumnDef { + const key = 'multiplier' + const header = 'multiplier' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + enableSorting: true, + enableGlobalFilter: false, + meta: { + headerWording: header + }, + cell: ({ cell }) => { + const value = cell.getValue() + return
{value}%
+ }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/name-column.tsx b/src/green-app/src/components/table-columns/name-column.tsx new file mode 100644 index 00000000..93ee3b93 --- /dev/null +++ b/src/green-app/src/components/table-columns/name-column.tsx @@ -0,0 +1,45 @@ +import { ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { ItemIconCell } from '../table-cells/item-icon-cell' +import { ItemNameCell } from '../table-cells/item-name-cell' + +type ColumnProps = Partial> & { + showRarityIndicator?: boolean +} + +export function nameColumn({ + showRarityIndicator, + ...props +}: ColumnProps = {}): ColumnDef { + const key = 'displayName' + const header = 'name' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + enableSorting: true, + enableGlobalFilter: true, + size: 230, + minSize: 90, + meta: { + headerWording: header, + staticResizing: true + }, + cell: ({ row }) => { + const value = row.getValue(key) + + return ( +
+ + +
+ ) + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/price-column.tsx b/src/green-app/src/components/table-columns/price-column.tsx new file mode 100644 index 00000000..2c3dabef --- /dev/null +++ b/src/green-app/src/components/table-columns/price-column.tsx @@ -0,0 +1,69 @@ +import { AccessorFn, ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { ItemValueCell } from '../table-cells/item-price-cell' + +type ColumnProps = Omit, 'accessorKey' | 'accessorFn'> & { + headerName: string + toCurrency?: 'chaos' | 'divine' | 'both' + showChange?: boolean + cumulativeColumn?: string + accessorKey: string + accessorFn?: AccessorFn +} + +export function priceColumn({ + accessorKey, + accessorFn, + headerName, + toCurrency, + showChange, + cumulativeColumn: cumulative, + enableSorting, + ...props +}: ColumnProps): ColumnDef { + return { + id: accessorKey, + accessorKey: accessorKey, + accessorFn: accessorFn, + header: ({ column }) => ( + + ), + enableSorting: enableSorting ?? true, + enableGlobalFilter: false, + enableResizing: false, + sortingFn: (rowA, rowB, columnId: string) => { + const val1 = rowA.getValue(columnId) + const val2 = rowB.getValue(columnId) + if (typeof val1 === 'number' && typeof val2 === 'number') { + return val1 - val2 + } else if (typeof val1 === 'number') { + return val1 - 0 + } else { + return 0 - (val2 as number) + } + }, + meta: { + headerWording: headerName + }, + cell: ({ row, table }) => { + let value = 0 + if (cumulative) { + const sortedRows = table.getSortedRowModel().rows + for (let i = 0; i < sortedRows.length; i++) { + const total = sortedRows[i].getValue(cumulative) + if (total !== undefined) { + value += total + } + if (sortedRows[i].id === row.id) { + break + } + } + } else if (accessorKey) { + value = row.getValue(accessorKey) + } + + return + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/price-input-column.tsx b/src/green-app/src/components/table-columns/price-input-column.tsx new file mode 100644 index 00000000..e5e6948d --- /dev/null +++ b/src/green-app/src/components/table-columns/price-input-column.tsx @@ -0,0 +1,64 @@ +import { ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { ChangeEvent } from 'react' +import { round } from '@/lib/currency' +import DebouncedInput from '../debounced-input' +import { SageItemGroup } from 'sage-common' + +type ColumnProps = Partial> & {} + +export function priceInputColumn< + T extends { selectedPrice?: number; originalPrice?: number; group?: SageItemGroup } +>(props: ColumnProps = {}): ColumnDef { + const key = 'selectedPrice' + const header = 'selectedPrice' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + enableSorting: true, + enableGlobalFilter: false, + enableResizing: false, + meta: { + headerWording: header + }, + cell: ({ row, table, column }) => { + const initialValue = row.getValue(key) + + const onInnerChange = (e: ChangeEvent) => { + const newValue = parseFloat(e.target.value) + if (Number.isNaN(newValue) || newValue < 0) return '' + return round(newValue, 4) + } + + const placeHolder = `${row.original.originalPrice ?? '?'}c` + + const updateTableData = (value: string | number) => { + if (typeof value === 'number') { + table.options.meta?.updateData?.(row.index, column.id, value) + } else if (!Number.isNaN(parseFloat(value))) { + table.options.meta?.updateData?.(row.index, column.id, parseFloat(value)) + } else { + table.options.meta?.updateData?.(row.index, column.id, value) + } + } + + return ( + updateTableData(value)} + onBlur={(value) => updateTableData(value)} + debounce={250} + /> + ) + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/props-column.tsx b/src/green-app/src/components/table-columns/props-column.tsx new file mode 100644 index 00000000..5e27bbc5 --- /dev/null +++ b/src/green-app/src/components/table-columns/props-column.tsx @@ -0,0 +1,42 @@ +import { parseUnsafeHashPropsToStr } from '@/lib/item-util' +import { ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { SageItemGroup } from 'sage-common' +import { ItemPropsCell } from '../table-cells/item-props-cell' +import { SageItemGroupSummary } from '@/types/echo-api/item-group' + +type ColumnProps = Partial> & {} + +export function propsColumn< + T extends { group?: SageItemGroup } | { summary?: SageItemGroupSummary } +>(props?: ColumnProps): ColumnDef { + const key = 'unsafeHashProperties' + const header = 'props' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + accessorFn: (val) => { + if ('group' in val) { + return parseUnsafeHashPropsToStr(val.group?.unsafeHashProperties) + } else if ('summary' in val) { + return parseUnsafeHashPropsToStr(val.summary?.unsafeHashProperties) + } + return '' + }, + enableSorting: true, + enableGlobalFilter: true, + size: 120, + minSize: 85, + meta: { + headerWording: header, + staticResizing: true + }, + cell: ({ row }) => { + const value = row.getValue(key) + return + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/quantity-column.tsx b/src/green-app/src/components/table-columns/quantity-column.tsx new file mode 100644 index 00000000..75da6efe --- /dev/null +++ b/src/green-app/src/components/table-columns/quantity-column.tsx @@ -0,0 +1,40 @@ +import { ColumnDef } from '@tanstack/react-table' +import { ItemQuantityCell } from '../table-cells/item-quantity-cell' +import { TableColumnHeader } from '../column-header' + +type ColumnProps = Partial> & { + diff?: boolean +} + +export function quantityColumn({ + diff, + ...props +}: ColumnProps = {}): ColumnDef { + const key = 'quantity' + const header = 'quantity' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + accessorFn: (value) => { + if ('stackSize' in value) { + return value.stackSize + } else if ('quantity' in value) { + return value.quantity + } + return 0 + }, + enableSorting: true, + enableGlobalFilter: false, + enableResizing: false, + meta: { + headerWording: header + }, + cell: ({ cell }) => { + const value = cell.getValue() + return + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/quantity-input-column.tsx b/src/green-app/src/components/table-columns/quantity-input-column.tsx new file mode 100644 index 00000000..6cce5d22 --- /dev/null +++ b/src/green-app/src/components/table-columns/quantity-input-column.tsx @@ -0,0 +1,64 @@ +import { round } from '@/lib/currency' +import { ColumnDef } from '@tanstack/react-table' +import { ChangeEvent } from 'react' +import { TableColumnHeader } from '../column-header' +import DebouncedInput from '../debounced-input' + +type ColumnProps = Partial> & {} + +export function quantityInputColumn( + props: ColumnProps = {} +): ColumnDef { + const key = 'selectedQuantity' + const header = 'selectedQuantity' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + enableSorting: true, + enableGlobalFilter: false, + enableResizing: false, + meta: { + headerWording: header + }, + cell: ({ row, table, column }) => { + const initialValue = row.getValue(key) + + const onInnerChange = (e: ChangeEvent) => { + const newValue = parseFloat(e.target.value) + if (Number.isNaN(newValue) || newValue < 0) return 0 + else if (newValue > row.original.quantity) return round(row.original.quantity) + return round(newValue, 4) + } + + const placeHolder = `${row.original.quantity ?? '?'}` + + const updateTableData = (value: string | number) => { + if (typeof value === 'number') { + table.options.meta?.updateData?.(row.index, column.id, value) + } else if (!Number.isNaN(parseFloat(value))) { + table.options.meta?.updateData?.(row.index, column.id, parseFloat(value)) + } else { + table.options.meta?.updateData?.(row.index, column.id, value) + } + } + + return ( + updateTableData(value)} + onBlur={(value) => updateTableData(value)} + debounce={250} + /> + ) + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/tabs-column.tsx b/src/green-app/src/components/table-columns/tabs-column.tsx new file mode 100644 index 00000000..8cbdc119 --- /dev/null +++ b/src/green-app/src/components/table-columns/tabs-column.tsx @@ -0,0 +1,34 @@ +import { ICompactTab } from '@/types/echo-api/stash' +import { ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { parseTabNames } from '@/lib/item-util' +import { ItemNameCell } from '../table-cells/item-name-cell' + +type ColumnProps = Partial> & {} + +export function tabsColumn( + props?: ColumnProps +): ColumnDef { + const key = 'tabs' + const header = 'tabs' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + accessorFn: (val) => parseTabNames(val.tabs), + enableSorting: true, + enableGlobalFilter: true, + size: 75, + minSize: 50, + meta: { + headerWording: header, + staticResizing: true + }, + cell: ({ row }) => { + const value = row.getValue(key) + return + }, + ...props + } +} diff --git a/src/green-app/src/components/table-columns/tag-column.tsx b/src/green-app/src/components/table-columns/tag-column.tsx new file mode 100644 index 00000000..4fac153a --- /dev/null +++ b/src/green-app/src/components/table-columns/tag-column.tsx @@ -0,0 +1,32 @@ +import { ColumnDef } from '@tanstack/react-table' +import { TableColumnHeader } from '../column-header' +import { SageItemGroup } from 'sage-common' +import { ItemNameCell } from '../table-cells/item-name-cell' + +type ColumnProps = Partial> & {} + +export function tagColumn( + props?: ColumnProps +): ColumnDef { + const key = 'tag' + const header = 'tag' + + return { + header: ({ column }) => , + id: key, + accessorKey: key, + accessorFn: (val) => val.group?.tag, + enableSorting: true, + enableGlobalFilter: true, + size: 65, + minSize: 65, + meta: { + headerWording: header + }, + cell: ({ row }) => { + const value = row.getValue(key) + return + }, + ...props + } +} diff --git a/src/green-app/src/components/table-pagination.tsx b/src/green-app/src/components/table-pagination.tsx index 80ab5718..2e245e10 100644 --- a/src/green-app/src/components/table-pagination.tsx +++ b/src/green-app/src/components/table-pagination.tsx @@ -11,6 +11,7 @@ import { } from '@/components/ui/select' import React from 'react' import { cn } from '@/lib/utils' +import { useTranslation } from 'react-i18next' interface TablePaginationProps { className?: string @@ -27,6 +28,7 @@ export function TablePagination({ pageSizes, enableHotkeyNavigation }: TablePaginationProps) { + const { t } = useTranslation() // on delete key press, remove last selected item React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -48,18 +50,20 @@ export function TablePagination({ }, [enableHotkeyNavigation, table]) return ( -
+
{showSelected ? (
- {table.getFilteredSelectedRowModel().rows.length} of{' '} - {table.getFilteredRowModel().rows.length} row(s) selected. + {t('label.rowsSelectedOf', { + current: table.getFilteredSelectedRowModel().rows.length, + total: table.getFilteredRowModel().rows.length + })}
) : (
)}
-

Rows per page

+

{t('label.rowsPerPage')}

- Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} + {t('label.pageOf', { + current: table.getState().pagination.pageIndex + 1, + total: table.getPageCount() + })}
diff --git a/src/green-app/src/components/time-tracker.tsx b/src/green-app/src/components/time-tracker.tsx new file mode 100644 index 00000000..ad2af824 --- /dev/null +++ b/src/green-app/src/components/time-tracker.tsx @@ -0,0 +1,34 @@ +import dayjs from 'dayjs' +import duration from 'dayjs/plugin/duration' +import relativeTime from 'dayjs/plugin/relativeTime' +import { useEffect, useReducer, useRef } from 'react' + +dayjs.extend(duration) +dayjs.extend(relativeTime) + +interface Props { + createdAt: number | dayjs.Dayjs + className?: string +} + +export function TimeTracker({ createdAt, className }: Props) { + const [, forceUpdate] = useReducer((x) => x + 1, 0) + const intervalRef = useRef() + + // refresh value of `createdAt` every ~ 1 minute + useEffect(() => { + intervalRef.current = setInterval(() => { + forceUpdate() + }, 1000) + + return () => { + clearInterval(intervalRef.current) + } + }, []) + + return ( +
+ {dayjs(createdAt).fromNow()} +
+ ) +} diff --git a/src/green-app/src/components/trade-filter-card.tsx b/src/green-app/src/components/trade-filter-card.tsx index cfca4e1c..7652f10f 100644 --- a/src/green-app/src/components/trade-filter-card.tsx +++ b/src/green-app/src/components/trade-filter-card.tsx @@ -1,7 +1,6 @@ 'use client' import { listSummaries } from '@/lib/http-util' -import { CaretSortIcon } from '@radix-ui/react-icons' import { ChangeEvent, Dispatch, @@ -13,7 +12,7 @@ import { useState } from 'react' -import { BasicSelect } from '@/components/poe/BasicSelect' +import { BasicSelect } from '@/components/basic-select' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { @@ -21,17 +20,23 @@ import { CommandEmpty, CommandGroup, CommandInput, - CommandItem + CommandItem, + CommandList } from '@/components/ui/command' import { Input } from '@/components/ui/input' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { LISTING_CATEGORIES } from '@/lib/listing-categories' +import { LISTING_CATEGORIES, ListingCategory, ListingSubCategory } from '@/lib/listing-categories' import { cn } from '@/lib/utils' +import { SageItemGroupSummary, SageItemGroupSummaryShard } from '@/types/echo-api/item-group' import { SageListingItemType } from '@/types/sage-listing-type' +import { useQueries } from '@tanstack/react-query' import { SquarePenIcon, XIcon } from 'lucide-react' import Image from 'next/image' -import { useQueries } from '@tanstack/react-query' -import { SageItemGroupSummary, SageItemGroupSummaryShard } from '@/types/echo-api/item-group' +import { ComboboxItem, ComboboxTrigger } from './ui/combobox' +import { useTranslation } from 'react-i18next' +import { Skeleton } from './ui/skeleton' +import { parseUnsafeHashProps } from '@/lib/item-util' +import { Badge } from './ui/badge' export type FilterOption = { hash: string @@ -46,7 +51,7 @@ export type FilterOption = { export type ListingFilter = { option: FilterOption | null selected: boolean - minimumQuantity: number | undefined + minimumQuantity?: number } export type ListingFilterGroupModes = 'AND' | 'NOT' | 'COUNT' @@ -54,12 +59,14 @@ export type ListingFilterGroupModes = 'AND' | 'NOT' | 'COUNT' export type ListingFilterGroup = { mode: ListingFilterGroupModes selected: boolean + minimumQuantity?: number // For COUNT filters: ListingFilter[] } type ListingFilterCardProps = { className?: string category: string | null + subCategory: string | null filterGroups: ListingFilterGroup[] onFilterGroupsChange: (filterGroups: ListingFilterGroup[]) => void } @@ -67,11 +74,14 @@ type ListingFilterCardProps = { const ListingFilterCard = ({ className, category, + subCategory, filterGroups, onFilterGroupsChange }: ListingFilterCardProps) => { + const { t } = useTranslation() const [options, setOptions] = useState([]) const [summaries, setSummaries] = useState>({}) + const [summariesPending, setSummariesPending] = useState(false) const [addGroupPreviewOpen, setAddGroupPreviewOpen] = useState(false) const updateGroup = useMemo(() => { @@ -98,8 +108,10 @@ const ListingFilterCard = ({
{filterGroups.map((group, i) => ( ))} {!addGroupPreviewOpen ? ( ) : (
@@ -126,6 +139,7 @@ const ListingFilterCard = ({ onOpenChange={(open) => { setTimeout(() => setAddGroupPreviewOpen(open), 0) }} + translate />
)} @@ -137,6 +151,7 @@ type ListingFilterGroupCardProps = { options: FilterOption[] summaries: Record group: ListingFilterGroup + loading: boolean onGroupChange: (f: ListingFilterGroup) => void removeGroup: () => void } @@ -145,12 +160,21 @@ const ListingFilterGroupCard = ({ options, summaries, group, + loading, onGroupChange, removeGroup }: ListingFilterGroupCardProps) => { + const { t } = useTranslation() const [editMode, setEditMode] = useState(false) const [groupSelectOpen, setGroupSelectOpen] = useState(false) + const availableOptions = useMemo(() => { + const selectedHashes = group.filters + .filter((g) => g.selected && g.option?.hash) + .flatMap((x) => x.option!.hash) + return options.filter((o) => !selectedHashes.includes(o.hash)) + }, [options, group.filters]) + function toggleSelected() { onGroupChange({ ...group, @@ -161,7 +185,8 @@ const ListingFilterGroupCard = ({ function setMode(mode: ListingFilterGroupModes) { onGroupChange({ ...group, - mode: mode + mode: mode, + minimumQuantity: undefined }) } @@ -187,49 +212,77 @@ const ListingFilterGroupCard = ({ [group, onGroupChange] ) + const handleMinQuantityChange = (e: ChangeEvent) => { + const newValue = parseInt(e.target.value) + if (Number.isNaN(newValue) || newValue < 0) { + onGroupChange({ + ...group, + minimumQuantity: undefined + }) + } else { + onGroupChange({ + ...group, + minimumQuantity: newValue + }) + } + } + return ( -
+
toggleSelected()} /> - {!editMode && ( +
+ {!editMode && ( + + )} + {editMode && ( +
+ { + setEditMode(false) + setMode(m as ListingFilterGroupModes) + }} + defaultOption={group.mode} + open={groupSelectOpen} + onOpenChange={(open) => { + setGroupSelectOpen(open) + if (!open) setTimeout(() => setEditMode(false), 0) + }} + translate + /> +
+ )} - )} - {editMode && ( -
- { - setEditMode(false) - setMode(m as ListingFilterGroupModes) - }} - defaultOption={group.mode} - open={groupSelectOpen} - onOpenChange={(open) => { - setGroupSelectOpen(open) - if (!open) setTimeout(() => setEditMode(false), 0) - }} + {group.mode === 'COUNT' && ( + -
- )} - - + )} + +
{group.selected && ( <> @@ -238,10 +291,12 @@ const ListingFilterGroupCard = ({ @@ -249,10 +304,12 @@ const ListingFilterGroupCard = ({ })} @@ -276,25 +333,28 @@ type ListingGroupFilterSelectProps = { removeFilter: (i: number) => void updateFilter: (index: number, filter: ListingFilter) => void options: FilterOption[] + availableOptions: FilterOption[] summaries: Record filter: ListingFilter index: number isNextFilter: boolean + loading: boolean } function ListingGroupFilterSelect({ options, + availableOptions, summaries, filter, index, updateFilter, removeFilter, - isNextFilter + isNextFilter, + loading }: ListingGroupFilterSelectProps) { + const { t } = useTranslation() const [popoverOpen, setPopoverOpen] = useState(false) - const [minQuantity, setMinQuantity] = useState(filter.minimumQuantity || '') - const selectedOption = useMemo( () => options?.find((option) => option.hash === filter.option?.hash), [filter.option?.hash, options] @@ -303,10 +363,8 @@ function ListingGroupFilterSelect({ const handleMinQuantityChange = (e: ChangeEvent) => { const newValue = parseFloat(e.target.value) if (Number.isNaN(newValue) || newValue < 0) { - setMinQuantity('') updateFilter(index, { ...filter, minimumQuantity: undefined }) } else { - setMinQuantity(newValue) updateFilter(index, { ...filter, minimumQuantity: newValue }) } } @@ -323,75 +381,96 @@ function ListingGroupFilterSelect({ ) : (
)} - - - - - - { - if (summaries[hash]?.displayName?.toLowerCase().includes(search.toLowerCase())) - return 1 - return 0 - }} - > - - No results. - - {options.map((c) => ( - { - const selectedOption = options.find((s) => s.hash === hash) - updateFilter(index, { ...filter, option: selectedOption ?? null }) - setPopoverOpen(false) - }} - > -
- d -
{c.displayName}
- {c.unsafeHashProperties.uses && ( -
{`${c.unsafeHashProperties.uses} Uses`}
- )} +
+ + + + {selectedOption ? ( + <> +
+ d +
{selectedOption.displayName}
- - ))} - - - -
- {!isNextFilter ? ( - <> + + ) : ( + <>{t('label.selectPh')} + )} + + + + { + if (summaries[hash]?.displayName?.toLowerCase().includes(search.toLowerCase())) + return 1 + return 0 + }} + > + + {t('label.noResults')} + + + {loading + ? Array.from(Array(5)).map((_, i) => ( + +
+ + +
+
+ )) + : availableOptions.map((c) => ( + { + const selectedOption = options.find((s) => s.hash === hash) + updateFilter(index, { ...filter, option: selectedOption ?? null }) + setPopoverOpen(false) + }} + disableSelection + > +
+ d +
{c.displayName}
+ {parseUnsafeHashProps(c.unsafeHashProperties).map(({ name, value }) => ( + + {value} + + ))} +
+
+ ))} +
+
+
+
+ + {!isNextFilter ? ( + ) : ( +
+ )} + + {!isNextFilter ? ( - - ) : ( - <> -
+ ) : (
- - )} + )} +
) } @@ -415,19 +491,44 @@ const MemorizedListingGroupFilterSelect = memo(ListingGroupFilterSelect) type SummariesQueriesType = { category: string | null + subCategory: string | null setSummaries: Dispatch>> setOptions: Dispatch> + setSummariesPending: Dispatch> } -const SummariesQueries = ({ category, setSummaries, setOptions }: SummariesQueriesType) => { - const categoryTagItem = useMemo( +const SummariesQueries = ({ + category, + subCategory, + setSummaries, + setOptions, + setSummariesPending +}: SummariesQueriesType) => { + const categoryItem = useMemo( () => LISTING_CATEGORIES.find((ca) => ca.name === category), [category] ) - const { summaries, options, isSummaryPending, isSummaryLoading, isSummaryError } = useQueries({ - queries: categoryTagItem - ? categoryTagItem.tags.map((tag) => { + const [selectedCategoryItem, categoriesToExclude] = useMemo(() => { + let selectedCategoryItem: ListingCategory | ListingSubCategory | undefined + if (subCategory) { + selectedCategoryItem = categoryItem?.subCategories.find((c) => c.name === subCategory) + if (!selectedCategoryItem) return [] + if (selectedCategoryItem.restItems) { + const categoriesToExclude = categoryItem?.subCategories.filter( + (c) => c.name !== selectedCategoryItem?.name + ) + return [selectedCategoryItem, categoriesToExclude] + } + return [selectedCategoryItem] + } + + return [categoryItem] + }, [categoryItem, subCategory]) + + const { summaries, options, isSummaryPending } = useQueries({ + queries: selectedCategoryItem + ? selectedCategoryItem.tags.map((tag) => { return { queryKey: ['summaries', tag], queryFn: () => listSummaries(tag), @@ -441,20 +542,50 @@ const SummariesQueries = ({ category, setSummaries, setOptions }: SummariesQueri const isSummaryError = summaryResults.some((result) => result.isError) const isSummaryLoading = summaryResults.some((result) => result.isLoading) - let summaries: SageItemGroupSummaryShard['summaries'] = {} + const summaries: SageItemGroupSummaryShard['summaries'] = {} let options: FilterOption[] = [] if (!(isSummaryError || isSummaryLoading)) { const summaryShards = summaryResults .filter((x) => x.data && !x.isPending) .map((x) => x.data!) summaryShards.forEach((e) => { - Object.entries(e.summaries).filter(([key, value]) => { - if (categoryTagItem?.filter?.({ group: value }) === false) { - delete e.summaries[key] + const tagCategoriesToExclude = categoriesToExclude?.filter((c) => + c.tags.includes(e.meta.tag) + ) + // Remove entire tag from output + if (tagCategoriesToExclude?.some((c) => c.filter === undefined)) { + return + } + + Object.entries(e.summaries).forEach(([key, value]) => { + if ( + !( + selectedCategoryItem?.filter?.({ + group: { + tag: e.meta.tag, + key: value.key, + unsafeHashProperties: value.unsafeHashProperties + } + }) === false + ) + ) { + const summary = e.summaries[key] + if ( + !tagCategoriesToExclude?.some((c) => + c.filter?.({ + group: { + tag: e.meta.tag, + key: summary.key, + unsafeHashProperties: summary.unsafeHashProperties + } + }) + ) && + e.summaries[key].displayName + ) { + summaries[key] = e.summaries[key] + } } }) - - summaries = { ...summaries, ...e.summaries } }) options = Object.entries(summaries).map(([key, value]): FilterOption => { @@ -483,7 +614,8 @@ const SummariesQueries = ({ category, setSummaries, setOptions }: SummariesQueri useEffect(() => { setSummaries(summaries) setOptions(options) - }, [setSummaries, summaries, setOptions, options]) + setSummariesPending(isSummaryPending) + }, [setSummaries, summaries, setOptions, options, isSummaryPending, setSummariesPending]) return null } diff --git a/src/green-app/src/components/translations-provider.tsx b/src/green-app/src/components/translations-provider.tsx new file mode 100644 index 00000000..8f694048 --- /dev/null +++ b/src/green-app/src/components/translations-provider.tsx @@ -0,0 +1,22 @@ +'use client' + +import { I18nextProvider } from 'react-i18next' +import { ReactNode } from 'react' +import initTranslations from '@/config/i18n.config' +import { Resource, createInstance } from 'i18next' + +export default function TranslationsProvider({ + children, + locale, + resources +}: { + children: ReactNode + locale: string + resources: Resource +}) { + const i18n = createInstance() + + initTranslations(locale, i18n, resources) + + return {children} +} diff --git a/src/green-app/src/components/ui/alert.tsx b/src/green-app/src/components/ui/alert.tsx new file mode 100644 index 00000000..5afd41d1 --- /dev/null +++ b/src/green-app/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/green-app/src/components/ui/combobox.tsx b/src/green-app/src/components/ui/combobox.tsx new file mode 100644 index 00000000..fc4b51bc --- /dev/null +++ b/src/green-app/src/components/ui/combobox.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { CommandItem } from './command' +import { cn } from '@/lib/utils' +import { CaretSortIcon, CheckIcon } from '@radix-ui/react-icons' +import { Slot } from '@radix-ui/react-slot' +import { Button } from './button' + +export interface ComboboxTriggerProps extends React.ButtonHTMLAttributes {} + +const ComboboxTrigger = React.forwardRef( + ({ className, children, ...props }, ref) => { + return ( + + ) + } +) +ComboboxTrigger.displayName = 'ComboboxTrigger' + +interface CommandItemProps extends React.ComponentPropsWithoutRef { + selected?: boolean + disableSelection?: boolean +} + +const ComboboxItem = React.forwardRef, CommandItemProps>( + ({ className, selected, disableSelection, children, ...props }, ref) => { + return ( + + {!disableSelection && ( + + {selected && ( + + + + )} + + )} + {children} + + ) + } +) +ComboboxItem.displayName = 'ComboboxItem' + +export { ComboboxTrigger, ComboboxItem } diff --git a/src/green-app/src/components/ui/command.tsx b/src/green-app/src/components/ui/command.tsx index 60187e8e..6b7c78e8 100644 --- a/src/green-app/src/components/ui/command.tsx +++ b/src/green-app/src/components/ui/command.tsx @@ -114,7 +114,7 @@ const CommandItem = React.forwardRef< , 'href'>, + VariantProps { + href: Url + selected?: boolean + external?: boolean +} + +const Link = React.forwardRef( + ({ className, selected, children, external, href, variant, ...props }, ref) => { + return ( + + {children} + {external && ( + + + + )} + + ) + } +) +Link.displayName = 'Link' + +export { Link, linkVariants } diff --git a/src/green-app/src/config/i18n.config.ts b/src/green-app/src/config/i18n.config.ts new file mode 100644 index 00000000..907ac9e6 --- /dev/null +++ b/src/green-app/src/config/i18n.config.ts @@ -0,0 +1,70 @@ +import { Resource, createInstance, i18n } from 'i18next' +import { initReactI18next } from 'react-i18next/initReactI18next' +import resourcesToBackend from 'i18next-resources-to-backend' +import { Config } from 'next-i18n-router/dist/types' +import dayjs from 'dayjs' +import updateLocale from 'dayjs/plugin/updateLocale' +dayjs.extend(updateLocale) + +export const i18nConfig: Config = { + locales: ['en', 'de', 'fr', 'ja', 'ko', 'pt', 'ru', 'zh', 'es'], + defaultLocale: 'en' +} + +export const namespaces = ['common', 'notification'] +export const defaultNS = 'common' + +export default async function initTranslations( + locale: string, + i18nInstance?: i18n, + resources?: Resource +) { + i18nInstance = i18nInstance || createInstance() + + i18nInstance.use(initReactI18next) + + if (!resources) { + i18nInstance.use( + resourcesToBackend( + (language: string, namespace: string) => import(`@/locales/${language}/${namespace}.json`) + ) + ) + } + + await i18nInstance.init({ + lng: locale, + resources, + fallbackLng: i18nConfig.defaultLocale, + supportedLngs: i18nConfig.locales, + defaultNS: defaultNS, + fallbackNS: defaultNS, + ns: namespaces, + preload: resources ? [] : i18nConfig.locales + }) + + const t = i18nInstance.t + + dayjs.updateLocale('en', { + relativeTime: { + future: t('relativeTime.future'), + past: t('relativeTime.past'), + s: t('relativeTime.s'), + m: t('relativeTime.m'), + mm: t('relativeTime.mm'), + h: t('relativeTime.h'), + hh: t('relativeTime.hh'), + d: t('relativeTime.d'), + dd: t('relativeTime.dd'), + M: t('relativeTime.M'), + MM: t('relativeTime.MM'), + y: t('relativeTime.y'), + yy: t('relativeTime.yy') + } + }) + + return { + i18n: i18nInstance, + resources: i18nInstance.services.resourceStore.data, + t + } +} diff --git a/src/green-app/src/hooks/useDivinePrice.ts b/src/green-app/src/hooks/useDivinePrice.ts index 2fd56242..055fb9c2 100644 --- a/src/green-app/src/hooks/useDivinePrice.ts +++ b/src/green-app/src/hooks/useDivinePrice.ts @@ -1,10 +1,10 @@ import { currentDivinePriceAtom } from '@/components/providers' import { DEFAULT_VALUATION_INDEX } from '@/lib/constants' import { listValuations } from '@/lib/http-util' -import { ItemGroupingService } from '@/lib/item-grouping-service' import { useQuery } from '@tanstack/react-query' import { useSetAtom } from 'jotai' import { useEffect } from 'react' +import { ItemGroupingService } from 'sage-common' export const useDivinePrice = (league: string | null) => { const setDivinePrice = useSetAtom(currentDivinePriceAtom) diff --git a/src/green-app/src/hooks/useToast.ts b/src/green-app/src/hooks/useToast.ts new file mode 100644 index 00000000..d7ba6473 --- /dev/null +++ b/src/green-app/src/hooks/useToast.ts @@ -0,0 +1,12 @@ +import { useNotificationStore } from '@/store/notificationStore' +import { useShallow } from 'zustand/react/shallow' + +export const useToast = () => { + const { addNotification: toast } = useNotificationStore( + useShallow((state) => ({ + addNotification: state.addNotification + })) + ) + + return toast +} diff --git a/src/green-app/src/hooks/useWhisperHashCopied.ts b/src/green-app/src/hooks/useWhisperHash.ts similarity index 53% rename from src/green-app/src/hooks/useWhisperHashCopied.ts rename to src/green-app/src/hooks/useWhisperHash.ts index df28f8ad..3e4a7e47 100644 --- a/src/green-app/src/hooks/useWhisperHashCopied.ts +++ b/src/green-app/src/hooks/useWhisperHash.ts @@ -1,10 +1,13 @@ -import { useListingsStore } from '@/app/listings/listingsStore' +import { useListingsStore } from '@/app/[locale]/listings/listingsStore' +import { currentUserAtom } from '@/components/providers' import { generateHashFromListing } from '@/lib/hash-utils' -import { NotificationCreate, postNotifications } from '@/lib/http-util' +import { NotificationCreate, listCharacters, postNotifications } from '@/lib/http-util' +import { PoeCharacter } from '@/types/poe-api-models' import { SageListingType } from '@/types/sage-listing-type' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { RowSelectionState } from '@tanstack/react-table' -import { useMemo } from 'react' +import { useAtomValue } from 'jotai' +import { useCallback, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' // Hash; SelectedQuantity @@ -12,15 +15,18 @@ export type NotificationItem = [string, number] export type NotificationBody = { uuid: string + ign: string items: NotificationItem[] } const createNotificationBody = ( listing: SageListingType, - selectedItemsMap: RowSelectionState + selectedItemsMap: RowSelectionState, + ign: string ): NotificationBody => { const body: NotificationBody = { uuid: listing.uuid, + ign, items: [] } if (listing.meta.listingMode === 'bulk') { @@ -45,7 +51,23 @@ const createNotificationBody = ( export const useWhisperHashCopied = ( listing: SageListingType -): [boolean, boolean, boolean, (() => void) | undefined] => { +): [boolean, boolean, boolean, boolean, (() => void) | undefined] => { + const currentUser = useAtomValue(currentUserAtom) + + const selectCurrentCharacter = useCallback((characters: PoeCharacter[] | undefined) => { + const currentChar = characters?.find((c) => c.current) + return currentChar?.name + }, []) + + const { data: currentIgn, isFetching: isCharactersFetching } = useQuery({ + queryKey: [currentUser?.profile?.uuid, 'characters'], + queryFn: () => listCharacters(), + select: selectCurrentCharacter, + staleTime: 5 * 60 * 1000, + gcTime: 5 * 60 * 1000, + enabled: !!currentUser?.profile?.uuid + }) + const mutation = useMutation({ mutationFn: (notification: NotificationCreate) => postNotifications(notification) }) @@ -65,19 +87,25 @@ export const useWhisperHashCopied = ( useShallow((state): [boolean, boolean, (() => void) | undefined] => { if (!generatedHash) return [false, false, undefined] const setMessageCopiedFn = () => { - if (!state.whisperedListings[listing.uuid]?.sentHashes.includes(generatedHash)) { - console.log('Send notification') - const body: any = {} - body.uuid = listing.uuid - body.items = listing.items - - mutation.mutate({ - targetId: listing.userId, - type: 'offering-buy', // Maybe offering-buy-update later to update the promise - body: createNotificationBody(listing, selectedItemsMap) - }) + if ( + !state.whisperedListings[listing.uuid]?.sentHashes.includes(generatedHash) && + currentIgn + ) { + mutation.mutate( + { + targetId: listing.userId, + type: 'offering-buy', // Maybe offering-buy-update later to update the promise + body: createNotificationBody(listing, selectedItemsMap, currentIgn) + }, + { + onSuccess: () => { + state.addWhisperedListing(listing.uuid, generatedHash) + } + } + ) + } else { + state.addWhisperedListing(listing.uuid, generatedHash) } - state.addWhisperedListing(listing.uuid, generatedHash) } if (!state.whisperedListings[listing.uuid]) { return [false, false, setMessageCopiedFn] @@ -90,5 +118,11 @@ export const useWhisperHashCopied = ( }) ) - return [disabled, messageCopied, messageSent, setMessageCopied] + return [ + disabled, + mutation.isPending || isCharactersFetching, + messageCopied, + messageSent, + setMessageCopied + ] } diff --git a/src/green-app/src/lib/currency.ts b/src/green-app/src/lib/currency.ts index da99d072..242371d5 100644 --- a/src/green-app/src/lib/currency.ts +++ b/src/green-app/src/lib/currency.ts @@ -1,11 +1,12 @@ -export type CurrencySwitch = "chaos" | "divine" | "both"; +export type CurrencySwitch = 'chaos' | 'divine' | 'both' export const chaosIcon = - "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lSZXJvbGxSYXJlIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/d119a0d734/CurrencyRerollRare.png"; + 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lSZXJvbGxSYXJlIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/d119a0d734/CurrencyRerollRare.png' export const divineIcon = - "https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lNb2RWYWx1ZXMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/e1a54ff97d/CurrencyModValues.png"; + 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ3VycmVuY3lNb2RWYWx1ZXMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/e1a54ff97d/CurrencyModValues.png' -export const round = (value: number, fraction = 2) => Math.round(value * 10 ** fraction) / 10 ** fraction; +export const round = (value: number, fraction = 2) => + Math.round(value * 10 ** fraction) / 10 ** fraction export const formatValue = ( inputValue?: number, @@ -15,48 +16,57 @@ export const formatValue = ( showChange = false, valueShort = true ) => { - let value = inputValue ?? 0; - let absValue: number = Math.abs(value); - let formattedValue = "0"; - let formattedPrimaryValue: number | undefined; // Chaos value if divine - let currencyShort: "c" | "div" = "c"; - let icon = chaosIcon; - let parsedValue = value >= 10 ? round(value, 1) : round(value, 2); - if (toCurrency === "divine" || (toCurrency === "both" && divinePrice && absValue >= divinePrice)) { + let value = inputValue ?? 0 + let absValue: number = Math.abs(value) + let formattedValue = '0' + let formattedPrimaryValue: number | undefined // Chaos value if divine + let currencyShort: 'c' | 'div' = 'c' + let icon = chaosIcon + let parsedValue = value >= 10 ? round(value, 1) : round(value, 2) + if ( + toCurrency === 'divine' || + (toCurrency === 'both' && divinePrice && absValue >= divinePrice) + ) { if (divinePrice && divinePrice !== 0 && absValue !== 0) { - absValue /= divinePrice; - value /= divinePrice; + absValue /= divinePrice + value /= divinePrice } - currencyShort = "div"; - icon = divineIcon; + currencyShort = 'div' + icon = divineIcon - parsedValue = value >= 10 ? round(value, 1) : round(value, 2); + parsedValue = value >= 10 ? round(value, 1) : round(value, 2) if (splitIcons && divinePrice && divinePrice !== 0 && absValue !== 0) { - formattedPrimaryValue = Math.trunc(value); - absValue = (absValue - Math.trunc(absValue)) * divinePrice; - value = (value - Math.trunc(value)) * divinePrice; - icon = chaosIcon; + formattedPrimaryValue = Math.trunc(value) + absValue = (absValue - Math.trunc(absValue)) * divinePrice + value = (value - Math.trunc(value)) * divinePrice + icon = chaosIcon } } - const prefix = value < 0 ? "− " : showChange && value > 0 ? "+ " : ""; + const prefix = value < 0 ? '− ' : showChange && value > 0 ? '+ ' : '' if (valueShort && absValue >= 1_000_000) { formattedValue = `${prefix}${(absValue / 1_000_000).toLocaleString(undefined, { - maximumFractionDigits: 0, - })}kk`; + maximumFractionDigits: 0 + })}kk` } else if (valueShort && absValue >= 1_000) { formattedValue = `${prefix}${(absValue / 1_000).toLocaleString(undefined, { minimumFractionDigits: 1, - maximumFractionDigits: 1, - })}k`; + maximumFractionDigits: 1 + })}k` } else { formattedValue = `${prefix}${absValue.toLocaleString(undefined, { - maximumFractionDigits: absValue >= 10 ? 1 : 2, - })}`; + maximumFractionDigits: absValue >= 10 ? 1 : 2 + })}` } - return { formattedValue, formattedPrimaryValue, value: parsedValue, currency: currencyShort, icon }; -}; + return { + formattedValue, + formattedPrimaryValue, + value: parsedValue, + currency: currencyShort, + icon + } +} -export type FormattedValue = ReturnType; +export type FormattedValue = ReturnType diff --git a/src/green-app/src/lib/http-util.ts b/src/green-app/src/lib/http-util.ts index 690cac6c..287e7e2b 100644 --- a/src/green-app/src/lib/http-util.ts +++ b/src/green-app/src/lib/http-util.ts @@ -1,14 +1,14 @@ 'use client' -import Axios from 'axios' -import { lowerCaseCompasses } from './compasses' -import { SageDatabaseOfferingType, SageOfferingType } from '@/types/sage-listing-type' -import { IStashTab } from '@/types/echo-api/stash' -import { SageValuationShard, SageValuationShardInternal } from '@/types/echo-api/valuation' import { SageItemGroupSummaryShard, SageItemGroupSummaryShardInternal } from '@/types/echo-api/item-group' +import { IStashTab } from '@/types/echo-api/stash' +import { SageValuationShard, SageValuationShardInternal } from '@/types/echo-api/valuation' import { PoeCharacter, PoeLeague } from '@/types/poe-api-models' +import { SageDatabaseOfferingType, SageOfferingType } from '@/types/sage-listing-type' +import Axios from 'axios' +import { LISTING_CATEGORIES } from './listing-categories' const dev = false const baseUrl = dev ? 'http://localhost:3001' : 'https://green-api.poestack.com' @@ -29,7 +29,7 @@ export function postListing(listing: SageDatabaseOfferingType) { export async function listSummaries(tag: string) { const resp = await Axios.get( - `https://pub-1ac9e2cd6dca4bda9dc260cb6a6f7c90.r2.dev/v10/summaries/${tag}.json`, + `https://pub-1ac9e2cd6dca4bda9dc260cb6a6f7c90.r2.dev/v10/summaries/${tag.replaceAll(' ', '_')}.json`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` @@ -54,23 +54,24 @@ export async function authDiscord(code: string) { } export async function listMyListings() { - const resp = await Axios.get(`${baseUrl}/my/listings`, { + const resp = await Axios.get(`${baseUrl}/my/listings`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` } }) - const listings = resp.data as string[] - return listings + return resp.data .map((listing): SageOfferingType => { - const parsed: SageDatabaseOfferingType = JSON.parse(listing) - return { - ...parsed, + ...listing, meta: { - ...parsed.meta, - timestampMs: parsed.meta.timestampMs - 2000, // ??? Somehow there is a difference between client & server of approx. 1-3 second - totalPrice: parsed.items.reduce((sum, item) => item.price * item.quantity + sum, 0) + ...listing.meta, + subCategory: + !listing.meta.subCategory || listing.meta.subCategory === 'ALL' + ? '' + : listing.meta.subCategory, + timestampMs: listing.meta.timestampMs - 2000, // ??? Somehow there is a difference between client & server of approx. 1-3 second + totalPrice: listing.items.reduce((sum, item) => item.price * item.quantity + sum, 0) } } }) @@ -81,7 +82,7 @@ export type SageDatabaseOfferingTypeExt = Awaited( `${baseUrl}/listings/${league.toLowerCase()}/${category}/${includeTimeOffset(startTimeMs)}`, { headers: { @@ -101,6 +102,11 @@ export async function listListings(league: string, category: string, startTimeMs } } ) + resp.data.forEach((listing) => { + if (!listing.meta.subCategory || listing.meta.subCategory === 'ALL') { + listing.meta.subCategory = '' + } + }) return resp.data } @@ -132,63 +138,64 @@ export async function postNotifications(notification: NotificationCreate) { export type Notification = { id: string - timestamp: string + timestamp: number type: string targetId: string senderId: string body: string } -export async function listNotifications( - startTimeMs: number -): Promise<{ notifications: Notification[] }> { - const resp = await Axios.get(`${baseUrl}/notifications/${includeTimeOffset(startTimeMs)}`, { - headers: { - Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` +export async function listNotifications(startTimeMs: number) { + const resp = await Axios.get<{ notifications: Notification[] }>( + `${baseUrl}/notifications/${includeTimeOffset(startTimeMs)}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` + } } - }) + ) return resp.data } //#region GGG-API export async function listStashes(league: string) { - const resp = await Axios.get(`${baseUrl}/stashes/${league}`, { + const resp = await Axios.get(`${baseUrl}/stashes/${league}`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` }, timeout: 6 * 60 * 1000 }) - return resp.data as IStashTab[] + return resp.data } export async function listStash(league: string, stashId: string) { - const resp = await Axios.get(`${baseUrl}/stash/${league}/${stashId}`, { + const resp = await Axios.get(`${baseUrl}/stash/${league}/${stashId}`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` }, timeout: 6 * 60 * 1000 }) - return resp.data as IStashTab + return resp.data } export async function listCharacters() { - const resp = await Axios.get(`${baseUrl}/characters`, { + const resp = await Axios.get(`${baseUrl}/characters`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` }, timeout: 6 * 60 * 1000 }) - return resp.data as PoeCharacter[] + return resp.data } export async function listLeagues() { - const resp = await Axios.get(`${baseUrl}/leagues`, { + const resp = await Axios.get(`${baseUrl}/leagues`, { headers: { Authorization: `Bearer ${localStorage.getItem('doNotShareJwt')}` }, timeout: 6 * 60 * 1000 }) - return resp.data as PoeLeague[] + return resp.data } //#endregion @@ -230,7 +237,8 @@ function itemGroupMapInternalToExternal( internal: SageItemGroupSummaryShardInternal ): SageItemGroupSummaryShard { const out: SageItemGroupSummaryShard = { - meta: internal.meta, + // FIXME: Return the original tag + meta: { ...internal.meta, tag: internal.meta.tag.replaceAll('_', ' ') }, summaries: {} } @@ -246,8 +254,19 @@ function itemGroupMapInternalToExternal( tag: out.meta.tag } - if (out.meta?.tag === 'compass' || out.meta?.tag === 'compasses') { - out.summaries[k]['displayName'] = lowerCaseCompasses[v?.k] + const tag = out.meta.tag + if (tag) { + const parseFn = LISTING_CATEGORIES.find((cat) => cat.tags.includes(tag))?.parseName + if (parseFn) { + out.summaries[k]['displayName'] = + parseFn({ + group: { + tag: out.meta.tag, + key: out.summaries[k].key, + unsafeHashProperties: out.summaries[k].unsafeHashProperties + } + }) || '' + } } }) diff --git a/src/green-app/src/lib/item-grouping-service.ts b/src/green-app/src/lib/item-grouping-service.ts deleted file mode 100644 index fafc8e93..00000000 --- a/src/green-app/src/lib/item-grouping-service.ts +++ /dev/null @@ -1,1258 +0,0 @@ -import { ItemUtils } from './item-util' -import objectHash from 'object-hash' -import { from, map } from 'rxjs' -import { PoeItem } from '@/types/poe-api-models' - -export type SageItemGroup = { key: string; tag: string; hash: string; unsafeHashProperties: any } - -export class ItemGroupingService { - private readonly pricingHandlers: ItemGroupIdentifier[] = [] - - constructor() { - this.pricingHandlers.push( - new InscribedUltimatumGroupIndentifier(), - new RewardMapGroupIdentifier(), - new WatchersEyeGroupIdentifier(), - new ScarabGroupIdentifier(), - new CardGroupIdentifier(), - new RelicGroupIdentifier(), - new TimelessJewelGroupIdentifier(), - new TattooGroupIdentifier(), - new BloodFilledVesselGroupIdentifier(), - new UnqiueGearGroupIdentifier(), - new CorpseGroupIdentifier(), - new TomeGroupIdentifier(), - new BeastGroupIdentifier(), - new MemoryGroupIdentifier(), - new HeistBlueprintsGroupIdentifier(), - new GemGroupIdentifier(), - new HeistContractsGroupIdentifier(), - new LogbookGroupIdentifier(), - new ClusterGroupIdentifier(), - new MapGroupIdentifier(), - new CompassGroupIdentifier(), - new IncubatorGroupIdentifier(), - new CurrencyGroupIdentifier() - ) - } - - public withGroupObserable(items: PoeItem[]) { - return from(items).pipe( - map((item) => ({ - data: item, - group: this.group(item) - })) - ) - } - - public withGroup(items: PoeItem[]) { - return items.map((item) => ({ - data: item, - group: this.group(item) - })) - } - - public group(item: PoeItem): { primaryGroup: SageItemGroup } | null { - if (item.lockedToAccount || item.lockedToCharacter) { - return null - } - - for (const groupIdentifier of this.pricingHandlers) { - const internalGroup: InternalGroup | null = groupIdentifier.group(item) - if (internalGroup) { - const hash = objectHash( - { - tag: internalGroup.tag, - key: internalGroup.key, - propertiesHash: internalGroup.hashProperties - }, - { unorderedArrays: true, unorderedObjects: true } - ) - - return { - primaryGroup: { - key: internalGroup.key, - tag: internalGroup.tag, - hash: hash, - unsafeHashProperties: internalGroup.hashProperties - } - } - } - } - - return null - } -} - -interface InternalGroup { - key: string - tag: string - hashProperties: Record - displayOverride?: string -} - -export interface ItemGroupIdentifier { - group: (item: PoeItem) => InternalGroup | null -} - -export class InscribedUltimatumGroupIndentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.baseType === 'Inscribed Ultimatum') { - const challenge = item.properties?.filter((prop) => prop.name === 'Challenge')?.[0] - ?.values?.[0]?.[0] - const reward = item.properties?.filter((prop) => prop.name?.startsWith('Reward'))?.[0] - ?.values?.[0]?.[0] - const sacrifice = item.properties?.filter((prop) => - prop.name?.startsWith('Requires Sacrifice') - )?.[0]?.values?.[0]?.[0] - - const group: InternalGroup = { - key: `${reward}`, - tag: 'inscribed ultimatum', - hashProperties: { - challenge: challenge, - reward: reward, - sacrifice: sacrifice - } - } - return group - } - return null - } -} - -export class RelicGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.typeLine?.endsWith(' Relic') && item.rarity === 'Unique') { - const group: InternalGroup = { - key: item.name!!.toLowerCase(), - tag: 'relic', - hashProperties: {} - } - return group - } - return null - } -} - -export class TimelessJewelGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.typeLine?.toLowerCase().includes('timeless jewel') && item.identified) { - const group: InternalGroup = { - key: item.name!!.toLowerCase(), - tag: 'timeless jewel', - hashProperties: { - mods: item.explicitMods!! - } - } - return group - } - return null - } -} - -export class CorpseGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const typeLine = item.typeLine?.toLowerCase() ?? '' - if (item.descrText === 'Right click this item to create this corpse.') { - return { - key: typeLine, - tag: 'corpse', - hashProperties: {} - } - } - return null - } -} - -export class ScarabGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const typeLine = item.typeLine?.toLowerCase() ?? '' - if ( - item.descrText === 'Can be used in a personal Map Device to add modifiers to a Map.' && - typeLine.endsWith(' scarab') - ) { - return { - key: typeLine, - tag: 'scarab', - hashProperties: {} - } - } - return null - } -} - -export class CardGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.icon?.endsWith('InventoryIcon.png')) { - const typeLine = item.typeLine?.toLowerCase() ?? '' - return { - key: typeLine, - tag: 'card', - hashProperties: {} - } - } - return null - } -} - -export class WatchersEyeGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if ( - (item.frameType === 3 || item.frameType === 10) && - item.baseType?.toLowerCase() === 'prismatic jewel' && - item.icon?.toLowerCase()?.includes('elderjewel.png') && - !item.identified - ) { - const ilvl = item.ilvl ?? item.itemLevel - const group: InternalGroup = { - key: "unidentified watcher's eye", - tag: 'unique', - hashProperties: { - ilvl: ilvl!! - } - } - return group - } - return null - } -} - -export class TomeGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.baseType?.toLowerCase() === 'forbidden tome' && item.identified) { - const ilvl = item.ilvl ?? item.itemLevel - const group: InternalGroup = { - key: 'forbidden tome', - tag: 'forbidden tome', - hashProperties: { - ilvl: ilvl!! - } - } - return group - } - return null - } -} - -export class TattooGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.baseType?.toLowerCase().startsWith('tattoo of the')) { - const group: InternalGroup = { - key: item.baseType?.toLowerCase().replace('tattoo of the', ''), - tag: 'tattoo', - hashProperties: {} - } - return group - } - return null - } -} - -export class BloodFilledVesselGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.baseType?.toLowerCase() === 'blood-filled vessel') { - const monsterLvl = item.properties?.find((p) => p.name === 'Monster Level')?.values?.[0]?.[0] - - if (monsterLvl) { - const group: InternalGroup = { - key: 'blood-filled vessel', - tag: 'fragment', - hashProperties: { - monsterLvl: parseInt(monsterLvl) - } - } - return group - } - } - return null - } -} - -export class UnqiueGearGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const key = item.name?.toLowerCase() - if ((item.frameType === 3 || item.frameType === 10) && key?.length) { - const baseGroup: InternalGroup = { - key: item.name!!.toLowerCase(), - tag: 'unique', - hashProperties: { - sixLink: false, - corruptedMods: null, - enchantMods: null - } - } - const itemCategory = ItemUtils.decodeIcon(item.icon!!, 0) - - let res = baseGroup - if (item.sockets?.filter((s) => s.group === 0).length === 6) { - res = { - ...res, - hashProperties: { ...res.hashProperties, sixLink: true } - } - } - - if (item.corrupted) { - const corruptedMods = item.implicitMods?.map((e) => e.replaceAll(/[0-9]+/g, '#')) ?? [] - corruptedMods.sort() - res = { - ...res, - hashProperties: { - ...res.hashProperties, - corruptedMods: corruptedMods.map((e) => e.toLowerCase()) - } - } - } else if (itemCategory?.includes('helmet')) { - const mappedEnchantMods = (item.enchantMods ?? []).map((e) => e.toLowerCase()) - mappedEnchantMods.sort() - if (mappedEnchantMods.length) { - res = { - ...res, - hashProperties: { - ...res.hashProperties, - enchantMods: mappedEnchantMods - } - } - } - } - - return res - } - return null - } -} - -export class BeastGroupIdentifier implements ItemGroupIdentifier { - - private sellableRedBeasts = [ - "craicic chimeral", - "vivid vulture", - "wild hellion alpha", - "vivid abberarach", - "farric lynx alpha", - "farric wolf alpha", - "craicic savage crab", - "saqawine cobra", - "primal rhex matriarch", - "vivid watcher", - "wild bristle matron", - "fenumal plagued arachnid", - "wild brambleback", - "craicic maw ", - "farric frost hellion alpha", - "farric tiger alpha", - ] - - private beastMods = [ - "fertile presence", - "aspect of the hellion", - "farric presence", - "satyr storm", - "tiger prey", - "spectral swipe", - "deep one's presence", - "churning claws", - "winter bloom", - "craicic presence", - "crushing claws", - "hadal dive", - "raven caller", - "putrid flight", - "saqawine presence", - "vile hatchery", - "spectral stampede", - "fenumal presence", - "unstable swarm", - "blood geyser", - "crimson flock", - "incendiary mite", - "infested earth", - ] - - group(item: PoeItem): InternalGroup | null { - if (item?.descrText === 'Right-click to add this to your bestiary.') { - const beastModCount = (item.explicitMods ?? []).filter((e) => this.beastMods.includes(e.toLowerCase())).length - - const baseType = item.baseType?.toLocaleLowerCase() ?? "" - if (this.sellableRedBeasts.includes(baseType) || item?.rarity === "Unique") { - return { - key: baseType!!, - tag: 'beast', - hashProperties: {} - } - } - else { - return { - key: beastModCount === 1 ? "yellow beast" : "red beast", - tag: 'beast', - hashProperties: {} - } - } - } - - return null - } -} - -export class MemoryGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const typeLine = item.typeLine?.toLocaleLowerCase() - if ( - typeLine && - item.descrText === - 'Right-click on this, then left click on a completed Map on your Atlas to apply this Memory.' - ) { - return { - key: typeLine, - tag: 'atlas memory', - hashProperties: {} - } - } - - return null - } -} - -export class GemGroupIdentifier implements ItemGroupIdentifier { - private readonly exceptionalGems: string[] = [ - 'enlighten', - 'empower', - 'enhance', - 'enlighten support', - 'empower support', - 'enhance support' - ] - - private convertLvlToRange(typeLine: string, lvl: number): string { - if (this.exceptionalGems.includes(typeLine) || lvl >= 20 || typeLine.includes('awakened')) { - return lvl.toString() - } - if (lvl >= 20 || typeLine.includes('awakened')) { - return lvl.toString() - } - return '1-19' - } - - private convertQToRange(typeLine: string, quality: number): string { - if (this.exceptionalGems.includes(typeLine) || typeLine.includes('awakened')) { - return 'any' - } - if (quality >= 20 || quality === 0) { - return quality.toString() - } - return '1-19' - } - - group(item: PoeItem): InternalGroup | null { - if (item.support === true || item.support === false) { - const typeLine = item.typeLine!!.toLowerCase() - const quality = this.convertQToRange( - typeLine, - parseInt( - item.properties - ?.filter((p) => p.name === 'Quality')?.[0] - ?.values?.[0]?.[0]?.replace(/[^0-9]/g, '') ?? '0' - ) - ) - const lvl = this.convertLvlToRange( - typeLine, - parseInt( - item.properties - ?.filter((p) => p.name === 'Level')?.[0] - ?.values?.[0]?.[0]?.replace(/[^0-9]/g, '') ?? '1' - ) - ) - - return { - key: typeLine, - tag: 'gem', - hashProperties: { - lvl: lvl ?? '1', - corrupted: !!item.corrupted, - quality: quality ?? '0' - } - } - } - - return null - } -} - -export class HeistBlueprintsGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.frameType === 3 || item.frameType === 10) return null - - const baseType = item.baseType?.toLowerCase() - if (baseType?.includes('blueprint:')) { - const wingsRevealed = item.properties?.filter((p) => p.name === 'Wings Revealed')?.[0] - ?.values?.[0]?.[0] - const target = item.properties - ?.filter((p) => p.name === 'Heist Target: {0}')?.[0] - ?.values?.[0]?.[0]?.toLowerCase() - const ilvl = item['ilvl'] ?? item.itemLevel - const totalWings = wingsRevealed?.split('/')?.[1] - const fullyRevealed = wingsRevealed?.split('/')?.[0] === totalWings - if (totalWings && ilvl && target) { - return { - key: target + ' blueprint', - tag: 'blueprint', - hashProperties: { - ilvl: ilvl >= 81 ? '81+' : '<81', - totalWings: parseInt(totalWings), - fullyRevealed - } - } - } - } - - return null - } -} - -export class HeistContractsGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.frameType === 3 || item.frameType === 10) return null - - const baseType = item.baseType?.toLowerCase() - - if (baseType?.includes('contract:') && !item.corrupted && !item.split) { - const ilvl = item['ilvl'] ?? item.itemLevel - const type = item.properties - ?.filter((p) => p.name === 'Requires {1} (Level {0})')?.[0] - ?.values?.[1]?.[0]?.toLowerCase() - if (ilvl && type) { - return { - key: type + ' contract', - tag: 'contract', - hashProperties: { - ilvl: ilvl >= 81 ? '83+' : '<83' - } - } - } - } - - return null - } -} - -export class LogbookGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const baseType = item.baseType?.toLowerCase() - - if (baseType?.includes('logbook')) { - const ilvl = item.ilvl ?? item.itemLevel - const factions = item.logbookMods?.map((m) => m?.faction?.name?.toLowerCase()) ?? [] - - let faction = null - if (factions.includes('knights of the sun')) { - faction = 'knights of the sun' - } else if (factions.includes('black scythe mercenaries')) { - faction = 'black scythe mercenaries' - } else if (factions.includes('order of the chalice')) { - faction = 'order of the chalice' - } else if (factions.includes('druids of the broken circle')) { - faction = 'druids of the broken circle' - } - - if (ilvl && faction) { - return { - key: faction + ' logbook', - tag: 'logbook', - hashProperties: { - ilvl: ilvl >= 83 ? '83+' : '<83', - corrupted: !!item.corrupted, - split: !!item.split - } - } - } - } - - return null - } -} - -export class CompassGroupIdentifier implements ItemGroupIdentifier { - private usesToString(uses: number): string { - if (uses === 4 || uses === 16) { - return `${uses}` - } - - if (uses < 4) { - return '<4' - } - - if (uses > 4 && uses < 16) { - return '5-15' - } - - return `${uses}` - } - - group(item: PoeItem): InternalGroup | null { - const baseType = item.baseType?.toLowerCase() - - if (baseType === 'charged compass') { - const usesMod = item - .enchantMods!!.filter( - (mod) => mod.includes(' uses remaining') || mod.includes(' use remaining') - )?.[0] - ?.replace(' uses remaining', '') - const otherMods = item - .enchantMods!!.filter( - (mod) => !mod.includes('uses remaining') && !mod.includes('use remaining') - ) - .join(' ') - .toLowerCase() - - return { - key: otherMods + ' compass', - tag: 'compass', - hashProperties: { - uses: this.usesToString(parseInt(usesMod)) - } - } - } - - return null - } -} - -export class ClusterGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - if (item.frameType === 3 || item.frameType === 10) return null - const baseType = item.baseType?.toLowerCase() - if (baseType && baseType.includes('cluster')) { - const numberPassiveSkills = - item.enchantMods - ?.filter((x) => x.startsWith('Adds ') && x.endsWith(' Passive Skills'))?.[0] - .split(' ')[1] ?? '-1' - const clusterType = item.enchantMods - ?.filter((x) => x.startsWith('Added Small Passive Skills grant: '))?.[0] - .replace('Added Small Passive Skills grant: ', '') - .toLowerCase() - const ilvl = item['ilvl'] ?? item.itemLevel - if (clusterType && numberPassiveSkills && ilvl) { - return { - key: baseType, - tag: 'cluster', - hashProperties: { - ilvl: ilvl, - clusterType: clusterType, - passives: parseInt(numberPassiveSkills) - } - } - } - } - return null - } -} - -export class IncubatorGroupIdentifier implements ItemGroupIdentifier { - incubatorBaseTypes = new Set([ - 'ornate incubator', - "diviner's incubator", - 'otherworldly incubator', - 'kalguuran incubator', - "thaumaturge's incubator", - 'infused incubator', - 'foreboding incubator', - "cartographer's incubator", - 'skittering incubator', - 'blighted incubator', - 'singular incubator', - 'primal incubator', - "celestial jeweller's incubator", - 'fossilised incubator', - 'abyssal incubator', - 'mysterious incubator', - "celestial armoursmith's incubator", - "geomancer's incubator", - 'fragmented incubator', - "celestial blacksmith's incubator", - 'maddening incubator', - 'morphing incubator', - 'enchanted incubator', - 'obscured incubator' - ]) - - group(item: PoeItem): InternalGroup | null { - const baseType = item.baseType?.toLowerCase() - if (baseType && this.incubatorBaseTypes.has(baseType)) { - const ilvl = item.ilvl ?? item.itemLevel - return { - key: baseType, - tag: 'incubator', - hashProperties: { - ilvl: ilvl!! - } - } - } - return null - } -} - -export class RewardMapGroupIdentifier implements ItemGroupIdentifier { - group(item: PoeItem): InternalGroup | null { - const mapTier = item.properties?.filter((prop) => prop.name === 'Map Tier')?.[0] - ?.values?.[0]?.[0] - const reward = item.properties?.filter((prop) => prop.name === 'Reward')?.[0]?.values?.[0]?.[0] - - if (mapTier?.length && reward?.length && item?.name?.length) { - return { - key: `${item.name} ${item.typeLine}`, - tag: 'reward map', - hashProperties: { - map: item.typeLine, - mapTier: mapTier, - reward: reward, - mods: item.explicitMods - } - } - } - - return null - } -} - -export class MapGroupIdentifier implements ItemGroupIdentifier { - private readonly mapImplicits: { [key: string]: string } = { - "Map contains Al-Hezmin's Citadel\nItem Quantity increases amount of Rewards Al-Hezmin drops by 20% of its value": - "al-hezmin's map", - "Map contains Veritania's Citadel\nItem Quantity increases amount of Rewards Veritania drops by 20% of its value": - "veritania's map", - "Map contains Baran's Citadel\nItem Quantity increases amount of Rewards Baran drops by 20% of its value": - "baran's map", - "Map contains Drox's Citadel\nItem Quantity increases amount of Rewards Drox drops by 20% of its value": - "drox's map", - 'Map is occupied by The Purifier': "purifier's map", - 'Map is occupied by The Constrictor': "constrictor's map", - 'Map is occupied by The Enslaver': "enslaver's map", - 'Map is occupied by The Eradicator': "eradicator's map" - } - - group(item: PoeItem): InternalGroup | null { - const baseType = item.baseType?.toLowerCase() - const mapTier = item.properties?.filter((prop) => prop.name === 'Map Tier')?.[0] - ?.values?.[0]?.[0] - - if (mapTier) { - const specialMapType = item.implicitMods - ?.map((mod) => this.mapImplicits[mod]) - .filter((group) => !!group) - .join(',') - - return { - key: (specialMapType || baseType)!!, - tag: 'map', - hashProperties: { - tier: mapTier - } - } - } - - return null - } -} - -export class CurrencyGroupIdentifier implements ItemGroupIdentifier { - private readonly currencyGroupIds = { - currency: { - items: new Set([ - 'gold', - 'chaos orb', - "rogue's marker", - 'mirror of kalandra', - 'mirror shard', - 'fracturing orb', - 'tempering orb', - 'orb of dominance', - 'secondary regrading lens', - "maven's orb", - 'divine orb', - 'tailoring orb', - 'tainted divine teardrop', - "hunter's exalted orb", - 'prime regrading lens', - 'elevated sextant', - 'sacred crystallised lifeforce', - 'fracturing shard', - 'tainted exalted orb', - "awakener's orb", - 'orb of conflict', - "crusader's exalted orb", - "warlord's exalted orb", - "redeemer's exalted orb", - 'eldritch chaos orb', - 'sacred orb', - 'blessing of chayula', - 'exceptional eldritch ember', - 'eldritch orb of annulment', - 'exceptional eldritch ichor', - 'tainted orb of fusing', - 'exalted orb', - 'tainted blessing', - 'eldritch exalted orb', - 'tainted mythic orb', - 'ritual vessel', - 'blessing of uul-netol', - 'blessing of tul', - 'veiled chaos orb', - 'blessing of xoph', - 'blessing of esh', - 'grand eldritch ember', - 'oil extractor', - 'orb of annulment', - 'tainted chaos orb', - "surveyor's compass", - 'grand eldritch ichor', - 'ancient orb', - 'tainted chromatic orb', - 'awakened sextant', - 'stacked deck', - "harbinger's orb", - "gemcutter's prism", - 'orb of unmaking', - 'greater eldritch ember', - "tainted jeweller's orb", - 'regal orb', - 'exalted shard', - 'greater eldritch ichor', - 'annulment shard', - "tainted armourer's scrap", - 'orb of regret', - 'vaal orb', - 'blessed orb', - 'orb of scouring', - 'instilling orb', - "cartographer's chisel", - 'enkindling orb', - 'orb of horizons', - "glassblower's bauble", - 'lesser eldritch ember', - 'lesser eldritch ichor', - 'orb of fusing', - "tainted blacksmith's whetstone", - 'chromatic orb', - 'orb of alchemy', - 'orb of alteration', - 'orb of augmentation', - 'orb of binding', - 'orb of chance', - "engineer's orb", - "jeweller's orb", - 'portal scroll', - 'vivid crystallised lifeforce', - "blacksmith's whetstone", - "armourer's scrap", - 'orb of transmutation', - 'wild crystallised lifeforce', - 'primal crystallised lifeforce', - 'scroll of wisdom' - ]) - }, - 'scouting report': { - items: new Set([ - "explorer's scouting report", - 'vaal scouting report', - 'singular scouting report', - 'influenced scouting report', - 'blighted scouting report', - 'otherworldly scouting report', - 'comprehensive scouting report', - 'delirious scouting report', - "operative's scouting report" - ]) - }, - fossil: { - items: new Set([ - 'glyphic fossil', - 'faceted fossil', - 'fractured fossil', - 'corroded fossil', - 'hollow fossil', - 'shuddering fossil', - 'sanctified fossil', - 'perfect fossil', - 'bloodstained fossil', - 'aetheric fossil', - 'bound fossil', - 'prismatic fossil', - 'serrated fossil', - 'tangled fossil', - 'deft fossil', - 'gilded fossil', - 'jagged fossil', - 'aberrant fossil', - 'dense fossil', - 'pristine fossil', - 'lucent fossil', - 'fundamental fossil', - 'metallic fossil', - 'scorched fossil', - 'frigid fossil' - ]) - }, - resonator: { - items: new Set([ - 'prime chaotic resonator', - 'powerful chaotic resonator', - 'potent chaotic resonator', - 'primitive chaotic resonator' - ]) - }, - artifacts: { - items: new Set(['astragali', 'scrap metal', 'burial medallion', 'exotic coinage']) - }, - misc: { - items: new Set(['albino rhoa feather', "facetor's lens"]) - }, - vial: { - items: new Set([ - 'vial of consequence', - 'vial of the ghost', - 'vial of dominance', - 'vial of sacrifice', - 'vial of transcendence', - 'vial of summoning', - 'vial of awakening', - 'vial of the ritual', - 'vial of fate' - ]) - }, - oil: { - items: new Set([ - 'golden oil', - 'tainted oil', - 'silver oil', - 'opalescent oil', - 'black oil', - 'crimson oil', - 'reflective oil', - 'violet oil', - 'azure oil', - 'indigo oil', - 'teal oil', - 'verdant oil', - 'amber oil', - 'sepia oil', - 'clear oil' - ]) - }, - 'delirium orb': { - items: new Set([ - 'skittering delirium orb', - "diviner's delirium orb", - 'fine delirium orb', - "cartographer's delirium orb", - 'fragmented delirium orb', - 'abyssal delirium orb', - 'singular delirium orb', - 'fossilised delirium orb', - "thaumaturge's delirium orb", - 'foreboding delirium orb', - 'amorphous delirium orb', - "blacksmith's delirium orb", - 'blighted delirium orb', - 'timeless delirium orb', - 'whispering delirium orb', - 'imperial delirium orb', - "jeweller's delirium orb", - 'obscured delirium orb', - "armoursmith's delirium orb" - ]) - }, - invitation: { - items: new Set([ - 'incandescent invitation', - 'screaming invitation', - "maven's invitation: the elderslayers", - "maven's invitation: the formed", - "maven's invitation: the twisted", - "maven's invitation: the hidden", - "maven's invitation: the feared", - "maven's invitation: the forgotten", - 'polaric invitation', - "maven's invitation: the atlas", - 'writhing invitation' - ]) - }, - breach: { - items: new Set([ - "chayula's flawless breachstone", - "esh's flawless breachstone", - "uul-netol's flawless breachstone", - "xoph's flawless breachstone", - "chayula's pure breachstone", - "tul's flawless breachstone", - "chayula's enriched breachstone", - "uul-netol's pure breachstone", - "xoph's pure breachstone", - "tul's pure breachstone", - "esh's pure breachstone", - "xoph's enriched breachstone", - "uul-netol's enriched breachstone", - "chayula's charged breachstone", - "uul-netol's charged breachstone", - "esh's enriched breachstone", - "tul's enriched breachstone", - "uul-netol's breachstone", - "chayula's breachstone", - "esh's charged breachstone", - "xoph's charged breachstone", - "tul's charged breachstone", - "xoph's breachstone", - "tul's breachstone", - "esh's breachstone", - 'splinter of uul-netol', - 'splinter of chayula', - 'splinter of xoph', - 'splinter of tul', - 'splinter of esh' - ]) - }, - fragment: { - items: new Set([ - 'visceral reliquary key', - 'forgotten reliquary key', - 'archive reliquary key', - 'oubliette reliquary key', - 'cosmic reliquary key', - 'shiny reliquary key', - 'voidborn reliquary key', - 'gift to the goddess', - "the maven's writ", - 'fragment of shape', - 'fragment of knowledge', - 'sacred blossom', - 'fragment of emptiness', - 'timeless maraketh emblem', - 'dedication to the goddess', - 'unrelenting timeless maraketh emblem', - 'simulacrum', - 'fragment of eradication', - 'unrelenting timeless eternal emblem', - 'fragment of enslavement', - 'fragment of constriction', - 'fragment of purification', - "al-hezmin's crest", - "baran's crest", - "drox's crest", - "veritania's crest", - 'crescent splinter', - 'unrelenting timeless templar emblem', - 'fragment of the phoenix', - 'fragment of the hydra', - 'timeless templar emblem', - 'fragment of the minotaur', - 'mortal ignorance', - 'tribute to the goddess', - 'unrelenting timeless karui emblem', - 'unrelenting timeless vaal emblem', - 'mortal rage', - 'mortal hope', - 'fragment of the chimera', - 'mortal grief', - 'timeless vaal emblem', - 'timeless karui emblem', - 'fragment of terror', - 'divine vessel', - 'sacrifice at midnight', - 'sacrifice at noon', - 'offering to the goddess', - 'sacrifice at dawn', - 'timeless eternal emblem', - 'timeless maraketh splinter', - 'timeless templar splinter', - 'sacrifice at dusk', - 'timeless karui splinter', - 'timeless vaal splinter', - 'timeless eternal empire splinter', - 'simulacrum splinter' - ]) - }, - invocation: { - items: new Set([ - "lycia's invocation of avatar of fire", - "lycia's invocation of runebinder", - "lycia's invocation of perfect agony", - "lycia's invocation of hex master", - "lycia's invocation of minion instability", - "lycia's invocation of imbalanced guard", - "lycia's invocation of eternal youth", - "lycia's invocation of solipsism", - "lycia's invocation of blood magic", - "lycia's invocation of ghost reaver", - "lycia's invocation of crimson dance", - "lycia's invocation of arrow dancing", - "lycia's invocation of versatile combatant", - "lycia's invocation of divine shield", - "lycia's invocation of iron grip", - "lycia's invocation of precise technique", - "lycia's invocation of acrobatics", - "lycia's invocation of vaal pact", - "lycia's invocation of iron will", - "lycia's invocation of zealot's oath", - "lycia's invocation of wicked ward", - "lycia's invocation of ancestral bond", - "lycia's invocation of point blank", - "lycia's invocation of the agnostic", - "lycia's invocation of the impaler", - "lycia's invocation of pain attunement", - "lycia's invocation of mind over matter", - "lycia's invocation of wind dancer", - "lycia's invocation of supreme ego", - "lycia's invocation of resolute technique", - "lycia's invocation of conduit", - "lycia's invocation of ghost dance", - "lycia's invocation of call to arms", - "lycia's invocation of eldritch battery", - "lycia's invocation of elemental equilibrium", - "lycia's invocation of elemental overload", - "lycia's invocation of glancing blows", - "lycia's invocation of iron reflexes", - "lycia's invocation of lethe shade", - "lycia's invocation of magebane", - "lycia's invocation of unwavering stance" - ]) - }, - catalyst: { - items: new Set([ - 'intrinsic catalyst', - 'noxious catalyst', - 'fertile catalyst', - 'turbulent catalyst', - 'imbued catalyst', - 'abrasive catalyst', - 'tempering catalyst', - 'prismatic catalyst', - 'accelerating catalyst', - 'unstable catalyst' - ]) - }, - essence: { - items: new Set([ - 'essence of horror', - 'essence of delirium', - 'essence of insanity', - 'essence of hysteria', - 'deafening essence of loathing', - 'deafening essence of contempt', - 'deafening essence of envy', - 'deafening essence of sorrow', - 'deafening essence of zeal', - 'deafening essence of woe', - 'deafening essence of greed', - 'deafening essence of hatred', - 'deafening essence of rage', - 'deafening essence of scorn', - 'deafening essence of spite', - 'shrieking essence of dread', - 'shrieking essence of loathing', - 'deafening essence of anger', - 'deafening essence of dread', - 'deafening essence of fear', - 'deafening essence of misery', - 'deafening essence of torment', - 'deafening essence of wrath', - 'shrieking essence of greed', - 'deafening essence of doubt', - 'deafening essence of suffering', - 'shrieking essence of contempt', - 'shrieking essence of envy', - 'shrieking essence of rage', - 'shrieking essence of scorn', - 'shrieking essence of sorrow', - 'shrieking essence of spite', - 'shrieking essence of woe', - 'shrieking essence of zeal', - 'shrieking essence of wrath', - 'shrieking essence of anger', - 'shrieking essence of fear', - 'remnant of corruption', - 'shrieking essence of hatred', - 'deafening essence of anguish', - 'muttering essence of anger', - 'muttering essence of contempt', - 'muttering essence of fear', - 'muttering essence of greed', - 'muttering essence of hatred', - 'muttering essence of sorrow', - 'muttering essence of torment', - 'muttering essence of woe', - 'screaming essence of dread', - 'screaming essence of envy', - 'screaming essence of greed', - 'screaming essence of loathing', - 'shrieking essence of misery', - 'shrieking essence of torment', - 'wailing essence of anguish', - 'wailing essence of contempt', - 'wailing essence of doubt', - 'wailing essence of fear', - 'wailing essence of greed', - 'wailing essence of hatred', - 'wailing essence of loathing', - 'wailing essence of rage', - 'wailing essence of sorrow', - 'wailing essence of spite', - 'wailing essence of suffering', - 'wailing essence of torment', - 'wailing essence of woe', - 'wailing essence of wrath', - 'wailing essence of zeal', - 'weeping essence of anger', - 'weeping essence of contempt', - 'weeping essence of doubt', - 'weeping essence of fear', - 'weeping essence of greed', - 'weeping essence of hatred', - 'weeping essence of rage', - 'weeping essence of sorrow', - 'weeping essence of suffering', - 'weeping essence of torment', - 'weeping essence of woe', - 'weeping essence of wrath', - 'whispering essence of contempt', - 'whispering essence of greed', - 'whispering essence of hatred', - 'whispering essence of woe', - 'wailing essence of anger', - 'screaming essence of anger', - 'shrieking essence of anguish', - 'screaming essence of zeal', - 'screaming essence of sorrow', - 'screaming essence of fear', - 'screaming essence of rage', - 'screaming essence of woe', - 'screaming essence of wrath', - 'shrieking essence of doubt', - 'screaming essence of contempt', - 'screaming essence of hatred', - 'screaming essence of scorn', - 'screaming essence of spite', - 'shrieking essence of suffering', - 'screaming essence of misery', - 'screaming essence of doubt', - 'screaming essence of torment', - 'screaming essence of anguish', - 'screaming essence of suffering' - ]) - } - } - - group(item: PoeItem): InternalGroup | null { - const typeLine = item.typeLine?.toLowerCase() ?? '' - - for (const [key, value] of Object.entries(this.currencyGroupIds)) { - if (value.items.has(typeLine)) { - return { - key: typeLine, - tag: key, - hashProperties: {} - } - } - } - - return null - } -} diff --git a/src/green-app/src/lib/item-util.ts b/src/green-app/src/lib/item-util.ts index c2fe14f3..2f289c58 100644 --- a/src/green-app/src/lib/item-util.ts +++ b/src/green-app/src/lib/item-util.ts @@ -1,8 +1,8 @@ -import { v4 as uuidv4 } from 'uuid' import { IDisplayedItem, IPricedItem } from '@/types/echo-api/priced-item' import { IChildStashTab, ICompactTab, IStashTab } from '@/types/echo-api/stash' -import { PoeItem, PoeItemProperty, PoeItemSocket } from '@/types/poe-api-models' import { ValuatedItem } from '@/types/item' +import { PoeItem, PoeItemProperty, PoeItemSocket } from '@/types/poe-api-models' +import { v4 as uuidv4 } from 'uuid' import { round } from './currency' import { LISTING_CATEGORIES } from './listing-categories' @@ -50,6 +50,13 @@ export const parseTabNames = (tabs: ICompactTab[]) => { return tabs.map((t) => t.name).join(', ') } +export const parseUnsafeHashPropsToStr = (unsafeHashProperties: any) => { + // Filterfn does not work, when an object is returned + return parseUnsafeHashProps(unsafeHashProperties) + .map((p) => `${p.name};;${p.value}`) + .join(';;;') +} + export const parseUnsafeHashProps = (unsafeHashProperties: any) => { const hashProps = Object.entries(unsafeHashProperties || {}) .filter(([name, value]) => { @@ -72,8 +79,8 @@ export const parseUnsafeHashProps = (unsafeHashProperties: any) => { }) hashProps.sort((a, b) => a.displayValue.length - b.displayValue.length) - // Filterfn does not work, when an object is returned - return hashProps.map((p) => `${p.name};;${p.displayValue}`).join(';;;') + + return hashProps.map((p) => ({ name: p.name, value: p.displayValue })) } export const getItemName = (name?: string, typeline?: string) => { @@ -303,20 +310,27 @@ export const createCompactTab = (stashTab: IStashTab | string): ICompactTab => { export const calculateItemPrices = (item: IDisplayedItem, multiplier: number) => { item.calculatedPrice = undefined - item.calculatedTotal = 0 + item.calculatedTotalPrice = 0 if (item.selectedPrice !== undefined || item.originalPrice !== undefined) { item.calculatedPrice = round((item.selectedPrice! ?? item.originalPrice!) * multiplier, 4) - item.calculatedTotal = round( + item.calculatedTotalPrice = round( (item.selectedPrice! ?? item.originalPrice!) * item.stackSize * multiplier, 4 ) } } -export const filterPricedItems = (pricedItems: IPricedItem[], category: string | null) => { +export const filterPricedItems = ( + pricedItems: IPricedItem[], + category: string | null, + subCategory: string | null +) => { return pricedItems.filter((pItem) => { if (!pItem.group || !category) return true - const result = LISTING_CATEGORIES.find((x) => x.name === category)?.filter?.(pItem) + const categoryItem = LISTING_CATEGORIES.find((x) => x.name === category) + const result = subCategory + ? categoryItem?.subCategories.find((c) => c.name === subCategory)?.filter?.(pItem) + : categoryItem?.filter?.(pItem) if (result === undefined) return true return result }) @@ -357,7 +371,7 @@ export const mapItemsToDisplayedItems = ( calculatedPrice: valuation?.pValues ? valuation.pValues[pItem.percentile] * (multiplier ?? 1) : undefined, - calculatedTotal: valuation?.pValues + calculatedTotalPrice: valuation?.pValues ? valuation.pValues[pItem.percentile] * stackSize * (multiplier ?? 1) : 0, selectedPrice: undefined, // We use the price as placeholder @@ -371,7 +385,7 @@ export const mapItemsToDisplayedItems = ( if (mappedItem.group && mappedItem.displayName === 'Chaos Orb') { mappedItem.originalPrice = 1 mappedItem.calculatedPrice = 1 * (multiplier ?? 1) - mappedItem.calculatedTotal = 1 * stackSize * (multiplier ?? 1) + mappedItem.calculatedTotalPrice = 1 * stackSize * (multiplier ?? 1) } if (mappedItem.group && overprices && overprices[mappedItem.group.hash] !== undefined) { diff --git a/src/green-app/src/lib/listing-categories.ts b/src/green-app/src/lib/listing-categories.ts index 9e015c88..317119ef 100644 --- a/src/green-app/src/lib/listing-categories.ts +++ b/src/green-app/src/lib/listing-categories.ts @@ -1,19 +1,24 @@ +import { SageItemGroup } from 'sage-common' import { lowerCaseCompasses } from './compasses' -import { SageItemGroup } from './item-grouping-service' -type ItemWithGroup = { group?: Pick } +type ItemWithGroup = { group?: Pick } -export interface ListingCategory { +export type ListingSubCategory = { name: string tags: string[] icon: string + restItems?: boolean filter?: (item: T) => boolean parseName?: (item: T) => string | undefined } +export type ListingCategory = Omit & { + subCategories: ListingSubCategory[] +} + export const LISTING_CATEGORIES: ListingCategory[] = [ { - name: 'compasses', + name: 'compass', tags: ['compass'], icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ2hhcmdlZENvbXBhc3MiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/ea8fcc3e35/ChargedCompass.png', filter: (item) => { @@ -23,104 +28,422 @@ export const LISTING_CATEGORIES: ListingCategory[] = [ if (item.group) { return lowerCaseCompasses[item.group?.key] } - } + }, + subCategories: [] }, { name: 'essence', tags: ['essence'], icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRXNzZW5jZS9Db250ZW1wdDYiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/332e9b32e9/Contempt6.png', - filter: (item) => { - return ![ - 'shrieking', - 'deafening', - 'essence of horror', - 'essence of delirium', - 'essence of hysteria' - ].some((essence) => item.group?.key.startsWith(essence)) - } - }, - { - name: 'essence high', - tags: ['essence'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRXNzZW5jZS9Ib3Jyb3IxIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/748d594bde/Horror1.png', - filter: (item) => { - return [ - 'shrieking', - 'deafening', - 'essence of horror', - 'essence of delirium', - 'essence of hysteria' - ].some((essence) => item.group?.key.startsWith(essence)) - } - }, - { - name: 'scarabs', - tags: ['scarab'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvU2NhcmFicy9UaWVyNFNjYXJhYkhhcmJpbmdlcnMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/81caefbf3f/Tier4ScarabHarbingers.png' + subCategories: [ + { + name: 'essence low', + tags: ['essence'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRXNzZW5jZS9Db250ZW1wdDMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/7347bf5d1c/Contempt3.png', + filter: (item) => { + return ![ + 'shrieking', + 'deafening', + 'essence of horror', + 'essence of delirium', + 'essence of hysteria' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'essence high', + tags: ['essence'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRXNzZW5jZS9Ib3Jyb3IxIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/748d594bde/Horror1.png', + filter: (item) => { + return [ + 'shrieking', + 'deafening', + 'essence of horror', + 'essence of delirium', + 'essence of hysteria' + ].some((essence) => item.group?.key.startsWith(essence)) + } + } + ] }, { name: 'heist', tags: ['contract'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSGVpc3QvQ29udHJhY3RJdGVtIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/8262f2ca0e/ContractItem.png' + // TODO: Readd blueprints once the backend is fixed + // tags: ['contract', 'blueprint'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSGVpc3QvQ29udHJhY3RJdGVtIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/8262f2ca0e/ContractItem.png', + filter: (item) => { + if (!item.group) return true + if (item.group.tag === 'blueprint') { + const ilvl = item?.group?.unsafeHashProperties?.['ilvl'] + return ilvl === '81+' + } else if (item.group.tag === 'contract') { + const ilvl = item?.group?.unsafeHashProperties?.['ilvl'] + return ilvl === '83+' + } + return true + }, + subCategories: [ + { + name: 'contract', + tags: ['contract'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSGVpc3QvQ29udHJhY3RJdGVtIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/8262f2ca0e/ContractItem.png', + filter: (item) => { + const ilvl = item?.group?.unsafeHashProperties?.['ilvl'] + return ilvl === '83+' + } + } + // { + // name: 'blueprint', + // tags: ['blueprint'], + // icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSGVpc3QvQmx1ZXByaW50Tm90QXBwcm92ZWQ4IiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/cc90ce9113/BlueprintNotApproved8.png', + // filter: (item) => { + // const ilvl = item?.group?.unsafeHashProperties?.['ilvl'] + // return ilvl === '81+' + // } + // } + ] }, { name: 'currency', tags: ['currency'], - icon: 'https://web.poecdn.com/image/Art/2DItems/Currency/CurrencyRerollRare.png' + icon: 'https://web.poecdn.com/image/Art/2DItems/Currency/CurrencyRerollRare.png', + subCategories: [ + { + name: 'stackedDeck', + tags: ['currency'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvRGl2aW5hdGlvbi9EZWNrIiwic2NhbGUiOjF9XQ/8e83aea79a/Deck.png', + filter: (item) => { + return ['stacked deck'].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'lifeforce', + tags: ['currency'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSGFydmVzdC9XaWxkTGlmZWZvcmNlIiwic2NhbGUiOjF9XQ/e3d0b372b0/WildLifeforce.png', + filter: (item) => { + return [ + 'vivid crystallised lifeforce', + 'wild crystallised lifeforce', + 'primal crystallised lifeforce' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'otherCurrency', + tags: ['currency'], + restItems: true, + icon: 'https://web.poecdn.com/image/Art/2DItems/Currency/CurrencyRerollRare.png' + } + ] }, { name: 'beast', tags: ['beast'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQmVzdGlhcnlPcmJGdWxsIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/3214b44360/BestiaryOrbFull.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQmVzdGlhcnlPcmJGdWxsIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/3214b44360/BestiaryOrbFull.png', + subCategories: [] }, { - name: 'fossil', - tags: ['fossil'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsdmUvR2x5cGhpY0Zvc3NpbCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/f5b3c6edf7/GlyphicFossil.png' - }, - { - name: 'resonator', - tags: ['resonator'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsdmUvUmVyb2xsMXgxQSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/eea57ec0df/Reroll1x1A.png' + name: 'delve', + tags: ['fossil', 'resonator'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsdmUvRmFjZXRlZEZvc3NpbCIsInNjYWxlIjoxfV0/473889cafb/FacetedFossil.png', + subCategories: [ + { + name: 'fossil', + tags: ['fossil'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsdmUvR2x5cGhpY0Zvc3NpbCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/f5b3c6edf7/GlyphicFossil.png' + }, + { + name: 'resonator', + tags: ['resonator'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsdmUvUmVyb2xsMXgxQSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/eea57ec0df/Reroll1x1A.png' + } + ] }, { - name: 'catalysts', + name: 'catalyst', tags: ['catalyst'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ2F0YWx5c3RzL0NoYW9zUGh5c2ljYWxDYXRhbHlzdCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/bbdf8917e4/ChaosPhysicalCatalyst.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQ2F0YWx5c3RzL0NoYW9zUGh5c2ljYWxDYXRhbHlzdCIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/bbdf8917e4/ChaosPhysicalCatalyst.png', + subCategories: [] }, { - name: 'fragments', - tags: ['fragment'], - icon: 'https://web.poecdn.com/image/Art/2DItems/Maps/AtlasMaps/FragmentPhoenix.png' + name: 'sanctum', + tags: ['relic', 'forbidden tome'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvUmVsaWNzL1JlbGljVW5pcXVlMngyIiwidyI6MiwiaCI6Miwic2NhbGUiOjF9XQ/15bd9eec94/RelicUnique2x2.png', + filter: (item) => { + if (!item.group || item.group.tag !== 'forbidden tome') return true + const ilvl = parseInt(item?.group?.unsafeHashProperties?.['ilvl']) + return !isNaN(ilvl) && ilvl >= 83 + }, + subCategories: [ + { + name: 'uniqueRelic', + tags: ['relic'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvUmVsaWNzL1JlbGljVW5pcXVlMngyIiwidyI6MiwiaCI6Miwic2NhbGUiOjF9XQ/15bd9eec94/RelicUnique2x2.png' + }, + { + name: 'forbiddenTome', + tags: ['forbidden tome'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvU2FuY3R1bS9TYW5jdHVtS2V5IiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/d0326cac9a/SanctumKey.png', + filter: (item) => { + const ilvl = parseInt(item?.group?.unsafeHashProperties?.['ilvl']) + return !isNaN(ilvl) && ilvl >= 83 + } + } + ] }, { - name: 'breach', - tags: ['breach'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQnJlYWNoL0JyZWFjaEZyYWdtZW50c0NoYW9zIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/04b5c032f4/BreachFragmentsChaos.png' + name: 'fragment', + tags: ['fragment', 'scarab', 'breach', 'invitation'], + icon: 'https://web.poecdn.com/image/Art/2DItems/Maps/AtlasMaps/FragmentPhoenix.png', + subCategories: [ + { + name: 'scarab', + tags: ['scarab'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvU2NhcmFicy9UaWVyNFNjYXJhYkhhcmJpbmdlcnMiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/81caefbf3f/Tier4ScarabHarbingers.png' + }, + { + name: 'breach', + tags: ['breach'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvQnJlYWNoL0JyZWFjaEZyYWdtZW50c0NoYW9zIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/04b5c032f4/BreachFragmentsChaos.png' + }, + { + name: 'legion', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9NYXJha2V0aEZyYWdtZW50IiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/7c42d58a5e/MarakethFragment.png', + filter: (item) => { + return [ + 'timeless maraketh emblem', + 'timeless templar emblem', + 'timeless vaal emblem', + 'timeless karui emblem', + 'timeless eternal emblem', + 'unrelenting timeless maraketh emblem', + 'unrelenting timeless templar emblem', + 'unrelenting timeless karui emblem', + 'unrelenting timeless vaal emblem', + 'unrelenting timeless eternal emblem', + 'timeless maraketh splinter', + 'timeless templar splinter', + 'timeless karui splinter', + 'timeless vaal splinter', + 'timeless eternal empire splinter' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'invitation', + tags: ['invitation'], + icon: 'https://web.poecdn.com/image/Art/2DItems/Currency/Atlas/NullVoid5.png?w=1&h=1&scale=1' + }, + { + name: 'mortalSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9VYmVyVmFhbENvbXBsZXRlIiwic2NhbGUiOjF9XQ/994d9e2821/UberVaalComplete.png', + filter: (item) => { + return ['mortal ignorance', 'mortal rage', 'mortal hope', 'mortal grief'].some( + (essence) => item.group?.key.startsWith(essence) + ) + } + }, + { + name: 'sacrificeSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9WYWFsQ29tcGxldGUiLCJzY2FsZSI6MX1d/63035d86d7/VaalComplete.png', + filter: (item) => { + return [ + 'sacrifice at midnight', + 'sacrifice at noon', + 'sacrifice at dawn', + 'sacrifice at dusk' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'shaperSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9TaGFwZXJDb21wbGV0ZSIsInNjYWxlIjoxfV0/ace686004d/ShaperComplete.png', + filter: (item) => { + return [ + 'fragment of the phoenix', + 'fragment of the hydra', + 'fragment of the minotaur', + 'fragment of the chimera' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'elderSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9FbGRlckNvbXBsZXRlIiwic2NhbGUiOjF9XQ/6db44597fe/ElderComplete.png', + filter: (item) => { + return [ + 'fragment of eradication', + 'fragment of enslavement', + 'fragment of constriction', + 'fragment of purification' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'conquererSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9TaXJ1c0ZyYWdtZW50Q29tcGxldGUiLCJzY2FsZSI6MX1d/c585a0ae79/SirusFragmentComplete.png', + filter: (item) => { + return ["al-hezmin's crest", "baran's crest", "drox's crest", "veritania's crest"].some( + (essence) => item.group?.key.startsWith(essence) + ) + } + }, + { + name: 'uberElderSet', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9VYmVyRWxkZXJDb21wbGV0ZSIsInNjYWxlIjoxfV0/715e041869/UberElderComplete.png', + filter: (item) => { + return [ + 'fragment of terror', + 'fragment of emptiness', + 'fragment of shape', + 'fragment of knowledge' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'simulacrum', + tags: ['fragment'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9EZWxpcml1bUZyYWdtZW50Iiwic2NhbGUiOjF9XQ/7f29157183/DeliriumFragment.png', + filter: (item) => { + return ['simulacrum', 'simulacrum splinter'].some((essence) => + item.group?.key.startsWith(essence) + ) + } + }, + { + name: 'otherFragments', + tags: ['fragment', 'scarab', 'breach', 'invitation'], + restItems: true, + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9Db3NtaWNDb3JlU3VwcG9ydGVyVmF1bHRLZXkiLCJzY2FsZSI6MX1d/1f2d34a36d/CosmicCoreSupporterVaultKey.png' + } + ] }, { - name: 'cards', + name: 'card', tags: ['card'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvRGl2aW5hdGlvbi9JbnZlbnRvcnlJY29uIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/f34bf8cbb5/InventoryIcon.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvRGl2aW5hdGlvbi9JbnZlbnRvcnlJY29uIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/f34bf8cbb5/InventoryIcon.png', + subCategories: [] }, { - name: 'delirium orbs', + name: 'delirium orb', tags: ['delirium orb'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsaXJpdW0vRGVsaXJpdW1PcmJTY2FyYWJzIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/fa4c5160ca/DeliriumOrbScarabs.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRGVsaXJpdW0vRGVsaXJpdW1PcmJTY2FyYWJzIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/fa4c5160ca/DeliriumOrbScarabs.png', + subCategories: [] }, { - name: 'logbooks', - tags: ['logbook'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9FeHBlZGl0aW9uQ2hyb25pY2xlMyIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/2802fe605e/ExpeditionChronicle3.png' + name: 'expedition', + tags: ['logbook', 'artifacts'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9FeHBlZGl0aW9uQ2hyb25pY2xlMyIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/2802fe605e/ExpeditionChronicle3.png', + filter: (item) => { + if (!item.group || item.group.tag !== 'logbook') return true + const ilvl = parseInt(item?.group?.unsafeHashProperties?.['ilvl']) + return !isNaN(ilvl) && ilvl >= 83 + }, + subCategories: [ + { + name: 'logbook', + tags: ['logbook'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9FeHBlZGl0aW9uQ2hyb25pY2xlMyIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/2802fe605e/ExpeditionChronicle3.png', + filter: (item) => { + const ilvl = parseInt(item?.group?.unsafeHashProperties?.['ilvl']) + return !isNaN(ilvl) && ilvl >= 83 + } + }, + { + name: 'artifact', + tags: ['artifacts'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvRXhwZWRpdGlvbi9CYXJ0ZXJSZWZyZXNoQ3VycmVuY3kiLCJzY2FsZSI6MX1d/0542d74d3c/BarterRefreshCurrency.png' + } + ] }, { - name: 'oils', + name: 'oil', tags: ['oil'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvT2lscy9Hb2xkZW5PaWwiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/69094a06e9/GoldenOil.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvT2lscy9Hb2xkZW5PaWwiLCJ3IjoxLCJoIjoxLCJzY2FsZSI6MX1d/69094a06e9/GoldenOil.png', + subCategories: [] }, { - name: 'incubators', + name: 'memory', + tags: ['atlas memory'], + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvTWVtb3J5TGluZS9OaWtvTWVtb3J5SXRlbSIsInciOjEsImgiOjEsInNjYWxlIjoxfV0/5c560ea8fd/NikoMemoryItem.png', + subCategories: [] + }, + { + name: 'incubator', tags: ['incubator'], - icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSW5jdWJhdGlvbi9JbmN1YmF0aW9uQXJtb3VyIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/637c41a730/IncubationArmour.png' + icon: 'https://web.poecdn.com/gen/image/WzI1LDE0LHsiZiI6IjJESXRlbXMvQ3VycmVuY3kvSW5jdWJhdGlvbi9JbmN1YmF0aW9uQXJtb3VyIiwidyI6MSwiaCI6MSwic2NhbGUiOjF9XQ/637c41a730/IncubationArmour.png', + subCategories: [], + filter: (item) => { + const ilvl = parseInt(item?.group?.unsafeHashProperties?.['ilvl']) + return !isNaN(ilvl) && ilvl >= 83 + } + }, + { + name: 'map', + tags: ['map'], + icon: 'https://web.poecdn.com/image/Art/2DItems/Maps/AtlasMaps/Gorge3.png?scale=1&w=1&h=1', + filter: (item) => { + return [ + 'lair of the hydra map', + 'maze of the minotaur map', + 'forge of the phoenix map', + 'pit of the chimera map', + "purifier's map", + "constrictor's map", + "enslaver's map", + "eradicator's map", + "al-hezmin's map", + "veritania's map", + "baran's map", + "drox's map" + ].some((essence) => item.group?.key.startsWith(essence)) + }, + subCategories: [ + { + name: 'shaperMap', + tags: ['map'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9BdGxhczJNYXBzL05ldy9QaG9lbml4IiwidyI6MSwiaCI6MSwic2NhbGUiOjEsIm1uIjoxOSwibXQiOjAsIm1pIjoxfV0/3a961684d2/Phoenix.png', + filter: (item) => { + return [ + 'lair of the hydra map', + 'maze of the minotaur map', + 'forge of the phoenix map', + 'pit of the chimera map' + ].some((essence) => item.group?.key.startsWith(essence)) + } + }, + { + name: 'elderMap', + tags: ['map'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9BdGxhczJNYXBzL05ldy9JbmZlc3RhdGlvbiIsInciOjEsImgiOjEsInNjYWxlIjoxLCJtbiI6MTksIm10IjoxNiwibWciOjEsIm1pIjoyfV0/6d86f8c63f/Infestation.png', + filter: (item) => { + return ["purifier's map", "constrictor's map", "enslaver's map", "eradicator's map"].some( + (essence) => item.group?.key.startsWith(essence) + ) + } + }, + { + name: 'conquerorMap', + tags: ['map'], + icon: 'https://web.poecdn.com/gen/image/WzI4LDE0LHsiZiI6IjJESXRlbXMvTWFwcy9BdGxhczJNYXBzL05ldy9CYXRocyIsInciOjEsImgiOjEsInNjYWxlIjoxLCJtbiI6MTksIm10IjoxNiwibWMiOjJ9XQ/6c27bce263/Baths.png', + filter: (item) => { + return ["al-hezmin's map", "veritania's map", "baran's map", "drox's map"].some( + (essence) => item.group?.key.startsWith(essence) + ) + } + } + // TODO: Normal maps not(corrupted, mirrored, split) + // TODO: Unique maps + // TODO: Blighted maps + // TODO: Blighted revanged maps + ] } ] diff --git a/src/green-app/src/lib/listing-filter-util.ts b/src/green-app/src/lib/listing-filter-util.ts index 37052f04..13937f5a 100644 --- a/src/green-app/src/lib/listing-filter-util.ts +++ b/src/green-app/src/lib/listing-filter-util.ts @@ -1,13 +1,27 @@ import { ListingFilter, ListingFilterGroup } from '@/components/trade-filter-card' -import { SageListingItemType, SageListingType } from '@/types/sage-listing-type' +import { SageListingItemType } from '@/types/sage-listing-type' + +type ResultCountItem = { + item: SageListingItemType + filtered: boolean + minQuantity?: number + index: number +} + +type ResultCountGroup = { + minQuantity: number + filters: Record + items: ResultCountItem[] +} export class ListingFilterUtil { static combineFilter = (filterGroups: ListingFilterGroup[]) => { filterGroups = filterGroups ?? [] const andFilters: Record = {} - filterGroups - .filter((group) => group.selected && group.mode === 'AND') + const andGroups = filterGroups.filter((group) => group.selected && group.mode === 'AND') + const hasAndGroups = andGroups.length > 0 + andGroups .flatMap((group) => group.filters) .forEach((filter) => { if ( @@ -20,19 +34,26 @@ export class ListingFilterUtil { } }) - const countFilters: Record = {} + const countGroups: { minQuantity: number; filters: Record }[] = [] filterGroups .filter((group) => group.selected && group.mode === 'COUNT') - .flatMap((group) => group.filters) - .forEach((filter) => { - if ( - !!filter.option?.hash && - filter.selected && - (!countFilters[filter.option.hash] || - (filter.minimumQuantity || 0) > (countFilters[filter.option.hash].minimumQuantity || 0)) - ) { - countFilters[filter.option.hash] = filter - } + .forEach((group) => { + const countFilters: Record = {} + group.filters.map((filter) => { + if ( + !!filter.option?.hash && + filter.selected && + (!countFilters[filter.option.hash] || + (filter.minimumQuantity || 0) > + (countFilters[filter.option.hash].minimumQuantity || 0)) + ) { + countFilters[filter.option.hash] = filter + } + }) + countGroups.push({ + minQuantity: group.minimumQuantity || 0, + filters: countFilters + }) }) const notFilters: Record = {} @@ -50,35 +71,61 @@ export class ListingFilterUtil { } }) - return { andFilters, countFilters, notFilters } + return { hasAndGroups, andFilters, countGroups, notFilters } } static filterItems(items: SageListingItemType[], filterGroups: ListingFilterGroup[]) { - // TODO: Add calculation for COUNT - const { andFilters, countFilters, notFilters } = this.combineFilter(filterGroups) + const { hasAndGroups, andFilters, countGroups, notFilters } = this.combineFilter(filterGroups) let notFilterValid = true - let andItems: { - item: SageListingItemType - filtered: boolean - minQuantity?: number - index: number - }[] = [] + const andItems: ResultCountItem[] = [] + + const countGroupResult: ResultCountGroup[] = countGroups.map((countGroup) => { + return { + minQuantity: countGroup.minQuantity, + filters: countGroup.filters, + items: [] + } + }) items.forEach((item, index) => { let result = false let filtered = false - const filtersLength = Object.keys(andFilters).length + const andFiltersLength = Object.keys(andFilters).length if (andFilters[item.hash]) { filtered = true if (item.quantity >= (andFilters[item.hash].minimumQuantity || 0)) { result = true } - } else if (filtersLength === 0) { + } else if (hasAndGroups && andFiltersLength === 0) { result = true } + countGroupResult.forEach((countGroup) => { + let result = false + const countFilters = countGroup.filters + const countFiltersLength = Object.keys(countFilters).length + + if (countFilters[item.hash]) { + filtered = true + if (item.quantity >= (countFilters[item.hash].minimumQuantity || 0)) { + result = true + } + } else if (countFiltersLength === 0) { + result = true + } + + if (result) { + countGroup.items.push({ + item, + minQuantity: countFilters[item.hash]?.minimumQuantity, + filtered, + index + }) + } + }) + if ( notFilters[item.hash] && (notFilters[item.hash].minimumQuantity === undefined || @@ -97,13 +144,47 @@ export class ListingFilterUtil { } }) + const resultItems: Record = {} + const allAndFiltersValid = Object.entries(andFilters).every(([hash]) => andItems.some((out) => out.item.hash === hash) ) - if (!allAndFiltersValid) { - andItems = [] + + if (allAndFiltersValid) { + andItems.forEach((out) => { + if ( + !resultItems[out.index] || + (out.minQuantity || 0) > (resultItems[out.index].minQuantity || 0) + ) { + resultItems[out.index] = out + } + }) } - return { valid: allAndFiltersValid && notFilterValid, items: andItems } + const countFiltersValid = countGroupResult.every((countGroup) => { + const validFilterCount = Object.entries(countGroup.filters).filter(([hash]) => + countGroup.items.some((out) => out.item.hash === hash) + ).length + if (validFilterCount >= countGroup.minQuantity) { + countGroup.items.forEach((out) => { + if ( + !resultItems[out.index] || + (out.minQuantity || 0) > (resultItems[out.index].minQuantity || 0) + ) { + resultItems[out.index] = out + } + }) + + return true + } + return false + }) + + const valid = allAndFiltersValid && notFilterValid && countFiltersValid + + return { + valid, + items: valid ? Object.values(resultItems) : [] + } } } diff --git a/src/green-app/src/lib/listing-util.ts b/src/green-app/src/lib/listing-util.ts new file mode 100644 index 00000000..4abac447 --- /dev/null +++ b/src/green-app/src/lib/listing-util.ts @@ -0,0 +1,69 @@ +import { SageItemGroupSummaryShard } from '@/types/echo-api/item-group' +import { SageValuationShard } from '@/types/echo-api/valuation' +import { + SageDatabaseOfferingType, + SageListingItemType, + SageListingType, + SageSelectedDatabaseOfferingItemType +} from '@/types/sage-listing-type' +import { DEFAULT_VALUATION_INDEX } from './constants' +import { LISTING_CATEGORIES } from './listing-categories' + +type SageSpecialOfferingType = Omit & { + items: SageSelectedDatabaseOfferingItemType[] +} + +export const calculateListingFromOfferingListing = ( + offering: SageSpecialOfferingType, + summaries: SageItemGroupSummaryShard['summaries'], + valuations: SageValuationShard['valuations'] +): SageListingType => { + const items = offering.items.map((e): SageListingItemType => { + const valuation = valuations[e.hash] + + const item: SageListingItemType = { + hash: e.hash, + price: e.price, + quantity: e.quantity, + displayName: summaries[e.hash]?.displayName, + calculatedTotalPrice: (e.selectedQuantity ?? e.quantity) * e.price, + primaryValuation: valuation?.pValues?.[DEFAULT_VALUATION_INDEX] ?? 0, + valuation: valuation, + summary: summaries[e.hash], + icon: summaries[e.hash]?.icon, + selectedQuantity: e.selectedQuantity ?? e.quantity + } + + if (item.primaryValuation <= 0) { + console.log('NOT FOUND', e) + } + return item + }) + + const calculatedTotalPrice = items.reduce((a, b) => a + b.calculatedTotalPrice, 0) + const calculatedTotalValuation = items.reduce((a, b) => a + b.primaryValuation * b.quantity, 0) + let multiplier = 100 + if (calculatedTotalPrice && calculatedTotalValuation) { + multiplier = (calculatedTotalPrice / calculatedTotalValuation) * 100 + } + + const categoryItem = LISTING_CATEGORIES.find((ca) => ca.name === offering.meta.category) + const selectedCategory = offering.meta.subCategory + ? categoryItem?.subCategories.find((c) => c.name === offering.meta.subCategory) + : categoryItem + + return { + userId: offering.userId, + uuid: offering.uuid, + deleted: offering.deleted, + meta: { + ...offering.meta, + multiplier, + calculatedTotalPrice, + calculatedTotalValuation, + icon: selectedCategory?.icon || '', + altIcon: '' + }, + items + } +} diff --git a/src/green-app/src/lib/whsiper-util.ts b/src/green-app/src/lib/whsiper-util.ts index 5234bdca..deca4354 100644 --- a/src/green-app/src/lib/whsiper-util.ts +++ b/src/green-app/src/lib/whsiper-util.ts @@ -1,22 +1,30 @@ import { SageListingItemType, SageListingType } from '@/types/sage-listing-type' import { round } from './currency' +import { TFunction } from 'i18next' export const createWishperAndCopyToClipboard = ( divinePrice: number, selectedListing: SageListingType, - selectedItems: SageListingItemType[] + selectedItems: SageListingItemType[], + t: TFunction<'common', undefined> ) => { + // TODO: Add locale to listing and translate it to the target language + const translateCategory = (categoryName: string) => { + // t(`categories.${categoryName}` as any) + categoryName + } + const getTabWhisper = () => { const divines = Math.trunc(selectedListing.meta.calculatedTotalPrice / divinePrice) const chaos = selectedListing.meta.calculatedTotalPrice % divinePrice const singleItemTradeMode = selectedListing.meta.listingMode === 'single' ? 'ALL' : '' if (chaos === 0) { - return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${selectedListing.meta.category} listing for ${divines}d` + return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${translateCategory(selectedListing.meta.subCategory || selectedListing.meta.category)} listing for ${divines}d` } else { if (divines === 0) { - return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${selectedListing.meta.category} listing for ${round(chaos)}c` + return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${translateCategory(selectedListing.meta.subCategory || selectedListing.meta.category)} listing for ${round(chaos)}c` } else { - return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${selectedListing.meta.category} listing for ${divines}d ${round(chaos)}c` + return `@${selectedListing.meta.ign} WTB ${singleItemTradeMode} ${translateCategory(selectedListing.meta.subCategory || selectedListing.meta.category)} listing for ${divines}d ${round(chaos)}c` } } } diff --git a/src/green-app/src/locales/de/common.json b/src/green-app/src/locales/de/common.json new file mode 100644 index 00000000..c5f22e2c --- /dev/null +++ b/src/green-app/src/locales/de/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "Speichern", + "cancel": "Abbrechen", + "close": "Schließen", + "discard": "Verwerfen", + "addGroup": "Gruppe hinzufügen", + "view": "Anzeigen", + "hardReset": "Daten zurücksetzen", + "softReset": "Daten zurücksetzen", + "logout": "Abmelden", + "login": "PoE-Konto verbinden (SSO)", + "linkDiscord": "Discord verknüpfen", + "poeTradeDiscord": "PoE Trading Discord", + "postOffering": "Angebot veröffentlichen", + "loadTabs": "{{selected}} von {{total}} Tabs laden", + "refreshStashTabs": "Stashtabs aktualisieren", + "unselectStashTabs": "Stashtabs abwählen", + "bulkOfferingsPage": "Massenangebote", + "bulkListingsPage": "Massenlisten", + "showUnreadNotificationsOnly": "Nur ungelesene anzeigen", + "clearAllNotifications": "Alle löschen", + "markAllNotifications": "Alle als gelesen markieren", + "copyWhisper": "Flüstern kopieren", + "copyWhisperAgain": "Erneut kopieren?", + "whisperCopied": "Flüstern kopiert!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "Sind Sie ganz sicher?", + "myOfferings": "Meine Angebote", + "bulkListing": "Massenliste", + "actions": "Aktionen", + "notifications": "Benachrichtigungen", + "notificationSettings": "Benachrichtigungseinstellungen", + "itemFilter": "Gegenstandsfilter" + }, + "label": { + "searchPh": "Suche ...", + "selectCategoryPh": "Kategorie auswählen", + "selectSubCategoryPh": "Unterkategorie auswählen", + "selectLeaguePh": "Liga auswählen", + "selectPh": "Auswählen ...", + "noResults": "Keine Ergebnisse.", + "noItemResults": "Kein Gegenstand gefunden.", + "noOfferingResults": "Keine Angebote gefunden.", + "noNotificationResults": "Sie haben keine Benachrichtigungen 🎉", + "otherCalculations": "ANDERE BERECHNUNGEN", + "wholeListingShort": "Ganzes Angebot", + "individualListingShort": "Einzelne Gegenstände", + "wholeOfferingShort": "Ganzes Angebot", + "individualOfferingShort": "Einzelne Gegenstände", + "wholeOffering": "Ganzes Angebot verkaufen", + "individualOffering": "Einzelne Gegenstände verkaufen", + "individualOfferingTT": "Ganzes Angebot: Das gesamte Angebot auf einmal verkaufen", + "wholeOfferingTT": "Einzelne Gegenstände: Einzelne Gegenstände zu individuellen Preisen verkaufen", + "multiplier": "Multiplikator: {{multiplier}}%", + "multiplierRange": "Multiplikator: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Gesamt:", + "totalPrice": "Gesamtpreis:", + "columnSettings": "Spalteneinstellungen", + "hardReset": "Harter Reset", + "columnSettingsResetTT": "Einstellungen zurücksetzen", + "discordTT": "Discord", + "account": "Mein Konto", + "league": "Liga", + "loading": "Laden ...", + "loadingItems": "Gegenstände laden ...", + "offeringPreviewTT": "Hier werden nur die Stashtabs, Kategorie, Unterkategorie und Verkaufsmodus ausgewählt.\n\nAlle anderen Einstellungen werden aus Ihren letzten Änderungen übernommen:", + "offeringPreviewLi1TT": "Multiplikator pro (Unter-)Kategorie", + "offeringPreviewLi2TT": "Überschreibungen/Überpreisungen", + "offeringPreviewLi3TT": "Nicht ausgewählte Gegenstände", + "notLoadedTT": "Nicht geladen", + "updatedTT": "Aktualisiert", + "maxNotification": "Maximale angezeigte Benachrichtigungen:", + "tradeInNewWindow": "Benachrichtigungen zu Handelsanfragen werden angezeigt, wenn jemand auf „Flüstern kopieren“ für eines Ihrer Angebote klickt.", + "showDetailsTT": "Details anzeigen", + "rowsSelectedOf": "{{current}} von {{total}} Zeile(n) ausgewählt.", + "pageOf": "Seite {{current}} von {{total}}", + "rowsPerPage": "Zeilen pro Seite", + "firstPage": "Zur ersten Seite gehen", + "prevPage": "Zur vorherigen Seite gehen", + "nextPage": "Zur nächsten Seite gehen", + "lastPage": "Zur letzten Seite gehen", + "minPh": "MIN", + "multiplierLabel": "Multiplikator:", + "askingPrice": "Preis anfragen:", + "sellMode": "Verkaufsmodus:", + "seller": "Verkäufer:", + "category": "Kategorie:", + "language": "Sprache" + }, + "body": { + "fallbackRenderInfo": "Wenn Sie bestätigen, werden alle Daten zurückgesetzt. Um den Fehler zu beheben, wäre es hilfreich, ihn zu melden. Danke!", + "hardReset": "Wenn Sie bestätigen, werden alle Daten zurückgesetzt. Dies kann bei Fehlern helfen. Um den Fehler zu beheben, wäre es hilfreich, ihn zu melden. Danke!", + "characterInfo": "Level {{level}} {{class}}", + "wtbAll": " wtb alle {{category}}", + "wtbPartial": " wtb {{count}} {{category}}", + "softReset": "Diese Aktion kann nicht rückgängig gemacht werden.\n\nWenn Sie bestätigen, werden die folgenden Daten zurückgesetzt:", + "softResetLi1": "Multiplikator pro (Unter-)Kategorie", + "softResetLi2": "Verkaufsmodus pro (Unter-)Kategorie", + "softResetLi3": "Überschreibungen/Überpreisungen", + "softResetLi4": "Nicht ausgewählte Gegenstände", + "maxNotification": "Benachrichtigungen werden in der Warteschlange angezeigt, wenn die maximale Anzahl erreicht ist.", + "tradeInNewWindow": "Benachrichtigungen zu Handelsanfragen werden angezeigt, wenn jemand auf „Flüstern kopieren“ für eines Ihrer Angebote klickt." + }, + "option": { + "AND": "UND", + "NOT": "NICHT", + "COUNT": "ANZAHL", + "showAll": "Alle anzeigen", + "showSelected": "Ausgewählte anzeigen", + "showUnselected": "Nicht ausgewählte anzeigen", + "showAllModes": "Alle Modi anzeigen", + "showWholeListings": "Ganze Angebote anzeigen", + "showIndividualListings": "Einzelne Angebote anzeigen" + }, + "columnTitle": { + "selection": "Auswahl", + "2_day_history": "Preis der letzten 2 Tage", + "7_day_history": "Preis der letzten 7 Tage", + "multiplier": "Multiplikator", + "name": "Name", + "selectedPrice": "Überschreiben", + "props": "Eigenschaften", + "quantity": "Menge", + "selectedQuantity": "Angefragte Menge", + "tabs": "Tab", + "tag": "Tag", + "price": "Preis", + "totalPrice": "Gesamtpreis", + "commulativePrice": "Kumulativ", + "category": "Kategorie", + "listingMode": "Verkaufsmodus", + "seller": "Verkäufer", + "created": "Erstellt", + "actions": "Aktionen" + }, + "categories": { + "compass": "Kompass", + "essence": "Essenzen", + "essence low": "Niedrige Essenzen", + "essence high": "Hohe Essenzen", + "heist": "Überfall", + "contract": "Verträge", + "blueprint": "Baupläne", + "currency": "Währung", + "stackedDeck": "Gestapelte Decks", + "lifeforce": "Lebenskraft", + "otherCurrency": "Andere Währung", + "beast": "Bestien", + "delve": "Abenteuer", + "fossil": "Fossilien", + "resonator": "Resonatoren", + "catalyst": "Katalysatoren", + "sanctum": "Heiligtum", + "uniqueRelic": "Einzigartige Relikte", + "forbiddenTome": "Verbotene Schriften", + "fragment": "Fragments", + "scarab": "Skarabäen", + "breach": "Riss", + "legion": "Legion", + "invitation": "Einladung", + "mortalSet": "Sterbliches Set", + "sacrificeSet": "Opfer Set", + "shaperSet": "Formgeber Set", + "elderSet": "Ältesten Set", + "conquererSet": "Eroberer Set", + "uberElderSet": "Uber-Ältester Set", + "simulacrum": "Simulakrum", + "otherFragments": "Andere Fragmente", + "card": "Karten", + "delirium orb": "Delirium Orb", + "expedition": "Expedition", + "logbook": "Logbuch", + "artifact": "Artefakt", + "oil": "Öl", + "memory": "Erinnerung", + "incubator": "Brutkammer", + "map": "Karte", + "shaperMap": "Formgeber Karte", + "elderMap": "Ältesten Karte", + "conquerorMap": "Eroberer Karte" + }, + "relativeTime": { + "future": "in %s", + "past": "vor %s", + "s": "ein paar Sekunden", + "m": "eine Minute", + "mm": "%d Minuten", + "h": "eine Stunde", + "hh": "%d Stunden", + "d": "ein Tag", + "dd": "%d Tage", + "M": "ein Monat", + "MM": "%d Monate", + "y": "ein Jahr", + "yy": "%d Jahre" + } +} diff --git a/src/green-app/src/locales/de/notification.json b/src/green-app/src/locales/de/notification.json new file mode 100644 index 00000000..9fcd142f --- /dev/null +++ b/src/green-app/src/locales/de/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Etwas ist schiefgelaufen: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Sei vorsichtig, {{ign}} möchte Gegenstände kaufen, die nicht angeboten werden ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/en/common.json b/src/green-app/src/locales/en/common.json new file mode 100644 index 00000000..86b56dc8 --- /dev/null +++ b/src/green-app/src/locales/en/common.json @@ -0,0 +1,211 @@ +{ + "action": { + "save": "Save", + "cancel": "Cancel", + "close": "Close", + "discard": "Discard", + "addGroup": "Add Group", + "view": "View", + "hardReset": "Reset Data", + "softReset": "Reset Data", + "logout": "Log out", + "login": "Connect PoE Account (SSO)", + "linkDiscord": "Link Discord", + "poeTradeDiscord": "PoE Trading Discord", + "postOffering": "Post Offering", + "loadTabs": "Load {{selected}} of {{total}} Tabs", + "refreshStashTabs": "Refresh Stashtabs", + "unselectStashTabs": "Unselect Stashtabs", + "bulkOfferingsPage": "Bulk Offerings", + "bulkListingsPage": "Bulk Listings", + "showUnreadNotificationsOnly": "Show unread only", + "clearAllNotifications": "Clear All", + "markAllNotifications": "Mark all as read", + "copyWhisper": "Copy whisper", + "copyWhisperAgain": "Copy again?", + "whisperCopied": "Whisper copied!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "Are you absolutely sure?", + "myOfferings": "My Offerings", + "bulkListing": "Bulk Listing", + "actions": "Actions", + "notifications": "Notifications", + "notificationSettings": "Notification Settings", + "itemFilter": "Item Filter" + }, + "label": { + "searchPh": "Search ...", + "selectCategoryPh": "Select category", + "selectSubCategoryPh": "Select subcategory", + "selectLeaguePh": "Select league", + "selectPh": "Select ...", + "noResults": "No results.", + "noItemResults": "No item found.", + "noOfferingResults": "No offerings found.", + "noNotificationResults": "You have no notifications 🎉", + "otherCalculations": "OTHER CALCULATIONS", + "wholeListingShort": "Whole offering", + "individualListingShort": "Individual items", + "wholeOfferingShort": "Whole offering", + "individualOfferingShort": "Individual items", + "wholeOffering": "Sell whole offering", + "individualOffering": "Sell individual items", + "individualOfferingTT": "Whole offering: Sell the whole offering at once", + "wholeOfferingTT": "Individual: Sell individual priced items", + "multiplier": "Multiplier: {{multiplier}}%", + "multiplierRange": "Multiplier: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Total:", + "totalPrice": "Total Price:", + "columnSettings": "Column settings", + "hardReset": "Hard Reset", + "columnSettingsResetTT": "Reset settings", + "discordTT": "Discord", + "account": "My Account", + "league": "League", + "loading": "Loading ...", + "loadingItems": "Loading items ...", + "offeringPreviewTT": "This selects only the stashtabs, category, subcategory and sell mode.\n\nAll other settings are taken from Your latest changes:", + "offeringPreviewLi1TT": "Multiplier per (sub-)category", + "offeringPreviewLi2TT": "Overrides/Overprices", + "offeringPreviewLi3TT": "Unselected items", + "notLoadedTT": "Not loaded", + "updatedTT": "Updated", + "maxNotification": "Maximum displayed notifications:", + "tradeInNewWindow": "Open trade requests in new window:", + "showDetailsTT": "Show Details", + "rowsSelectedOf": "{{current}} of {{total}} row(s) selected.", + "pageOf": "Page {{current}} of {{total}}", + "rowsPerPage": "Rows per page", + "firstPage": "Go to first page", + "prevPage": "Go to previous page", + "nextPage": "Go to next page", + "lastPage": "Go to last page", + "minPh": "MIN", + "multiplierLabel": "Multiplier:", + "askingPrice": "Asking Price:", + "sellMode": "Sell Mode:", + "seller": "Seller:", + "category": "Category:", + "language": "Language" + }, + "body": { + "fallbackRenderInfo": "If you confirm, all data will be reset. To fix the bug, it would help to report it. Thanks!", + "hardReset": "If you confirm, all data will be reset. This can help with bugs. To fix the bug, it would help to report it. Thanks!", + "characterInfo": "Level {{level}} {{class}}", + "wtbAll": " wtb all {{category}}", + "wtbPartial": " wtb {{count}} {{category}}", + "softReset": "This action cannot be undone.\n\n If you confirm, the following data will be reset:", + "softResetLi1": "Multiplier per (sub-)category", + "softResetLi2": "Sell mode per (sub-)category", + "softResetLi3": "Overrides/Overprices", + "softResetLi4": "Unselected items", + "maxNotification": "Notifications are queued to appear when the maximum number is reached.", + "tradeInNewWindow": "Trade requests notifications appear when someone clicks “Copy Whisper” for one of your offers." + }, + "option": { + "AND": "AND", + "NOT": "NOT", + "COUNT": "COUNT", + "showAll": "Show all", + "showSelected": "Show selected", + "showUnselected": "Show unselected", + "showAllModes": "Show all modes", + "showWholeListings": "Show whole listings", + "showIndividualListings": "Show individual listings" + }, + "columnTitle": { + "selection": "Selection", + "2_day_history": "Price last 2 days", + "7_day_history": "Price last 7 days", + "multiplier": "Multiplier", + "name": "Name", + "selectedPrice": "Override", + "props": "Props", + "quantity": "Quantity", + "selectedQuantity": "Asking Quantity", + "tabs": "Tab", + "tag": "Tag", + "price": "Price", + "totalPrice": "Total Price", + "commulativePrice": "Commulative", + "category": "Category", + "listingMode": "Sell Mode", + "seller": "Seller", + "created": "Created", + "actions": "Actions" + }, + "categories": { + "compass": "Compasses", + "essence": "Essences", + "essence low": "Essences Low", + "essence high": "Essences High", + "heist": "Heist", + "contract": "Contracts", + "blueprint": "Blueprints", + "currency": "Currency", + "stackedDeck": "Stacked Decks", + "lifeforce": "Lifeforce", + "otherCurrency": "Other Currency", + "beast": "Beasts", + "delve": "Delve", + "fossil": "Fossils", + "resonator": "Resonators", + "catalyst": "Catalysts", + "sanctum": "Sanctum", + "uniqueRelic": "Unique Relics", + "forbiddenTome": "Forbidden Tomes", + "fragment": "Fragments", + "scarab": "Scarabs", + "breach": "Breach", + "legion": "Legion", + "invitation": "Invitation", + "mortalSet": "Mortal Set", + "sacrificeSet": "Sacrifice Set", + "shaperSet": "Shaper Set", + "elderSet": "Elder Set", + "conquererSet": "Conquerer Set", + "uberElderSet": "Uber Elder Set", + "simulacrum": "Simulacrum", + "otherFragments": "Other Fragments", + "card": "Cards", + "delirium orb": "Delirium Orbs", + "expedition": "Expedition", + "logbook": "Logbooks", + "artifact": "Artifacts", + "oil": "Oils", + "memory": "Memory", + "incubator": "Incubators", + "map": "Maps", + "shaperMap": "Shaper Maps", + "elderMap": "Elder Maps", + "conquerorMap": "Conquerors Maps" + }, + "relativeTime": { + "future": "in %s", + "past": "%s ago", + "s": "a few seconds", + "m": "a minute", + "mm": "%d minutes", + "h": "an hour", + "hh": "%d hours", + "d": "a day", + "dd": "%d days", + "M": "a month", + "MM": "%d months", + "y": "a year", + "yy": "%d years" + }, + "locales": { + "de": "Deutsch ", + "en": "English", + "es": "Español", + "fr": "Français", + "ja": "日本語", + "ko": "한국어", + "pt": "Português", + "ru": "Русский", + "zh": "中文" + } +} diff --git a/src/green-app/src/locales/en/notification.json b/src/green-app/src/locales/en/notification.json new file mode 100644 index 00000000..05a01296 --- /dev/null +++ b/src/green-app/src/locales/en/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Something went wrong: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Be careful {{ign}} wants to buy items which are not offered ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/es/common.json b/src/green-app/src/locales/es/common.json new file mode 100644 index 00000000..8767ad62 --- /dev/null +++ b/src/green-app/src/locales/es/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "Guardar", + "cancel": "Cancelar", + "close": "Cerrar", + "discard": "Descartar", + "addGroup": "Agregar Grupo", + "view": "Ver", + "hardReset": "Restablecer Datos", + "softReset": "Restablecer Datos", + "logout": "Cerrar Sesión", + "login": "Conectar Cuenta de PoE (SSO)", + "linkDiscord": "Enlazar Discord", + "poeTradeDiscord": "Discord de Comercio de PoE", + "postOffering": "Publicar Oferta", + "loadTabs": "Cargar {{selected}} de {{total}} Pestañas", + "refreshStashTabs": "Actualizar Pestañas de Almacenamiento", + "unselectStashTabs": "Anular selección de Pestañas de Almacenamiento", + "bulkOfferingsPage": "Ofertas a Granel", + "bulkListingsPage": "Listados a Granel", + "showUnreadNotificationsOnly": "Mostrar solo no leídas", + "clearAllNotifications": "Borrar Todo", + "markAllNotifications": "Marcar todo como leído", + "copyWhisper": "Copiar Susurro", + "copyWhisperAgain": "¿Copiar de nuevo?", + "whisperCopied": "Susurro copiado!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "¿Estás completamente seguro?", + "myOfferings": "Mis Ofertas", + "bulkListing": "Listado a Granel", + "actions": "Acciones", + "notifications": "Notificaciones", + "notificationSettings": "Configuración de Notificaciones", + "itemFilter": "Filtro de Ítems" + }, + "label": { + "searchPh": "Buscar ...", + "selectCategoryPh": "Seleccionar categoría", + "selectSubCategoryPh": "Seleccionar subcategoría", + "selectLeaguePh": "Seleccionar liga", + "selectPh": "Seleccionar ...", + "noResults": "Sin resultados.", + "noItemResults": "No se encontró ningún ítem.", + "noOfferingResults": "No se encontraron ofertas.", + "noNotificationResults": "No tienes notificaciones 🎉", + "otherCalculations": "OTROS CÁLCULOS", + "wholeListingShort": "Oferta Completa", + "individualListingShort": "Ítems Individuales", + "wholeOfferingShort": "Oferta Completa", + "individualOfferingShort": "Ítems Individuales", + "wholeOffering": "Vender oferta completa", + "individualOffering": "Vender ítems individuales", + "individualOfferingTT": "Oferta completa: Vender toda la oferta de una vez", + "wholeOfferingTT": "Individual: Vender ítems con precios individuales", + "multiplier": "Multiplicador: {{multiplier}}%", + "multiplierRange": "Multiplicador: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Total:", + "totalPrice": "Precio Total:", + "columnSettings": "Configuración de Columnas", + "hardReset": "Restablecimiento Duro", + "columnSettingsResetTT": "Restablecer configuración", + "discordTT": "Discord", + "account": "Mi Cuenta", + "league": "Liga", + "loading": "Cargando ...", + "loadingItems": "Cargando ítems ...", + "offeringPreviewTT": "Esto selecciona solo las pestañas de almacenamiento, categoría, subcategoría y modo de venta.\n\nTodas las demás configuraciones se toman de tus cambios más recientes:", + "offeringPreviewLi1TT": "Multiplicador por (sub-)categoría", + "offeringPreviewLi2TT": "Anulaciones/Sobreprecios", + "offeringPreviewLi3TT": "Ítems no seleccionados", + "notLoadedTT": "No cargado", + "updatedTT": "Actualizado", + "maxNotification": "Máximo de notificaciones mostradas:", + "tradeInNewWindow": "Las notificaciones de solicitudes de comercio aparecen cuando alguien hace clic en “Copiar Susurro” en una de tus ofertas.", + "showDetailsTT": "Mostrar detalles", + "rowsSelectedOf": "{{current}} de {{total}} fila(s) seleccionada(s).", + "pageOf": "Página {{current}} de {{total}}", + "rowsPerPage": "Filas por página", + "firstPage": "Ir a la primera página", + "prevPage": "Ir a la página anterior", + "nextPage": "Ir a la página siguiente", + "lastPage": "Ir a la última página", + "minPh": "MÍN", + "multiplierLabel": "Multiplicador:", + "askingPrice": "Precio solicitado:", + "sellMode": "Modo de venta:", + "seller": "Vendedor:", + "category": "Categoría:", + "language": "Idioma" + }, + "body": { + "fallbackRenderInfo": "Si confirmas, se restablecerán todos los datos. Para corregir el error, sería útil informarlo. ¡Gracias!", + "hardReset": "Si confirmas, se restablecerán todos los datos. Esto puede ayudar con los errores. Para corregir el error, sería útil informarlo. ¡Gracias!", + "characterInfo": "Nivel {{level}} {{class}}", + "wtbAll": " comprar todo {{category}}", + "wtbPartial": " comprar {{count}} {{category}}", + "softReset": "Esta acción no se puede deshacer.\n\n Si confirmas, se restablecerán los siguientes datos:", + "softResetLi1": "Multiplicador por (sub-)categoría", + "softResetLi2": "Modo de venta por (sub-)categoría", + "softResetLi3": "Anulaciones/Sobreprecios", + "softResetLi4": "Ítems no seleccionados", + "maxNotification": "Las notificaciones están en cola para aparecer cuando se alcance el número máximo.", + "tradeInNewWindow": "Las notificaciones de solicitudes de comercio aparecen cuando alguien hace clic en “Copiar Susurro” en una de tus ofertas." + }, + "option": { + "AND": "Y", + "NOT": "NO", + "COUNT": "CONTAR", + "showAll": "Mostrar todo", + "showSelected": "Mostrar seleccionados", + "showUnselected": "Mostrar no seleccionados", + "showAllModes": "Mostrar todos los modos", + "showWholeListings": "Mostrar listados completos", + "showIndividualListings": "Mostrar listados individuales" + }, + "columnTitle": { + "selection": "Selección", + "2_day_history": "Precio últimos 2 días", + "7_day_history": "Precio últimos 7 días", + "multiplier": "Multiplicador", + "name": "Nombre", + "selectedPrice": "Anulación", + "props": "Propiedades", + "quantity": "Cantidad", + "selectedQuantity": "Cantidad Solicitada", + "tabs": "Pestaña", + "tag": "Etiqueta", + "price": "Precio", + "totalPrice": "Precio Total", + "commulativePrice": "Acumulado", + "category": "Categoría", + "listingMode": "Modo de Venta", + "seller": "Vendedor", + "created": "Creado", + "actions": "Acciones" + }, + "categories": { + "compass": "Brújulas", + "essence": "Esencias", + "essence low": "Esencias Bajas", + "essence high": "Esencias Altas", + "heist": "Atraco", + "contract": "Contratos", + "blueprint": "Planos", + "currency": "Moneda", + "stackedDeck": "Mazos Apilados", + "lifeforce": "Fuerza Vital", + "otherCurrency": "Otra Moneda", + "beast": "Bestias", + "delve": "Incursión", + "fossil": "Fósiles", + "resonator": "Resonadores", + "catalyst": "Catalizadores", + "sanctum": "Santuario", + "uniqueRelic": "Reliquias Únicas", + "forbiddenTome": "Tomo Prohibido", + "fragment": "Fragmentos", + "scarab": "Scarabs", + "breach": "Breach", + "legion": "Legión", + "invitation": "Invitación", + "mortalSet": "Conjunto Mortal", + "sacrificeSet": "Conjunto de Sacrificio", + "shaperSet": "Conjunto de Shaper", + "elderSet": "Conjunto de Anciano", + "conquererSet": "Conjunto de Conquistador", + "uberElderSet": "Conjunto de Uber Anciano", + "simulacrum": "Simulacro", + "otherFragments": "Otros Fragmentos", + "card": "Cartas", + "delirium orb": "Orbes delirantes", + "expedition": "Expedición", + "logbook": "Libros de registro", + "artifact": "Artefactos", + "oil": "Aceites", + "memory": "Memoria", + "incubator": "Incubadoras", + "map": "Mapas", + "shaperMap": "Mapas de Shaper", + "elderMap": "Mapas de Anciano", + "conquerorMap": "Mapas de Conquistador" + }, + "relativeTime": { + "future": "en %s", + "past": "hace %s", + "s": "unos segundos", + "m": "un minuto", + "mm": "%d minutos", + "h": "una hora", + "hh": "%d horas", + "d": "un día", + "dd": "%d días", + "M": "un mes", + "MM": "%d meses", + "y": "un año", + "yy": "%d años" + } +} diff --git a/src/green-app/src/locales/es/notification.json b/src/green-app/src/locales/es/notification.json new file mode 100644 index 00000000..c7b21194 --- /dev/null +++ b/src/green-app/src/locales/es/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Algo salió mal: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Ten cuidado, {{ign}} quiere comprar artículos que no están siendo ofrecidos..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/fr/common.json b/src/green-app/src/locales/fr/common.json new file mode 100644 index 00000000..3fdcfcfc --- /dev/null +++ b/src/green-app/src/locales/fr/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "Enregistrer", + "cancel": "Annuler", + "close": "Fermer", + "discard": "Annuler", + "addGroup": "Ajouter un groupe", + "view": "Voir", + "hardReset": "Réinitialiser les données", + "softReset": "Réinitialiser les données", + "logout": "Déconnexion", + "login": "Connecter le compte PoE (SSO)", + "linkDiscord": "Lier Discord", + "poeTradeDiscord": "Discord d'échange PoE", + "postOffering": "Publier une offre", + "loadTabs": "Charger {{selected}} de {{total}} onglets", + "refreshStashTabs": "Actualiser les onglets Stashtabs", + "unselectStashTabs": "Désélectionner les onglets Stashtabs", + "bulkOfferingsPage": "Offres en vrac", + "bulkListingsPage": "Listes en vrac", + "showUnreadNotificationsOnly": "Afficher uniquement les non lus", + "clearAllNotifications": "Effacer tout", + "markAllNotifications": "Tout marquer comme lu", + "copyWhisper": "Copier chuchotement", + "copyWhisperAgain": "Copier à nouveau ?", + "whisperCopied": "Chuchotement copié !" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "Êtes-vous absolument sûr ?", + "myOfferings": "Mes offres", + "bulkListing": "Listage en vrac", + "actions": "Actions", + "notifications": "Notifications", + "notificationSettings": "Paramètres de notification", + "itemFilter": "Filtre d'objet" + }, + "label": { + "searchPh": "Recherche...", + "selectCategoryPh": "Sélectionnez une catégorie", + "selectSubCategoryPh": "Sélectionnez une sous-catégorie", + "selectLeaguePh": "Sélectionnez une ligue", + "selectPh": "Sélectionner...", + "noResults": "Aucun résultat.", + "noItemResults": "Aucun objet trouvé.", + "noOfferingResults": "Aucune offre trouvée.", + "noNotificationResults": "Vous n'avez aucune notification 🎉", + "otherCalculations": "AUTRES CALCULS", + "wholeListingShort": "Listing complet", + "individualListingShort": "Objets individuels", + "wholeOfferingShort": "Offre complète", + "individualOfferingShort": "Objets individuels", + "wholeOffering": "Vendre l'offre complète", + "individualOffering": "Vendre des objets individuels", + "individualOfferingTT": "Offre complète : Vendre toute l'offre en une seule fois", + "wholeOfferingTT": "Individuel : Vendre des objets à prix unitaire", + "multiplier": "Multiplicateur : {{multiplier}}%", + "multiplierRange": "Multiplicateur : {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Total :", + "totalPrice": "Prix total :", + "columnSettings": "Paramètres de colonne", + "hardReset": "Réinitialisation complète", + "columnSettingsResetTT": "Réinitialiser les paramètres", + "discordTT": "Discord", + "account": "Mon compte", + "league": "Ligue", + "loading": "Chargement...", + "loadingItems": "Chargement des objets...", + "offeringPreviewTT": "Ceci sélectionne uniquement les onglets, la catégorie, la sous-catégorie et le mode de vente.\n\nTous les autres paramètres sont pris à partir de vos dernières modifications :", + "offeringPreviewLi1TT": "Multiplicateur par (sous-)catégorie", + "offeringPreviewLi2TT": "Substitutions/Surévaluations", + "offeringPreviewLi3TT": "Objets non sélectionnés", + "notLoadedTT": "Non chargé", + "updatedTT": "Mis à jour", + "maxNotification": "Nombre maximum de notifications affichées :", + "tradeInNewWindow": "Les notifications des demandes d'échange apparaissent lorsqu'un utilisateur clique sur « Copier chuchotement » pour l'une de vos offres.", + "showDetailsTT": "Afficher les détails", + "rowsSelectedOf": "{{current}} de {{total}} ligne(s) sélectionnée(s).", + "pageOf": "Page {{current}} de {{total}}", + "rowsPerPage": "Lignes par page", + "firstPage": "Aller à la première page", + "prevPage": "Aller à la page précédente", + "nextPage": "Aller à la page suivante", + "lastPage": "Aller à la dernière page", + "minPh": "MIN", + "multiplierLabel": "Multiplicateur :", + "askingPrice": "Prix demandé :", + "sellMode": "Mode de vente :", + "seller": "Vendeur :", + "category": "Catégorie :", + "language": "Langue" + }, + "body": { + "fallbackRenderInfo": "Si vous confirmez, toutes les données seront réinitialisées. Pour corriger le bogue, il serait utile de le signaler. Merci !", + "hardReset": "Si vous confirmez, toutes les données seront réinitialisées. Cela peut aider à résoudre les bogues. Pour corriger le bogue, il serait utile de le signaler. Merci !", + "characterInfo": "Niveau {{level}} {{class}}", + "wtbAll": " wtb tous les {{category}}", + "wtbPartial": " wtb {{count}} {{category}}", + "softReset": "Cette action ne peut pas être annulée.\n\n Si vous confirmez, les données suivantes seront réinitialisées :", + "softResetLi1": "Multiplicateur par (sous-)catégorie", + "softResetLi2": "Mode de vente par (sous-)catégorie", + "softResetLi3": "Substitutions/Surévaluations", + "softResetLi4": "Objets non sélectionnés", + "maxNotification": "Les notifications sont mises en file d'attente pour apparaître lorsque le nombre maximum est atteint.", + "tradeInNewWindow": "Les notifications des demandes d'échange apparaissent lorsque quelqu'un clique sur « Copier chuchotement » pour l'une de vos offres." + }, + "option": { + "AND": "ET", + "NOT": "NON", + "COUNT": "NOMBRE", + "showAll": "Afficher tout", + "showSelected": "Afficher les sélectionnés", + "showUnselected": "Afficher les non sélectionnés", + "showAllModes": "Afficher tous les modes", + "showWholeListings": "Afficher tous les listings", + "showIndividualListings": "Afficher les listings individuels" + }, + "columnTitle": { + "selection": "Sélection", + "2_day_history": "Prix des 2 derniers jours", + "7_day_history": "Prix des 7 derniers jours", + "multiplier": "Multiplicateur", + "name": "Nom", + "selectedPrice": "Substitution", + "props": "Propriétés", + "quantity": "Quantité", + "selectedQuantity": "Quantité demandée", + "tabs": "Onglet", + "tag": "Étiquette", + "price": "Prix", + "totalPrice": "Prix total", + "commulativePrice": "Cumulatif", + "category": "Catégorie", + "listingMode": "Mode de vente", + "seller": "Vendeur", + "created": "Créé", + "actions": "Actions" + }, + "categories": { + "compass": "Boussoles", + "essence": "Essences", + "essence low": "Essences Faibles", + "essence high": "Essences Fortes", + "heist": "Casse", + "contract": "Contrats", + "blueprint": "Plans", + "currency": "Monnaie", + "stackedDeck": "Paquets Empilés", + "lifeforce": "Force Vitale", + "otherCurrency": "Autre Monnaie", + "beast": "Bêtes", + "delve": "Fosses", + "fossil": "Fossiles", + "resonator": "Résonateurs", + "catalyst": "Catalyseurs", + "sanctum": "Sanctuaire", + "uniqueRelic": "Reliques Uniques", + "forbiddenTome": "Tome Interdit", + "fragment": "Fragments", + "scarab": "Scarabées", + "breach": "Faille", + "legion": "Légion", + "invitation": "Invitation", + "mortalSet": "Ensemble Mortel", + "sacrificeSet": "Ensemble de Sacrifice", + "shaperSet": "Ensemble de Façonneur", + "elderSet": "Ensemble d'Aîné", + "conquererSet": "Ensemble de Conquérant", + "uberElderSet": "Ensemble d'Uber Aîné", + "simulacrum": "Simulacre", + "otherFragments": "Autres Fragments", + "card": "Cartes", + "delirium orb": "Orbes de Délire", + "expedition": "Expédition", + "logbook": "Journaux de Bord", + "artifact": "Artefacts", + "oil": "Huiles", + "memory": "Mémoire", + "incubator": "Incubateurs", + "map": "Cartes", + "shaperMap": "Cartes de Façonneur", + "elderMap": "Cartes d'Aîné", + "conquerorMap": "Cartes de Conquérant" + }, + "relativeTime": { + "future": "dans %s", + "past": "il y a %s", + "s": "quelques secondes", + "m": "une minute", + "mm": "%d minutes", + "h": "une heure", + "hh": "%d heures", + "d": "un jour", + "dd": "%d jours", + "M": "un mois", + "MM": "%d mois", + "y": "un an", + "yy": "%d ans" + } +} diff --git a/src/green-app/src/locales/fr/notification.json b/src/green-app/src/locales/fr/notification.json new file mode 100644 index 00000000..fdaefe27 --- /dev/null +++ b/src/green-app/src/locales/fr/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Quelque chose s'est mal passé : {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Fais attention, {{ign}} veut acheter des objets qui ne sont pas offerts ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/ja/common.json b/src/green-app/src/locales/ja/common.json new file mode 100644 index 00000000..97108fd6 --- /dev/null +++ b/src/green-app/src/locales/ja/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "保存", + "cancel": "キャンセル", + "close": "閉じる", + "discard": "破棄", + "addGroup": "グループを追加", + "view": "表示", + "hardReset": "データをリセット", + "softReset": "データをリセット", + "logout": "ログアウト", + "login": "PoEアカウントに接続(SSO)", + "linkDiscord": "Discordをリンク", + "poeTradeDiscord": "PoEトレードDiscord", + "postOffering": "オファリングを投稿", + "loadTabs": "{{selected}} / {{total}}タブを読み込む", + "refreshStashTabs": "スタッシュタブを更新", + "unselectStashTabs": "スタッシュタブの選択を解除", + "bulkOfferingsPage": "一括オファリング", + "bulkListingsPage": "一括リスト", + "showUnreadNotificationsOnly": "未読のみ表示", + "clearAllNotifications": "すべてクリア", + "markAllNotifications": "すべてを既読にする", + "copyWhisper": "ささやきをコピー", + "copyWhisperAgain": "再度コピーしますか?", + "whisperCopied": "ささやきがコピーされました!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "本当に確実ですか?", + "myOfferings": "マイオファリング", + "bulkListing": "一括リスト", + "actions": "アクション", + "notifications": "通知", + "notificationSettings": "通知設定", + "itemFilter": "アイテムフィルター" + }, + "label": { + "searchPh": "検索...", + "selectCategoryPh": "カテゴリーを選択", + "selectSubCategoryPh": "サブカテゴリーを選択", + "selectLeaguePh": "リーグを選択", + "selectPh": "選択...", + "noResults": "結果なし。", + "noItemResults": "アイテムが見つかりません。", + "noOfferingResults": "オファリングが見つかりません。", + "noNotificationResults": "通知はありません 🎉", + "otherCalculations": "その他の計算", + "wholeListingShort": "全体のオファリング", + "individualListingShort": "個別のアイテム", + "wholeOfferingShort": "全体のオファリング", + "individualOfferingShort": "個別のアイテム", + "wholeOffering": "全体のオファリングを売る", + "individualOffering": "個別のアイテムを売る", + "individualOfferingTT": "全体のオファリング:一度に全体のオファリングを販売します", + "wholeOfferingTT": "個別:個別の価格設定されたアイテムを販売します", + "multiplier": "乗数:{{multiplier}}%", + "multiplierRange": "乗数:{{multiplierFrom}}% - {{multiplierTo}}%", + "total": "合計:", + "totalPrice": "合計価格:", + "columnSettings": "列の設定", + "hardReset": "ハードリセット", + "columnSettingsResetTT": "設定をリセット", + "discordTT": "Discord", + "account": "マイアカウント", + "league": "リーグ", + "loading": "読み込み中...", + "loadingItems": "アイテムを読み込み中...", + "offeringPreviewTT": "これにより、スタッシュタブ、カテゴリー、サブカテゴリー、および販売モードのみが選択されます。\n\n他のすべての設定は、最新の変更から取得されます:", + "offeringPreviewLi1TT": "(サブ)カテゴリーごとの乗数", + "offeringPreviewLi2TT": "オーバーライド/オーバープライス", + "offeringPreviewLi3TT": "選択されていないアイテム", + "notLoadedTT": "読み込まれていません", + "updatedTT": "更新済み", + "maxNotification": "表示される通知の最大数:", + "tradeInNewWindow": "取引要求の通知は、誰かがあなたのオファーの1つに「ささやきをコピー」をクリックしたときに表示されます。", + "showDetailsTT": "詳細を表示", + "rowsSelectedOf": "{{current}} / {{total}} 行が選択されました。", + "pageOf": "{{current}} / {{total}}ページ目", + "rowsPerPage": "ページごとの行数", + "firstPage": "最初のページに移動", + "prevPage": "前のページに移動", + "nextPage": "次のページに移動", + "lastPage": "最後のページに移動", + "minPh": "最小", + "multiplierLabel": "乗数:", + "askingPrice": "要求価格:", + "sellMode": "販売モード:", + "seller": "販売者:", + "category": "カテゴリー:", + "language": "言語" + }, + "body": { + "fallbackRenderInfo": "確認すると、すべてのデータがリセットされます。バグを修正するには、報告してください。ありがとうございます!", + "hardReset": "確認すると、すべてのデータがリセットされます。これはバグを修正するのに役立ちます。バグを修正するには、報告してください。ありがとうございます!", + "characterInfo": "レベル{{level}} {{class}}", + "wtbAll": "{{category}}全体を購入", + "wtbPartial": "{{count}} {{category}}を部分的に購入", + "softReset": "このアクションは取り消すことができません。\n\n確認すると、次のデータがリセットされます:", + "softResetLi1": "(サブ)カテゴリーごとの乗数", + "softResetLi2": "(サブ)カテゴリーごとの販売モード", + "softResetLi3": "オーバーライド/値上げ", + "softResetLi4": "選択されていないアイテム", + "maxNotification": "通知は、最大数に達したときに表示されるようにキューに入れられます。", + "tradeInNewWindow": "取引要求の通知は、誰かがあなたのオファーの1つに「ささやきをコピー」をクリックしたときに表示されます。" + }, + "option": { + "AND": "AND", + "NOT": "NOT", + "COUNT": "COUNT", + "showAll": "すべて表示", + "showSelected": "選択したものを表示", + "showUnselected": "選択されていないものを表示", + "showAllModes": "すべてのモードを表示", + "showWholeListings": "全リストを表示", + "showIndividualListings": "個別のリストを表示" + }, + "columnTitle": { + "selection": "選択", + "2_day_history": "過去2日間の価格", + "7_day_history": "過去7日間の価格", + "multiplier": "乗数", + "name": "名前", + "selectedPrice": "オーバーライド", + "props": "プロパティ", + "quantity": "数量", + "selectedQuantity": "数量を要求", + "tabs": "タブ", + "tag": "タグ", + "price": "価格", + "totalPrice": "合計価格", + "commulativePrice": "累積", + "category": "カテゴリー", + "listingMode": "販売モード", + "seller": "販売者", + "created": "作成日", + "actions": "アクション" + }, + "categories": { + "compass": "コンパス", + "essence": "エッセンス", + "essence low": "低エッセンス", + "essence high": "高エッセンス", + "heist": "ハイスト", + "contract": "契約書", + "blueprint": "設計図", + "currency": "通貨", + "stackedDeck": "重ねられたデッキ", + "lifeforce": "生命力", + "otherCurrency": "その他の通貨", + "beast": "獣", + "delve": "ダイブ", + "fossil": "化石", + "resonator": "共鳴器", + "catalyst": "触媒", + "sanctum": "聖域", + "uniqueRelic": "ユニークな遺物", + "forbiddenTome": "禁断の書", + "fragment": "断片", + "scarab": "スカラベ", + "breach": "ブリーチ", + "legion": "軍団", + "invitation": "招待", + "mortalSet": "不滅のセット", + "sacrificeSet": "犠牲のセット", + "shaperSet": "シェイパーのセット", + "elderSet": "長老のセット", + "conquererSet": "征服者のセット", + "uberElderSet": "ウーバーエルダーセット", + "simulacrum": "シミュラクラム", + "otherFragments": "その他の断片", + "card": "カード", + "delirium orb": "狂気のオーブ", + "expedition": "遠征", + "logbook": "ログブック", + "artifact": "アーティファクト", + "oil": "オイル", + "memory": "メモリ", + "incubator": "インキュベーター", + "map": "地図", + "shaperMap": "シェイパーマップ", + "elderMap": "長老マップ", + "conquerorMap": "征服者マップ" + }, + "relativeTime": { + "future": "%s後", + "past": "%s前", + "s": "数秒", + "m": "1分", + "mm": "%d分", + "h": "1時間", + "hh": "%d時間", + "d": "1日", + "dd": "%d日", + "M": "1ヶ月", + "MM": "%dヶ月", + "y": "1年", + "yy": "%d年" + } +} diff --git a/src/green-app/src/locales/ja/notification.json b/src/green-app/src/locales/ja/notification.json new file mode 100644 index 00000000..f5ef8b90 --- /dev/null +++ b/src/green-app/src/locales/ja/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "何かがうまくいきませんでした:{{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "{{ign}}が提供されていないアイテムを購入しようとしています..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/ko/common.json b/src/green-app/src/locales/ko/common.json new file mode 100644 index 00000000..28a82084 --- /dev/null +++ b/src/green-app/src/locales/ko/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "저장", + "cancel": "취소", + "close": "닫기", + "discard": "포기", + "addGroup": "그룹 추가", + "view": "보기", + "hardReset": "데이터 재설정", + "softReset": "데이터 재설정", + "logout": "로그아웃", + "login": "PoE 계정 연결 (SSO)", + "linkDiscord": "디스코드 링크", + "poeTradeDiscord": "PoE 거래 디스코드", + "postOffering": "오퍼링 게시", + "loadTabs": "{{selected}} / {{total}} 탭 불러오기", + "refreshStashTabs": "보관함 탭 새로고침", + "unselectStashTabs": "보관함 탭 선택 취소", + "bulkOfferingsPage": "대량 오퍼링", + "bulkListingsPage": "대량 목록", + "showUnreadNotificationsOnly": "읽지 않은 것만 표시", + "clearAllNotifications": "모두 지우기", + "markAllNotifications": "모두 읽음으로 표시", + "copyWhisper": "속삭임 복사", + "copyWhisperAgain": "다시 복사?", + "whisperCopied": "속삭임이 복사되었습니다!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "정말로 확실합니까?", + "myOfferings": "내 오퍼링", + "bulkListing": "대량 목록", + "actions": "작업", + "notifications": "알림", + "notificationSettings": "알림 설정", + "itemFilter": "아이템 필터" + }, + "label": { + "searchPh": "검색 ...", + "selectCategoryPh": "카테고리 선택", + "selectSubCategoryPh": "하위 카테고리 선택", + "selectLeaguePh": "리그 선택", + "selectPh": "선택 ...", + "noResults": "결과 없음.", + "noItemResults": "아이템을 찾을 수 없음.", + "noOfferingResults": "오퍼링을 찾을 수 없음.", + "noNotificationResults": "알림이 없습니다 🎉", + "otherCalculations": "기타 계산", + "wholeListingShort": "전체 오퍼링", + "individualListingShort": "개별 항목", + "wholeOfferingShort": "전체 오퍼링", + "individualOfferingShort": "개별 항목", + "wholeOffering": "전체 오퍼링 판매", + "individualOffering": "개별 항목 판매", + "individualOfferingTT": "전체 오퍼링: 한 번에 전체 오퍼링 판매", + "wholeOfferingTT": "개별 항목: 개별 가격 항목 판매", + "multiplier": "배수: {{multiplier}}%", + "multiplierRange": "배수: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "총:", + "totalPrice": "총 가격:", + "columnSettings": "열 설정", + "hardReset": "하드 리셋", + "columnSettingsResetTT": "설정 초기화", + "discordTT": "디스코드", + "account": "내 계정", + "league": "리그", + "loading": "로드 중 ...", + "loadingItems": "아이템 로드 중 ...", + "offeringPreviewTT": "여기서는 스태시 탭, 카테고리, 하위 카테고리 및 판매 모드만 선택됩니다.\n\n다른 모든 설정은 최신 변경 사항에서 가져옵니다:", + "offeringPreviewLi1TT": "카테고리 당 배수", + "offeringPreviewLi2TT": "오버라이드/가격 인상", + "offeringPreviewLi3TT": "선택되지 않은 항목", + "notLoadedTT": "로드되지 않음", + "updatedTT": "업데이트됨", + "maxNotification": "최대 표시되는 알림 수:", + "tradeInNewWindow": "거래 요청 알림은 누군가가 귀하의 제안 중 하나를 “속삭임 복사” 버튼을 클릭할 때 나타납니다.", + "showDetailsTT": "세부 정보 표시", + "rowsSelectedOf": "{{total}}개의 행 중 {{current}}개 선택됨.", + "pageOf": "{{total}} 페이지 중 {{current}} 페이지", + "rowsPerPage": "페이지 당 행 수", + "firstPage": "첫 페이지로 이동", + "prevPage": "이전 페이지로 이동", + "nextPage": "다음 페이지로 이동", + "lastPage": "마지막 페이지로 이동", + "minPh": "최소", + "multiplierLabel": "배수:", + "askingPrice": "가격 문의:", + "sellMode": "판매 모드:", + "seller": "판매자:", + "category": "카테고리:", + "language": "언어" + }, + "body": { + "fallbackRenderInfo": "확인하면 모든 데이터가 재설정됩니다. 버그를 수정하려면 보고서를 제출하는 것이 도움이 됩니다. 감사합니다!", + "hardReset": "확인하면 모든 데이터가 재설정됩니다. 버그를 수정하는 데 도움이 됩니다. 보고서를 제출하여 버그를 수정하세요. 감사합니다!", + "characterInfo": "레벨 {{level}} {{class}}", + "wtbAll": "{{category}} 모두 구매", + "wtbPartial": "{{count}} {{category}} 구매", + "softReset": "이 작업은 취소할 수 없습니다.\n\n 확인하면 다음 데이터가 재설정됩니다:", + "softResetLi1": "카테고리 당 배수", + "softResetLi2": "카테고리 당 판매 모드", + "softResetLi3": "오버라이드/가격 인상", + "softResetLi4": "선택되지 않은 항목", + "maxNotification": "최대 표시 알림이 최대 수에 도달하면 대기열에 있습니다.", + "tradeInNewWindow": "거래 요청 알림은 누군가가 귀하의 제안 중 하나에 대해 “속삭임 복사”를 클릭할 때 표시됩니다." + }, + "option": { + "AND": "그리고", + "NOT": "아님", + "COUNT": "개수", + "showAll": "모두 표시", + "showSelected": "선택한 항목 표시", + "showUnselected": "선택하지 않은 항목 표시", + "showAllModes": "모든 모드 표시", + "showWholeListings": "전체 목록 표시", + "showIndividualListings": "개별 목록 표시" + }, + "columnTitle": { + "selection": "선택", + "2_day_history": "지난 2일 동안의 가격", + "7_day_history": "지난 7일 동안의 가격", + "multiplier": "배수", + "name": "이름", + "selectedPrice": "오버라이드", + "props": "속성", + "quantity": "수량", + "selectedQuantity": "요청 수량", + "tabs": "탭", + "tag": "태그", + "price": "가격", + "totalPrice": "총 가격", + "commulativePrice": "누적 가격", + "category": "카테고리", + "listingMode": "판매 모드", + "seller": "판매자", + "created": "생성됨", + "actions": "작업" + }, + "categories": { + "compass": "나침반", + "essence": "에센스", + "essence low": "저레벨 에센스", + "essence high": "고레벨 에센스", + "heist": "강탈", + "contract": "계약서", + "blueprint": "설계도", + "currency": "통화", + "stackedDeck": "중첩 덱", + "lifeforce": "생명력", + "otherCurrency": "기타 통화", + "beast": "야수", + "delve": "진입", + "fossil": "화석", + "resonator": "공명체", + "catalyst": "촉매", + "sanctum": "성소", + "uniqueRelic": "고유 유물", + "forbiddenTome": "금기의 서", + "fragment": "파편", + "scarab": "스카라베", + "breach": "이음", + "legion": "군단", + "invitation": "초대장", + "mortalSet": "죽음의 세트", + "sacrificeSet": "희생의 세트", + "shaperSet": "쉐이퍼의 세트", + "elderSet": "장로의 세트", + "conquererSet": "정복자의 세트", + "uberElderSet": "우버 장로 세트", + "simulacrum": "모의", + "otherFragments": "기타 파편", + "card": "카드", + "delirium orb": "광기의 오브", + "expedition": "원정", + "logbook": "로그북", + "artifact": "유물", + "oil": "오일", + "memory": "기억", + "incubator": "부화기", + "map": "지도", + "shaperMap": "쉐이퍼 맵", + "elderMap": "장로 맵", + "conquerorMap": "정복자 맵" + }, + "relativeTime": { + "future": "%s 후", + "past": "%s 전", + "s": "몇 초", + "m": "1분", + "mm": "%d분", + "h": "한 시간", + "hh": "%d시간", + "d": "하루", + "dd": "%d일", + "M": "한 달", + "MM": "%d달", + "y": "일 년", + "yy": "%d년" + } +} diff --git a/src/green-app/src/locales/ko/notification.json b/src/green-app/src/locales/ko/notification.json new file mode 100644 index 00000000..206220a2 --- /dev/null +++ b/src/green-app/src/locales/ko/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "문제가 발생했습니다: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "{{ign}}님이 제공되지 않는 항목을 구매하려고 합니다..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/pt/common.json b/src/green-app/src/locales/pt/common.json new file mode 100644 index 00000000..e31e3f4f --- /dev/null +++ b/src/green-app/src/locales/pt/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "Salvar", + "cancel": "Cancelar", + "close": "Fechar", + "discard": "Descartar", + "addGroup": "Adicionar Grupo", + "view": "Visualizar", + "hardReset": "Redefinir Dados", + "softReset": "Redefinir Dados", + "logout": "Sair", + "login": "Conectar Conta PoE (SSO)", + "linkDiscord": "Link Discord", + "poeTradeDiscord": "Discord de Negociação PoE", + "postOffering": "Publicar Oferta", + "loadTabs": "Carregar {{selected}} de {{total}} Abas", + "refreshStashTabs": "Atualizar Abas Stashtabs", + "unselectStashTabs": "Desmarcar Abas Stashtabs", + "bulkOfferingsPage": "Ofertas em Massa", + "bulkListingsPage": "Listagens em Massa", + "showUnreadNotificationsOnly": "Mostrar apenas não lidos", + "clearAllNotifications": "Limpar Tudo", + "markAllNotifications": "Marcar todos como lidos", + "copyWhisper": "Copiar Sussurro", + "copyWhisperAgain": "Copiar novamente?", + "whisperCopied": "Sussurro copiado!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "Tem certeza absoluta?", + "myOfferings": "Minhas Ofertas", + "bulkListing": "Listagem em Massa", + "actions": "Ações", + "notifications": "Notificações", + "notificationSettings": "Configurações de Notificação", + "itemFilter": "Filtro de Item" + }, + "label": { + "searchPh": "Pesquisar...", + "selectCategoryPh": "Selecionar categoria", + "selectSubCategoryPh": "Selecionar subcategoria", + "selectLeaguePh": "Selecionar liga", + "selectPh": "Selecionar...", + "noResults": "Sem resultados.", + "noItemResults": "Nenhum item encontrado.", + "noOfferingResults": "Nenhuma oferta encontrada.", + "noNotificationResults": "Você não tem notificações 🎉", + "otherCalculations": "OUTROS CÁLCULOS", + "wholeListingShort": "Listagem completa", + "individualListingShort": "Itens individuais", + "wholeOfferingShort": "Oferta completa", + "individualOfferingShort": "Itens individuais", + "wholeOffering": "Vender oferta completa", + "individualOffering": "Vender itens individuais", + "individualOfferingTT": "Oferta completa: Vender toda a oferta de uma vez", + "wholeOfferingTT": "Individual: Vender itens com preço unitário", + "multiplier": "Multiplicador: {{multiplier}}%", + "multiplierRange": "Multiplicador: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Total:", + "totalPrice": "Preço Total:", + "columnSettings": "Configurações de Coluna", + "hardReset": "Redefinição Total", + "columnSettingsResetTT": "Redefinir configurações", + "discordTT": "Discord", + "account": "Minha Conta", + "league": "Liga", + "loading": "Carregando...", + "loadingItems": "Carregando itens...", + "offeringPreviewTT": "Isso seleciona apenas as abas, categoria, subcategoria e modo de venda.\n\nTodas as outras configurações são retiradas das suas últimas alterações:", + "offeringPreviewLi1TT": "Multiplicador por (sub-)categoria", + "offeringPreviewLi2TT": "Substituições/Superavaliações", + "offeringPreviewLi3TT": "Itens não selecionados", + "notLoadedTT": "Não carregado", + "updatedTT": "Atualizado", + "maxNotification": "Número máximo de notificações exibidas:", + "tradeInNewWindow": "As notificações de solicitações de troca aparecem quando alguém clica em 'Copiar sussurro' para uma de suas ofertas.", + "showDetailsTT": "Mostrar detalhes", + "rowsSelectedOf": "{{current}} de {{total}} linha(s) selecionada(s).", + "pageOf": "Página {{current}} de {{total}}", + "rowsPerPage": "Linhas por página", + "firstPage": "Ir para a primeira página", + "prevPage": "Ir para a página anterior", + "nextPage": "Ir para a próxima página", + "lastPage": "Ir para a última página", + "minPh": "MÍN", + "multiplierLabel": "Multiplicador:", + "askingPrice": "Preço Pedido:", + "sellMode": "Modo de Venda:", + "seller": "Vendedor:", + "category": "Categoria:", + "language": "Língua" + }, + "body": { + "fallbackRenderInfo": "Se você confirmar, todos os dados serão redefinidos. Para corrigir o bug, seria útil relatar isso. Obrigado!", + "hardReset": "Se você confirmar, todos os dados serão redefinidos. Isso pode ajudar com bugs. Para corrigir o bug, seria útil relatar isso. Obrigado!", + "characterInfo": "Nível {{level}} {{class}}", + "wtbAll": " wtb todos {{category}}", + "wtbPartial": " wtb {{count}} {{category}}", + "softReset": "Esta ação não pode ser desfeita.\n\n Se você confirmar, os seguintes dados serão redefinidos:", + "softResetLi1": "Multiplicador por (sub-)categoria", + "softResetLi2": "Modo de venda por (sub-)categoria", + "softResetLi3": "Substituições/Superavaliações", + "softResetLi4": "Itens não selecionados", + "maxNotification": "As notificações estão enfileiradas para aparecer quando o número máximo for atingido.", + "tradeInNewWindow": "As notificações de solicitações de troca aparecem quando alguém clica em 'Copiar sussurro' para uma de suas ofertas." + }, + "option": { + "AND": "E", + "NOT": "NÃO", + "COUNT": "CONTAR", + "showAll": "Mostrar tudo", + "showSelected": "Mostrar selecionados", + "showUnselected": "Mostrar não selecionados", + "showAllModes": "Mostrar todos os modos", + "showWholeListings": "Mostrar listagens completas", + "showIndividualListings": "Mostrar listagens individuais" + }, + "columnTitle": { + "selection": "Seleção", + "2_day_history": "Preço dos últimos 2 dias", + "7_day_history": "Preço dos últimos 7 dias", + "multiplier": "Multiplicador", + "name": "Nome", + "selectedPrice": "Substituição", + "props": "Propriedades", + "quantity": "Quantidade", + "selectedQuantity": "Quantidade Solicitada", + "tabs": "Abas", + "tag": "Tag", + "price": "Preço", + "totalPrice": "Preço Total", + "commulativePrice": "Cumulativo", + "category": "Categoria", + "listingMode": "Modo de Venda", + "seller": "Vendedor", + "created": "Criado", + "actions": "Ações" + }, + "categories": { + "compass": "Bússolas", + "essence": "Essências", + "essence low": "Essências Baixas", + "essence high": "Essências Altas", + "heist": "Assalto", + "contract": "Contratos", + "blueprint": "Blueprints", + "currency": "Moeda", + "stackedDeck": "Baralhos Empilhados", + "lifeforce": "Força Vital", + "otherCurrency": "Outra Moeda", + "beast": "Bestas", + "delve": "Investida", + "fossil": "Fósseis", + "resonator": "Ressonadores", + "catalyst": "Catalisadores", + "sanctum": "Santuário", + "uniqueRelic": "Relíquias Únicas", + "forbiddenTome": "Tomo Proibido", + "fragment": "Fragmentos", + "scarab": "Escaravelhos", + "breach": "Brecha", + "legion": "Legião", + "invitation": "Convite", + "mortalSet": "Conjunto Mortal", + "sacrificeSet": "Conjunto de Sacrifício", + "shaperSet": "Conjunto de Moldador", + "elderSet": "Conjunto de Ancião", + "conquererSet": "Conjunto de Conquistador", + "uberElderSet": "Conjunto de Super Ancião", + "simulacrum": "Simulacro", + "otherFragments": "Outros Fragmentos", + "card": "Cartas", + "delirium orb": "Orbes do Delírio", + "expedition": "Expedição", + "logbook": "Livros de Registro", + "artifact": "Artefatos", + "oil": "Óleos", + "memory": "Memória", + "incubator": "Incubadoras", + "map": "Mapas", + "shaperMap": "Mapas do Moldador", + "elderMap": "Mapas do Ancião", + "conquerorMap": "Mapas do Conquistador" + }, + "relativeTime": { + "future": "em %s", + "past": "%s atrás", + "s": "alguns segundos", + "m": "um minuto", + "mm": "%d minutos", + "h": "uma hora", + "hh": "%d horas", + "d": "um dia", + "dd": "%d dias", + "M": "um mês", + "MM": "%d meses", + "y": "um ano", + "yy": "%d anos" + } +} diff --git a/src/green-app/src/locales/pt/notification.json b/src/green-app/src/locales/pt/notification.json new file mode 100644 index 00000000..eca96152 --- /dev/null +++ b/src/green-app/src/locales/pt/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Algo deu errado: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Cuidado, {{ign}} quer comprar itens que não estão sendo oferecidos ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/ru/common.json b/src/green-app/src/locales/ru/common.json new file mode 100644 index 00000000..6e8733b8 --- /dev/null +++ b/src/green-app/src/locales/ru/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "Сохранить", + "cancel": "Отмена", + "close": "Закрыть", + "discard": "Отменить", + "addGroup": "Добавить группу", + "view": "Просмотр", + "hardReset": "Сброс данных", + "softReset": "Сброс данных", + "logout": "Выйти", + "login": "Подключить учетную запись PoE (SSO)", + "linkDiscord": "Ссылка на Discord", + "poeTradeDiscord": "Торговый Discord PoE", + "postOffering": "Опубликовать предложение", + "loadTabs": "Загрузить {{selected}} из {{total}} вкладок", + "refreshStashTabs": "Обновить вкладки сташи", + "unselectStashTabs": "Отменить выбор вкладок сташи", + "bulkOfferingsPage": "Массовые предложения", + "bulkListingsPage": "Массовые списки", + "showUnreadNotificationsOnly": "Показать только непрочитанные", + "clearAllNotifications": "Очистить все", + "markAllNotifications": "Отметить все как прочитанные", + "copyWhisper": "Скопировать шепот", + "copyWhisperAgain": "Скопировать снова?", + "whisperCopied": "Шепот скопирован!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "Вы абсолютно уверены?", + "myOfferings": "Мои предложения", + "bulkListing": "Массовое размещение", + "actions": "Действия", + "notifications": "Уведомления", + "notificationSettings": "Настройки уведомлений", + "itemFilter": "Фильтр предметов" + }, + "label": { + "searchPh": "Поиск ...", + "selectCategoryPh": "Выберите категорию", + "selectSubCategoryPh": "Выберите подкатегорию", + "selectLeaguePh": "Выберите лигу", + "selectPh": "Выбрать ...", + "noResults": "Нет результатов.", + "noItemResults": "Предметы не найдены.", + "noOfferingResults": "Предложения не найдены.", + "noNotificationResults": "У вас нет уведомлений 🎉", + "otherCalculations": "ДРУГИЕ РАСЧЕТЫ", + "wholeListingShort": "Вся оферта", + "individualListingShort": "Отдельные предметы", + "wholeOfferingShort": "Вся оферта", + "individualOfferingShort": "Отдельные предметы", + "wholeOffering": "Продать всю оферту", + "individualOffering": "Продать отдельные предметы", + "individualOfferingTT": "Вся оферта: Продать всю оферту сразу", + "wholeOfferingTT": "Отдельные предметы: Продать отдельные предметы по цене", + "multiplier": "Множитель: {{multiplier}}%", + "multiplierRange": "Множитель: {{multiplierFrom}}% - {{multiplierTo}}%", + "total": "Всего:", + "totalPrice": "Общая цена:", + "columnSettings": "Настройки столбцов", + "hardReset": "Жесткий сброс", + "columnSettingsResetTT": "Сбросить настройки", + "discordTT": "Дискорд", + "account": "Мой аккаунт", + "league": "Лига", + "loading": "Загрузка ...", + "loadingItems": "Загрузка предметов ...", + "offeringPreviewTT": "Здесь выбираются только вкладки, категория, подкатегория и режим продажи.\n\nВсе остальные настройки берутся из ваших последних изменений:", + "offeringPreviewLi1TT": "Множитель для (под-)категории", + "offeringPreviewLi2TT": "Замещения/Переоценка", + "offeringPreviewLi3TT": "Невыбранные предметы", + "notLoadedTT": "Не загружено", + "updatedTT": "Обновлено", + "maxNotification": "Максимальное количество отображаемых уведомлений:", + "tradeInNewWindow": "Уведомления о запросах на торги появляются, когда кто-то нажимает кнопку «Скопировать шепот» для одного из ваших предложений.", + "showDetailsTT": "Показать детали", + "rowsSelectedOf": "{{current}} из {{total}} строк(и) выбрано.", + "pageOf": "Страница {{current}} из {{total}}", + "rowsPerPage": "Строк на странице", + "firstPage": "Перейти на первую страницу", + "prevPage": "Перейти на предыдущую страницу", + "nextPage": "Перейти на следующую страницу", + "lastPage": "Перейти на последнюю страницу", + "minPh": "МИН", + "multiplierLabel": "Множитель:", + "askingPrice": "Цена запроса:", + "sellMode": "Режим продажи:", + "seller": "Продавец:", + "category": "Категория:", + "language": "Язык" + }, + "body": { + "fallbackRenderInfo": "Если вы подтвердите, все данные будут сброшены. Чтобы устранить ошибку, помогите ее сообщить. Спасибо!", + "hardReset": "Если вы подтвердите, все данные будут сброшены. Это может помочь с багами. Чтобы устранить ошибку, помогите ее сообщить. Спасибо!", + "characterInfo": "Уровень {{level}} {{class}}", + "wtbAll": "куплю все {{category}}", + "wtbPartial": "куплю {{count}} {{category}}", + "softReset": "Это действие нельзя отменить.\n\n Если вы подтвердите, следующие данные будут сброшены:", + "softResetLi1": "Множитель для (под-)категории", + "softResetLi2": "Режим продажи для (под-)категории", + "softResetLi3": "Замещения/Переоценка", + "softResetLi4": "Невыбранные предметы", + "maxNotification": "Уведомления ставятся в очередь для отображения, когда достигнуто максимальное количество.", + "tradeInNewWindow": "Уведомления о запросах на обмен появляются, когда кто-то нажимает «Скопировать шепот» для одного из ваших предложений." + }, + "option": { + "AND": "И", + "NOT": "НЕ", + "COUNT": "ПОДСЧЕТ", + "showAll": "Показать все", + "showSelected": "Показать выбранные", + "showUnselected": "Показать невыбранные", + "showAllModes": "Показать все режимы", + "showWholeListings": "Показать все списки", + "showIndividualListings": "Показать отдельные списки" + }, + "columnTitle": { + "selection": "Выбор", + "2_day_history": "Цена за последние 2 дня", + "7_day_history": "Цена за последние 7 дней", + "multiplier": "Множитель", + "name": "Имя", + "selectedPrice": "Переопределение", + "props": "Свойства", + "quantity": "Количество", + "selectedQuantity": "Запрашиваемое количество", + "tabs": "Вкладка", + "tag": "Тег", + "price": "Цена", + "totalPrice": "Общая цена", + "commulativePrice": "Кумулятивный", + "category": "Категория", + "listingMode": "Режим продажи", + "seller": "Продавец", + "created": "Создано", + "actions": "Действия" + }, + "categories": { + "compass": "Компасы", + "essence": "Эссенции", + "essence low": "Низкие Эссенции", + "essence high": "Высокие Эссенции", + "heist": "Грабеж", + "contract": "Контракты", + "blueprint": "Чертежи", + "currency": "Валюта", + "stackedDeck": "Накладные колоды", + "lifeforce": "Жизненная сила", + "otherCurrency": "Другая валюта", + "beast": "Звери", + "delve": "Погружение", + "fossil": "Ископаемые", + "resonator": "Резонаторы", + "catalyst": "Катализаторы", + "sanctum": "Святилище", + "uniqueRelic": "Уникальные реликвии", + "forbiddenTome": "Запретный том", + "fragment": "Фрагменты", + "scarab": "Скарабеи", + "breach": "Пролом", + "legion": "Легион", + "invitation": "Приглашение", + "mortalSet": "Смертельный набор", + "sacrificeSet": "Набор жертвоприношений", + "shaperSet": "Набор формовщика", + "elderSet": "Набор старейшин", + "conquererSet": "Набор завоевателя", + "uberElderSet": "Набор Ультрастарейшин", + "simulacrum": "Симулякр", + "otherFragments": "Другие фрагменты", + "card": "Карты", + "delirium orb": "Сферы бреда", + "expedition": "Экспедиция", + "logbook": "Журналы", + "artifact": "Артефакты", + "oil": "Масла", + "memory": "Память", + "incubator": "Инкубаторы", + "map": "Карты", + "shaperMap": "Карты формовщика", + "elderMap": "Карты старейшин", + "conquerorMap": "Карты завоевателя" + }, + "relativeTime": { + "future": "через %s", + "past": "%s назад", + "s": "несколько секунд", + "m": "минута", + "mm": "%d минут", + "h": "час", + "hh": "%d часов", + "d": "день", + "dd": "%d дней", + "M": "месяц", + "MM": "%d месяцев", + "y": "год", + "yy": "%d лет" + } +} diff --git a/src/green-app/src/locales/ru/notification.json b/src/green-app/src/locales/ru/notification.json new file mode 100644 index 00000000..5c7862fa --- /dev/null +++ b/src/green-app/src/locales/ru/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "Что-то пошло не так: {{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "Будьте осторожны, {{ign}} хочет купить предметы, которые не предлагаются ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/locales/zh/common.json b/src/green-app/src/locales/zh/common.json new file mode 100644 index 00000000..09493ccb --- /dev/null +++ b/src/green-app/src/locales/zh/common.json @@ -0,0 +1,200 @@ +{ + "action": { + "save": "保存", + "cancel": "取消", + "close": "关闭", + "discard": "放弃", + "addGroup": "添加分组", + "view": "查看", + "hardReset": "硬重置数据", + "softReset": "软重置数据", + "logout": "登出", + "login": "连接 PoE 帐户 (SSO)", + "linkDiscord": "链接 Discord", + "poeTradeDiscord": "PoE 交易 Discord", + "postOffering": "发布供应", + "loadTabs": "加载{{selected}}/{{total}}选项卡", + "refreshStashTabs": "刷新货栈选项卡", + "unselectStashTabs": "取消选择货栈选项卡", + "bulkOfferingsPage": "批量供应", + "bulkListingsPage": "批量列表", + "showUnreadNotificationsOnly": "仅显示未读", + "clearAllNotifications": "清除所有", + "markAllNotifications": "全部标记为已读", + "copyWhisper": "复制密语", + "copyWhisperAgain": "再次复制?", + "whisperCopied": "密语已复制!" + }, + "title": { + "appTitle": "PoeStack", + "alertDialogQuesting": "您确定吗?", + "myOfferings": "我的供应", + "bulkListing": "批量列表", + "actions": "操作", + "notifications": "通知", + "notificationSettings": "通知设置", + "itemFilter": "物品过滤器" + }, + "label": { + "searchPh": "搜索...", + "selectCategoryPh": "选择类别", + "selectSubCategoryPh": "选择子类别", + "selectLeaguePh": "选择联赛", + "selectPh": "选择...", + "noResults": "无结果。", + "noItemResults": "未找到物品。", + "noOfferingResults": "未找到供应。", + "noNotificationResults": "您没有通知 🎉", + "otherCalculations": "其他计算", + "wholeListingShort": "整体供应", + "individualListingShort": "单独物品", + "wholeOfferingShort": "整体供应", + "individualOfferingShort": "单独物品", + "wholeOffering": "出售整个供应", + "individualOffering": "出售单独物品", + "individualOfferingTT": "整体供应:一次性出售整个供应", + "wholeOfferingTT": "单独:出售单个定价物品", + "multiplier": "倍增器:{{multiplier}}%", + "multiplierRange": "倍增器:{{multiplierFrom}}% - {{multiplierTo}}%", + "total": "总计:", + "totalPrice": "总价格:", + "columnSettings": "列设置", + "hardReset": "硬重置", + "columnSettingsResetTT": "重置设置", + "discordTT": "Discord", + "account": "我的帐户", + "league": "联赛", + "loading": "加载中...", + "loadingItems": "加载物品...", + "offeringPreviewTT": "这仅选择存货选项卡、类别、子类别和销售模式。\n\n所有其他设置均取自您的最新更改:", + "offeringPreviewLi1TT": "每个 (子-) 类别的倍增器", + "offeringPreviewLi2TT": "覆盖/高价", + "offeringPreviewLi3TT": "未选择的物品", + "notLoadedTT": "未加载", + "updatedTT": "已更新", + "maxNotification": "最多显示通知数:", + "tradeInNewWindow": "在新窗口中打开交易请求:", + "showDetailsTT": "显示详情", + "rowsSelectedOf": "已选择{{total}}行中的{{current}}行。", + "pageOf": "{{current}}页,共{{total}}页", + "rowsPerPage": "每页行数", + "firstPage": "转到第一页", + "prevPage": "转到上一页", + "nextPage": "转到下一页", + "lastPage": "转到最后一页", + "minPh": "最小", + "multiplierLabel": "倍增器:", + "askingPrice": "要价:", + "sellMode": "销售模式:", + "seller": "卖家:", + "category": "类别:", + "language": "语言" + }, + "body": { + "fallbackRenderInfo": "如果您确认,所有数据将被重置。要修复错误,报告错误会很有帮助。谢谢!", + "hardReset": "如果您确认,所有数据将被重置。这可以帮助解决错误。要修复错误,报告错误会很有帮助。谢谢!", + "characterInfo": "等级{{level}} {{class}}", + "wtbAll": " wtb 所有{{category}}", + "wtbPartial": " wtb {{count}} {{category}}", + "softReset": "此操作无法撤消。\n\n如果您确认,将重置以下数据:", + "softResetLi1": "每个 (子-) 类别的倍增器", + "softResetLi2": "每个 (子-) 类别的销售模式", + "softResetLi3": "覆盖/高价", + "softResetLi4": "未选择的物品", + "maxNotification": "通知排队等待,直到达到最大数量。", + "tradeInNewWindow": "当有人点击您的供应中的“复制密语”时,交易请求通知将出现。" + }, + "option": { + "AND": "与", + "NOT": "非", + "COUNT": "计数", + "showAll": "显示全部", + "showSelected": "显示已选择", + "showUnselected": "显示未选择", + "showAllModes": "显示所有模式", + "showWholeListings": "显示全部供应", + "showIndividualListings": "显示单独供应" + }, + "columnTitle": { + "selection": "选择", + "2_day_history": "过去2天价格", + "7_day_history": "过去7天价格", + "multiplier": "倍增器", + "name": "名称", + "selectedPrice": "覆盖", + "props": "属性", + "quantity": "数量", + "selectedQuantity": "要求数量", + "tabs": "选项卡", + "tag": "标签", + "price": "价格", + "totalPrice": "总价", + "commulativePrice": "累积", + "category": "类别", + "listingMode": "销售模式", + "seller": "卖家", + "created": "创建时间", + "actions": "操作" + }, + "categories": { + "compass": "指南针", + "essence": "精华", + "essence low": "低级精华", + "essence high": "高级精华", + "heist": "劫掠", + "contract": "合同", + "blueprint": "蓝图", + "currency": "货币", + "stackedDeck": "堆叠卡组", + "lifeforce": "生命力", + "otherCurrency": "其他货币", + "beast": "野兽", + "delve": "深度", + "fossil": "化石", + "resonator": "共鸣器", + "catalyst": "催化剂", + "sanctum": "圣所", + "uniqueRelic": "独特文物", + "forbiddenTome": "禁忌之书", + "fragment": "碎片", + "scarab": "圣甲虫", + "breach": "裂隙", + "legion": "军团", + "invitation": "邀请", + "mortalSet": "死亡套装", + "sacrificeSet": "牺牲套装", + "shaperSet": "塑形者套装", + "elderSet": "长老套装", + "conquererSet": "征服者套装", + "uberElderSet": "超级长老套装", + "simulacrum": "模拟", + "otherFragments": "其他碎片", + "card": "卡牌", + "delirium orb": "疯狂法球", + "expedition": "远征", + "logbook": "日志", + "artifact": "神器", + "oil": "油", + "memory": "记忆", + "incubator": "孵化器", + "map": "地图", + "shaperMap": "塑形者地图", + "elderMap": "长老地图", + "conquerorMap": "征服者地图" + }, + "relativeTime": { + "future": "在%s", + "past": "%s前", + "s": "几秒钟", + "m": "一分钟", + "mm": "%d分钟", + "h": "一小时", + "hh": "%d小时", + "d": "一天", + "dd": "%d天", + "M": "一个月", + "MM": "%d个月", + "y": "一年", + "yy": "%d年" + } +} diff --git a/src/green-app/src/locales/zh/notification.json b/src/green-app/src/locales/zh/notification.json new file mode 100644 index 00000000..d4bbcd60 --- /dev/null +++ b/src/green-app/src/locales/zh/notification.json @@ -0,0 +1,21 @@ +{ + "success": { + "title": {}, + "description": {} + }, + "error": { + "unknown_error": "发生了一些问题:{{message}}", + "title": {}, + "description": {} + }, + "warning": { + "title": {}, + "description": { + "tradeRequestNotValid": "小心,{{ign}} 想购买未提供的物品 ..." + } + }, + "info": { + "title": {}, + "description": {} + } +} diff --git a/src/green-app/src/middleware.ts b/src/green-app/src/middleware.ts new file mode 100644 index 00000000..9b217f7b --- /dev/null +++ b/src/green-app/src/middleware.ts @@ -0,0 +1,12 @@ +import { i18nRouter } from 'next-i18n-router' +import { i18nConfig } from './config/i18n.config' +import { NextRequest } from 'next/server' + +export function middleware(request: NextRequest) { + return i18nRouter(request, i18nConfig) +} + +// only applies this middleware to files in the app directory +export const config = { + matcher: '/((?!api|static|.*\\..*|_next).*)' +} diff --git a/src/green-app/src/store/notificationStore.ts b/src/green-app/src/store/notificationStore.ts new file mode 100644 index 00000000..972e3130 --- /dev/null +++ b/src/green-app/src/store/notificationStore.ts @@ -0,0 +1,221 @@ +import { ToastData } from '@/components/notifier' +import dayjs from 'dayjs' +import { produce } from 'immer' +import { ReactNode } from 'react' +import { Id, ToastOptions as ToastifyToastOptions, TypeOptions, toast } from 'react-toastify' +import { v4 as uuidV4 } from 'uuid' +import { create } from 'zustand' +import { createJSONStorage, persist } from 'zustand/middleware' + +type ToastOptions = Omit & { + data?: {} | ToastData +} + +type AddNotificationOptions = ToastOptions & { + display?: boolean + read?: boolean + createdAt?: number +} + +export type Notification = { + id: Id + content: ReactNode + type: TypeOptions + options?: ToastOptions + read: boolean + createdAt: number +} + +type State = { + notifications: Notification[] + activeToasts: Id[] + toastsToDisplay: Id[] + displayBlocked: boolean + maxActiveToasts: number + openTradeOverviewInNewWindow: boolean +} + +type Actions = { + addNotification: (content: ReactNode, type: TypeOptions, options?: AddNotificationOptions) => Id + composedonOpen: (id: Id, props: T, notification: Notification) => void + composedonClose: (id: Id, props: T, notification: Notification) => void + handleToastOpen: (id: Id) => void + handleToastClose: (id: Id) => void + clear: () => void + markAllAsRead: () => void + markAsRead: (id: Id | Id[]) => void + remove: (id: Id | Id[]) => void + dismissAll: (blockDisplay?: boolean) => void + blockDisplay: (blockDisplay: boolean) => void + setMaxActiveToasts: (maxActiveToasts: number) => void + setOpenTradeOverviewInNewWindow: (newWindow: boolean) => void +} + +type NotificationState = State & Actions + +const initialState: State = { + notifications: [], + activeToasts: [], + toastsToDisplay: [], + displayBlocked: false, + maxActiveToasts: 4, + openTradeOverviewInNewWindow: true +} + +export const useNotificationStore = create()( + persist( + (set, get) => ({ + ...initialState, + addNotification: (content, type, notificationOptions) => { + const { toastId, onOpen, onClose, display, read, createdAt, ...options } = + notificationOptions || {} + + const id = toastId ?? uuidV4() + set((state) => { + let toastsToDisplay = state.toastsToDisplay + const notification: Notification = { + id: id, + type: type, + content: content, + options: notificationOptions, + read: read ?? false, + createdAt: createdAt ?? dayjs.utc().valueOf() + } + if ((display ?? true) && !state.displayBlocked) { + if (state.activeToasts.length < state.maxActiveToasts) { + // Fill active toasts + toast(content, { + type: type, + toastId: id, + onOpen: (props) => get().composedonOpen(id, props, notification), + onClose: (props) => get().composedonClose(id, props, notification), + ...options + }) + } else { + // Fill queue + toastsToDisplay = [...toastsToDisplay, id] + } + } + return { notifications: [notification, ...state.notifications], toastsToDisplay } + }) + return id + }, + composedonOpen: (id, props, notification) => { + get().handleToastOpen(id) + notification?.options?.onOpen?.(props) + }, + composedonClose: (id, props, notification) => { + get().handleToastClose(id) + notification?.options?.onClose?.(props) + }, + handleToastOpen: (id) => + set((state) => { + if (state.activeToasts.includes(id)) { + return {} + } + return { activeToasts: [...state.activeToasts, id] } + }), + handleToastClose: (id) => + set((state) => { + const activeToasts = [...state.activeToasts] + let toastsToDisplay = state.toastsToDisplay + const idx = activeToasts.indexOf(id) + if (idx !== -1) { + activeToasts.splice(idx, 1) + } + if (activeToasts.length < state.maxActiveToasts && toastsToDisplay.length > 0) { + // Show next toasts + toastsToDisplay = toastsToDisplay.slice() + const nextId = toastsToDisplay.shift() + const nextN = state.notifications.find((n) => n.id === nextId) + if (nextN) { + if (!activeToasts.includes(nextN.id)) { + // Ensure toasts are not shown in between event and this set function + activeToasts.push(nextN.id) + } + const { toastId, onOpen, onClose, ...options } = nextN.options || {} + toast(nextN.content, { + type: nextN.type, + toastId: nextN.id, + onOpen: (props) => get().composedonOpen(nextN.id, props, nextN), + onClose: (props) => get().composedonClose(nextN.id, props, nextN), + ...options + }) + } + } + return { activeToasts, toastsToDisplay } + }), + dismissAll: (blockDisplay) => { + set( + produce((state: NotificationState) => { + state.toastsToDisplay = [] + state.activeToasts.forEach((id) => toast.dismiss(id)) + if (blockDisplay !== undefined) { + state.displayBlocked = blockDisplay + console.log('dismissAll: Set blockDisplay: ', blockDisplay) + } + }) + ) + }, + blockDisplay: (blockDisplay) => + set( + produce((state: NotificationState) => { + state.displayBlocked = blockDisplay + console.log('blockDisplay: Set blockDisplay: ', blockDisplay) + }) + ), + clear: () => + set( + produce((state: NotificationState) => { + state.notifications = [] + }) + ), + markAllAsRead: () => + set( + produce((state: NotificationState) => { + state.notifications.forEach((n) => (n.read = true)) + }) + ), + markAsRead: (ids) => + set( + produce((state: NotificationState) => { + if (Array.isArray(ids)) { + ids.forEach((id) => { + const n = state.notifications.find((n) => n.id === id) + if (n) n.read = true + }) + } else { + const n = state.notifications.find((n) => n.id === ids) + if (n) n.read = true + } + }) + ), + remove: (ids) => + set( + produce((state: NotificationState) => { + if (Array.isArray(ids)) { + ids.forEach((id) => { + const idx = state.notifications.findIndex((n) => n.id === id) + if (idx > -1) state.notifications.splice(idx, 1) + }) + } else { + const idx = state.notifications.findIndex((n) => n.id === ids) + if (idx > -1) state.notifications.splice(idx, 1) + } + }) + ), + setMaxActiveToasts: (maxActiveToasts: number) => set({ maxActiveToasts }), + setOpenTradeOverviewInNewWindow: (newWindow: boolean) => + set({ openTradeOverviewInNewWindow: newWindow }) + }), + { + name: 'notification-storage', + storage: createJSONStorage(() => localStorage), + partialize: (state) => ({ + maxActiveToasts: state.maxActiveToasts, + openTradeOverviewInNewWindow: state.openTradeOverviewInNewWindow + }), + version: 1 + } + ) +) diff --git a/src/green-app/src/types/echo-api/item-group.ts b/src/green-app/src/types/echo-api/item-group.ts index e75302d9..b496ffe6 100644 --- a/src/green-app/src/types/echo-api/item-group.ts +++ b/src/green-app/src/types/echo-api/item-group.ts @@ -13,7 +13,7 @@ export type SageItemGroupSummary = { } export type SageItemGroupSummaryShard = { - meta: { tag?: string } + meta: { tag: string } summaries: { [itemGroupHashString: string]: SageItemGroupSummary } } @@ -25,6 +25,6 @@ export type SageItemGroupSummaryInternal = { } export type SageItemGroupSummaryShardInternal = { - meta: { tag?: string } + meta: { tag: string } summaries: { [itemGroupHashString: string]: SageItemGroupSummaryInternal } } diff --git a/src/green-app/src/types/echo-api/priced-item.ts b/src/green-app/src/types/echo-api/priced-item.ts index bce52ccb..aac5217a 100644 --- a/src/green-app/src/types/echo-api/priced-item.ts +++ b/src/green-app/src/types/echo-api/priced-item.ts @@ -1,6 +1,6 @@ // Ref networth-plugin -import { SageItemGroup } from '@/lib/item-grouping-service' +import { SageItemGroup } from 'sage-common' import { SageValuation } from './valuation' import { PoeItem } from '../poe-api-models' import { ICompactTab } from './stash' @@ -20,7 +20,7 @@ export interface IDisplayedItem extends IPricedItem { displayName: string originalPrice?: number calculatedPrice?: number - calculatedTotal: number + calculatedTotalPrice: number stackSize: number totalStacksize: number icon: string diff --git a/src/green-app/src/types/i18next.d.ts b/src/green-app/src/types/i18next.d.ts new file mode 100644 index 00000000..3997c93e --- /dev/null +++ b/src/green-app/src/types/i18next.d.ts @@ -0,0 +1,16 @@ +import { resources } from './resouces' +import { defaultNS } from '../config/i18n.config' + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: typeof defaultNS + resources: typeof resources + } +} + +declare module 'react-i18next' { + interface CustomTypeOptions { + defaultNS: typeof defaultNS + resources: typeof resources + } +} diff --git a/src/green-app/src/types/item.ts b/src/green-app/src/types/item.ts index 664ea5c8..f8b20735 100644 --- a/src/green-app/src/types/item.ts +++ b/src/green-app/src/types/item.ts @@ -1,4 +1,4 @@ -import { SageItemGroup } from '@/lib/item-grouping-service' +import { SageItemGroup } from 'sage-common' import { IStashTab } from './echo-api/stash' import { PoeItem } from './poe-api-models' import { SageValuation } from './echo-api/valuation' diff --git a/src/green-app/src/types/react-table.d.ts b/src/green-app/src/types/react-table.d.ts index 647c95cc..fe135a24 100644 --- a/src/green-app/src/types/react-table.d.ts +++ b/src/green-app/src/types/react-table.d.ts @@ -9,6 +9,6 @@ declare module '@tanstack/table-core' { removePadding?: boolean } interface TableMeta { - updateData: (rowIndex: number, columnId: string, value: number | string) => void + updateData?: (rowIndex: number, columnId: string, value: number | string) => void } } diff --git a/src/green-app/src/types/resouces.ts b/src/green-app/src/types/resouces.ts new file mode 100644 index 00000000..9d3be3f7 --- /dev/null +++ b/src/green-app/src/types/resouces.ts @@ -0,0 +1,7 @@ +import common from '../locales/en/common.json' +import notification from '../locales/en/notification.json' + +export const resources = { + common, + notification +} as const diff --git a/src/green-app/src/types/sage-listing-type.ts b/src/green-app/src/types/sage-listing-type.ts index 9554d852..de855a9a 100644 --- a/src/green-app/src/types/sage-listing-type.ts +++ b/src/green-app/src/types/sage-listing-type.ts @@ -9,6 +9,13 @@ export type SageDatabaseOfferingItemType = { price: number } +export type SageSelectedDatabaseOfferingItemType = { + hash: string + quantity: number + price: number + selectedQuantity?: number +} + export type SageDatabaseOfferingType = { uuid: string // Unique identifier across relistings userId: string @@ -25,14 +32,8 @@ export type SageDatabaseOfferingType = { items: SageDatabaseOfferingItemType[] } -export type SageOfferingType = Omit & { +export type SageOfferingType = SageDatabaseOfferingType & { meta: { - league: string - category: string - ign: string - timestampMs: number - listingMode: ListingMode - tabs: string[] totalPrice: number } } diff --git a/src/sage-common/src/item-groups/item-grouping-service.ts b/src/sage-common/src/item-groups/item-grouping-service.ts index 6afc6d04..88f335b0 100644 --- a/src/sage-common/src/item-groups/item-grouping-service.ts +++ b/src/sage-common/src/item-groups/item-grouping-service.ts @@ -1,7 +1,7 @@ import objectHash from 'object-hash' import { from, map } from 'rxjs' -import { PoeItem } from '../ggg/poe-api-models'; -import { ItemUtils } from './item-utils'; +import { PoeItem } from '../ggg/poe-api-models' +import { ItemUtils } from './item-utils' export type SageItemGroup = { key: string; tag: string; hash: string; unsafeHashProperties: any } @@ -19,7 +19,7 @@ export class ItemGroupingService { new TimelessJewelGroupIdentifier(), new TattooGroupIdentifier(), new BloodFilledVesselGroupIdentifier(), - new UnqiueGearGroupIdentifier(), + new UnqiueGearGroupIdentifier(), // TODO: Identify unique maps new CorpseGroupIdentifier(), new TomeGroupIdentifier(), new BeastGroupIdentifier(), @@ -102,8 +102,8 @@ export class InscribedUltimatumGroupIndentifier implements ItemGroupIdentifier { ?.values?.[0]?.[0] const reward = item.properties?.filter((prop) => prop.name?.startsWith('Reward'))?.[0] ?.values?.[0]?.[0] - const sacrifice = item.properties?.filter((prop) => - prop.name?.startsWith('Requires Sacrifice') + const sacrifice = item.properties?.filter( + (prop) => prop.name?.startsWith('Requires Sacrifice') )?.[0]?.values?.[0]?.[0] const group: InternalGroup = { @@ -125,6 +125,7 @@ export class RelicGroupIdentifier implements ItemGroupIdentifier { group(item: PoeItem): InternalGroup | null { if (item.typeLine?.endsWith(' Relic') && item.rarity === 'Unique') { const group: InternalGroup = { + // FIXME: There is one relic without a item.name key: item.name!!.toLowerCase(), tag: 'relic', hashProperties: {} @@ -226,6 +227,7 @@ export class TomeGroupIdentifier implements ItemGroupIdentifier { key: 'forbidden tome', tag: 'forbidden tome', hashProperties: { + // TODO: Define level range < 83 & >= 83 ilvl: ilvl!! } } @@ -323,67 +325,67 @@ export class UnqiueGearGroupIdentifier implements ItemGroupIdentifier { } export class BeastGroupIdentifier implements ItemGroupIdentifier { - private sellableRedBeasts = [ - "craicic chimeral", - "vivid vulture", - "wild hellion alpha", - "vivid abberarach", - "farric lynx alpha", - "farric wolf alpha", - "craicic savage crab", - "saqawine cobra", - "primal rhex matriarch", - "vivid watcher", - "wild bristle matron", - "fenumal plagued arachnid", - "wild brambleback", - "craicic maw ", - "farric frost hellion alpha", - "farric tiger alpha", + 'craicic chimeral', + 'vivid vulture', + 'wild hellion alpha', + 'vivid abberarach', + 'farric lynx alpha', + 'farric wolf alpha', + 'craicic savage crab', + 'saqawine cobra', + 'primal rhex matriarch', + 'vivid watcher', + 'wild bristle matron', + 'fenumal plagued arachnid', + 'wild brambleback', + 'craicic maw ', + 'farric frost hellion alpha', + 'farric tiger alpha' ] private beastMods = [ - "fertile presence", - "aspect of the hellion", - "farric presence", - "satyr storm", - "tiger prey", - "spectral swipe", + 'fertile presence', + 'aspect of the hellion', + 'farric presence', + 'satyr storm', + 'tiger prey', + 'spectral swipe', "deep one's presence", - "churning claws", - "winter bloom", - "craicic presence", - "crushing claws", - "hadal dive", - "raven caller", - "putrid flight", - "saqawine presence", - "vile hatchery", - "spectral stampede", - "fenumal presence", - "unstable swarm", - "blood geyser", - "crimson flock", - "incendiary mite", - "infested earth", + 'churning claws', + 'winter bloom', + 'craicic presence', + 'crushing claws', + 'hadal dive', + 'raven caller', + 'putrid flight', + 'saqawine presence', + 'vile hatchery', + 'spectral stampede', + 'fenumal presence', + 'unstable swarm', + 'blood geyser', + 'crimson flock', + 'incendiary mite', + 'infested earth' ] group(item: PoeItem): InternalGroup | null { if (item?.descrText === 'Right-click to add this to your bestiary.') { - const beastModCount = (item.explicitMods ?? []).filter((e) => this.beastMods.includes(e.toLowerCase())).length + const beastModCount = (item.explicitMods ?? []).filter((e) => + this.beastMods.includes(e.toLowerCase()) + ).length - const baseType = item.baseType?.toLocaleLowerCase() ?? "" - if (this.sellableRedBeasts.includes(baseType) || item?.rarity === "Unique") { + const baseType = item.baseType?.toLocaleLowerCase() ?? '' + if (this.sellableRedBeasts.includes(baseType) || item?.rarity === 'Unique') { return { key: baseType!!, tag: 'beast', hashProperties: {} } - } - else { + } else { return { - key: beastModCount === 1 ? "yellow beast" : "red beast", + key: beastModCount === 1 ? 'yellow beast' : 'red beast', tag: 'beast', hashProperties: {} } @@ -486,20 +488,18 @@ export class HeistBlueprintsGroupIdentifier implements ItemGroupIdentifier { if (baseType?.includes('blueprint:')) { const wingsRevealed = item.properties?.filter((p) => p.name === 'Wings Revealed')?.[0] ?.values?.[0]?.[0] - const target = item.properties - ?.filter((p) => p.name === 'Heist Target: {0}')?.[0] - ?.values?.[0]?.[0]?.toLowerCase() const ilvl = item['ilvl'] ?? item.itemLevel const totalWings = wingsRevealed?.split('/')?.[1] const fullyRevealed = wingsRevealed?.split('/')?.[0] === totalWings - if (totalWings && ilvl && target) { + if (totalWings && ilvl) { return { - key: target + ' blueprint', + key: 'blueprint', tag: 'blueprint', hashProperties: { ilvl: ilvl >= 81 ? '81+' : '<81', totalWings: parseInt(totalWings), - fullyRevealed + fullyRevealed, + unmodifiable: !!item.corrupted || !!item.duplicated } } } @@ -515,7 +515,7 @@ export class HeistContractsGroupIdentifier implements ItemGroupIdentifier { const baseType = item.baseType?.toLowerCase() - if (baseType?.includes('contract:') && !item.corrupted && !item.split) { + if (baseType?.includes('contract:')) { const ilvl = item['ilvl'] ?? item.itemLevel const type = item.properties ?.filter((p) => p.name === 'Requires {1} (Level {0})')?.[0] @@ -525,7 +525,8 @@ export class HeistContractsGroupIdentifier implements ItemGroupIdentifier { key: type + ' contract', tag: 'contract', hashProperties: { - ilvl: ilvl >= 81 ? '83+' : '<83' + ilvl: ilvl >= 81 ? '83+' : '<83', + unmodifiable: !!item.corrupted || !!item.duplicated } } } @@ -560,7 +561,7 @@ export class LogbookGroupIdentifier implements ItemGroupIdentifier { tag: 'logbook', hashProperties: { ilvl: ilvl >= 83 ? '83+' : '<83', - corrupted: !!item.corrupted, + unmodifiable: !!item.corrupted || !!item.duplicated, split: !!item.split } } @@ -1068,6 +1069,7 @@ export class CurrencyGroupIdentifier implements ItemGroupIdentifier { 'timeless vaal splinter', 'timeless eternal empire splinter', 'simulacrum splinter' + // Missing: Valdos Puzzle Box ]) }, invocation: {