diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 1d3d98c..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(timeout:*)", - "Bash(npm run dev:*)", - "Bash(node --version:*)", - "Bash(npm --version)", - "Bash(npx prettier:*)", - "Bash(pnpm lint:*)", - "Bash(pnpm lint:fix:*)", - "Bash(pnpm --filter chrome-extension lint:*)", - "Bash(pnpm --filter @extension/content-script lint:*)", - "Bash(pnpm --filter @extension/ui lint:*)", - "Bash(pnpm --filter @extension/shared lint:*)", - "Bash(pnpm --filter @extension/shared lint:fix:*)", - "Bash(pnpm --filter @extension/sidepanel lint:fix:*)", - "Bash(xargs sed -i \"s|from ''@extension/shared/lib/utils/csv-tracker''|from ''@app/utils/csv-tracker''|g; s|from ''@extension/shared/lib/utils/storage-service''|from ''@app/utils/storage-service''|g; s|from ''@extension/shared/lib/utils/weekly-stats-storage''|from ''@app/utils/weekly-stats-storage''|g; s|from ''@extension/shared/lib/utils/user-stats-storage''|from ''@app/utils/user-stats-storage''|g; s|from ''@extension/shared/lib/utils/types''|from ''@app/types''|g; s|from ''@extension/shared/lib/hooks/useTrackedDataQuery''|from ''@app/hooks/useTrackedDataQuery''|g; s|from ''@extension/shared/lib/hooks/useUserStatsQuery''|from ''@app/hooks/useUserStatsQuery''|g; s|from ''@extension/shared/lib/hooks/useWeeklyStatsQuery''|from ''@app/hooks/useWeeklyStatsQuery''|g; s|from ''@extension/shared/lib/hooks/useFormatting''|from ''@app/hooks/useFormatting''|g; s|from ''@extension/shared/lib/hooks/useHourlyExp''|from ''@app/hooks/useHourlyExp''|g; s|from ''@extension/shared/lib/hooks/useScreenData''|from ''@app/hooks/useScreenData''|g; s|from ''@extension/shared/lib/hooks/useTrackedData''|from ''@app/hooks/useTrackedData''|g; s|from ''@extension/shared/lib/hooks/useUserStats''|from ''@app/hooks/useUserStats''|g; s|from ''@extension/shared/lib/hooks/useHourStats''|from ''@app/hooks/useHourStats''|g; s|from ''@extension/shared/lib/hooks/usePeriodStats''|from ''@app/hooks/usePeriodStats''|g; s|from ''@extension/shared/lib/hooks/useDataExport''|from ''@app/hooks/useDataExport''|g; s|from ''@extension/shared/lib/hooks/useItemValuesQuery''|from ''@app/hooks/useItemValuesQuery''|g; s|import\\(''@extension/shared/lib/utils/weekly-stats-storage''\\)|import\\(''@app/utils/weekly-stats-storage''\\)|g\")", - "Bash(xargs sed:*)", - "Bash(pnpm install:*)", - "Bash(pnpm build:*)", - "Bash(pnpm add:*)", - "Bash(pnpm format:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(npx eslint:*)", - "Bash(git push:*)", - "Bash(pnpm type-check:*)", - "Bash(pnpm dev)", - "Bash(npx vite:*)", - "Bash(taskkill:*)", - "Bash(ls:*)", - "Bash(python3 -c \" import sys, json for line in sys.stdin: try: obj = json.loads\\(line.strip\\(\\)\\) if obj.get\\(''type''\\) == ''human'': msg = obj.get\\(''message'', {}\\) if isinstance\\(msg, dict\\): content = msg.get\\(''content'', ''''\\) if isinstance\\(content, str\\): print\\(''HUMAN:'', content[:300]\\) elif isinstance\\(content, list\\): for c in content: if isinstance\\(c, dict\\) and c.get\\(''type''\\) == ''text'': print\\(''HUMAN:'', c.get\\(''text'', ''''\\)[:300]\\) elif obj.get\\(''type''\\) == ''assistant'': msg = obj.get\\(''message'', {}\\) if isinstance\\(msg, dict\\): content = msg.get\\(''content'', ''''\\) if isinstance\\(content, list\\): for c in content: if isinstance\\(c, dict\\) and c.get\\(''type''\\) == ''text'': print\\(''ASSISTANT:'', c.get\\(''text'', ''''\\)[:200]\\) except: pass \")", - "Bash(node -e:*)", - "WebFetch(domain:ui.shadcn.com)", - "WebFetch(domain:raw.githubusercontent.com)", - "Bash(cd:*)", - "Bash(node:*)", - "Bash(pnpm remove:*)", - "Bash(pnpm test:*)", - "Bash(python -m json.tool:*)", - "Bash(git ls-tree:*)", - "Bash(echo:*)", - "Bash(awk:*)" - ] - } -} diff --git a/.example.env b/.example.env deleted file mode 100644 index 59aaabd..0000000 --- a/.example.env +++ /dev/null @@ -1,3 +0,0 @@ -CEB_EXAMPLE=example_env -CEB_DEV_LOCALE= -CEB_CI= diff --git a/.gitignore b/.gitignore index 08afbac..0e5573c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,12 @@ tsconfig.tsbuildinfo # etc .DS_Store .idea +.claude/settings.local.json +.turbo/ + +# extension artifacts +*.crx +*.pem # compiled **/tailwind-output.css diff --git a/.turbo/preferences/tui.json b/.turbo/preferences/tui.json deleted file mode 100644 index 15055d5..0000000 --- a/.turbo/preferences/tui.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "is_task_list_visible": true, - "active_task": null -} diff --git a/app/panel/SidePanel.tsx b/app/panel/SidePanel.tsx index 9909c2d..b0de95e 100644 --- a/app/panel/SidePanel.tsx +++ b/app/panel/SidePanel.tsx @@ -8,9 +8,13 @@ const SidePanelContent = () => { const { display, setDisplay, ActiveScreen } = useSidePanel(); return ( -
-
- +
+
+
+
+
+ +
); diff --git a/app/panel/components/Calendar/CalendarCell.tsx b/app/panel/components/Calendar/CalendarCell.tsx index 6b696b3..204ccd5 100644 --- a/app/panel/components/Calendar/CalendarCell.tsx +++ b/app/panel/components/Calendar/CalendarCell.tsx @@ -8,6 +8,7 @@ interface CalendarCellProps { data: CalendarCellData; isToday?: boolean; isMuted?: boolean; + compact?: boolean; onClick?: () => void; } @@ -26,49 +27,97 @@ const getIntensityClass = (totalExp: number): string => { return "bg-primary/25"; }; -const CalendarCell = memo(({ label, data, isToday = false, isMuted = false, onClick }: CalendarCellProps) => { - const hasData = data.hasData; +const MAX_VISIBLE_SKILLS = 4; - return ( - - ); -}); + {/* ── Cost ── */} + {data.foodUsed > 0 && ( + <> +
+ + {data.totalDamageReceived.toLocaleString()} HP · {formatGP(data.foodUsed)} GP + + + )} +
+ )} + + ); + }, +); CalendarCell.displayName = "CalendarCell"; diff --git a/app/panel/components/Calendar/views/DayView.tsx b/app/panel/components/Calendar/views/DayView.tsx index ce93e32..6b75003 100644 --- a/app/panel/components/Calendar/views/DayView.tsx +++ b/app/panel/components/Calendar/views/DayView.tsx @@ -69,7 +69,7 @@ const HourRow = memo(({ hour, data, mainSkill, trendPct }: HourRowProps) => {
- {formatExp(data.totalExp)} xp + {formatExp(data.totalExp)} exp {trendPct !== undefined && ( <> | @@ -133,17 +133,38 @@ const HourRow = memo(({ hour, data, mainSkill, trendPct }: HourRowProps) => { )} {hasStats && ( -
- {data.totalDropValue > 0 && Total: {formatGP(data.totalDropValue)} GP} +
+ {data.totalDropValue > 0 && ( + + Total Value: {formatGP(data.totalDropValue)} GP + + )} + {data.foodUsed > 0 && ( + + Food Cost: {formatGP(data.foodUsed)} GP + + )} {data.netProfit !== 0 && ( - 0 ? "text-primary" : "text-destructive")}> - Net: {data.netProfit > 0 ? "+" : ""} + 0 ? "bg-green-500/15 text-green-600" : "bg-red-500/15 text-red-600", + )}> + {data.netProfit > 0 ? "+" : ""} {formatGP(data.netProfit)} GP )} - {data.foodUsed > 0 && Food: {formatGP(data.foodUsed)} GP} - {data.totalFights > 0 && Fights: {data.totalFights.toLocaleString()}} - {data.totalSkillingActions > 0 && Actions: {data.totalSkillingActions.toLocaleString()}} + {data.totalFights > 0 && ( + + Fights: {data.totalFights.toLocaleString()} + + )} + {data.totalSkillingActions > 0 && ( + + Actions:{" "} + {data.totalSkillingActions.toLocaleString()} + + )}
)} diff --git a/app/panel/components/Calendar/views/MonthView.tsx b/app/panel/components/Calendar/views/MonthView.tsx index c00a575..4de0cc9 100644 --- a/app/panel/components/Calendar/views/MonthView.tsx +++ b/app/panel/components/Calendar/views/MonthView.tsx @@ -71,6 +71,7 @@ const MonthView = memo(({ currentDate, getCell, onDrillDown }: MonthViewProps) = data={getCell(key)} isToday={isToday} isMuted={!isCurrentMonth} + compact onClick={() => onDrillDown("day", date)} /> ))} diff --git a/app/panel/components/Calendar/views/WeekView.tsx b/app/panel/components/Calendar/views/WeekView.tsx index bf18285..a6b2d11 100644 --- a/app/panel/components/Calendar/views/WeekView.tsx +++ b/app/panel/components/Calendar/views/WeekView.tsx @@ -34,7 +34,7 @@ const WeekView = memo(({ currentDate, getCell, onDrillDown }: WeekViewProps) => }, [currentDate]); return ( -
+
{days.map(({ date, key, label, isToday }) => ( label={name} data={cellData} isToday={isCurrentMonth} + compact onClick={() => onDrillDown("month", new Date(year, i, 1))} /> ); diff --git a/app/panel/components/Header/index.tsx b/app/panel/components/Header/index.tsx index 2f2d6d7..c71c193 100644 --- a/app/panel/components/Header/index.tsx +++ b/app/panel/components/Header/index.tsx @@ -1,7 +1,6 @@ import DISPLAY from "../../constants/Tabs"; -import { Badge, cn, Popover, PopoverTrigger, PopoverContent } from "@app/components"; +import { Badge, cn } from "@app/components"; import * as RadixIcons from "@radix-ui/react-icons"; -import { useState } from "react"; interface HeaderProps { headerText?: string; @@ -9,178 +8,40 @@ interface HeaderProps { setDisplay: (value: string) => void; } -const tabIcons: Record> = { - [DISPLAY.DASHBOARD]: RadixIcons.HomeIcon, - [DISPLAY.CALENDAR]: RadixIcons.CalendarIcon, - [DISPLAY.PROFILE]: RadixIcons.PersonIcon, - [DISPLAY.STATS]: RadixIcons.BarChartIcon, - [DISPLAY.LOOT]: RadixIcons.CubeIcon, - [DISPLAY.HISTORY]: RadixIcons.ClockIcon, - [DISPLAY.SETTINGS]: RadixIcons.GearIcon, - [DISPLAY.DATA_VIEW]: RadixIcons.TableIcon, -}; - -const Header: React.FC = ({ display, setDisplay }) => { - const [desktopPopoverOpen, setDesktopPopoverOpen] = useState(false); - const [mobilePopoverOpen, setMobilePopoverOpen] = useState(false); - - // Main tabs (excluding settings) - const mainTabs = [ - { key: DISPLAY.DASHBOARD, label: "Dashboard" }, - { key: DISPLAY.CALENDAR, label: "Calendar" }, - { key: DISPLAY.PROFILE, label: "Profile" }, - { key: DISPLAY.STATS, label: "Performance" }, - { key: DISPLAY.LOOT, label: "Loot" }, - { key: DISPLAY.HISTORY, label: "History" }, - ]; - - // Settings dropdown items (only shown in dropdown) - const settingsDropdownItems = [ - { key: DISPLAY.DATA_VIEW, label: "Data View" }, - { key: DISPLAY.SETTINGS, label: "Settings" }, - ]; - - // All tabs including settings for popover menu - const allTabs = [...mainTabs, ...settingsDropdownItems]; - - const handleTabSelect = (tabKey: string, isMobile: boolean = false) => { - setDisplay(tabKey); - if (isMobile) { - setMobilePopoverOpen(false); - } else { - setDesktopPopoverOpen(false); - } - }; - - return ( -
- {/* Tab Navigation - Responsive Badge Row */} - {/* Above 700: normal layout with main tabs and settings dropdown */} -
- {mainTabs.map(tab => { - const isSelected = display === tab.key; - const IconComponent = tabIcons[tab.key] || RadixIcons.DotFilledIcon; - - return ( - setDisplay(tab.key)} - isActive={isSelected} - className="h-10 w-10 p-0 min-[700px]:h-auto min-[700px]:w-auto min-[700px]:px-2.5 min-[700px]:py-0.5" - aria-label={tab.label} - aria-selected={isSelected} - role="tab"> - - ); - })} - - {/* Settings dropdown for desktop */} - - - - - - -
- {settingsDropdownItems.map(item => { - const IconComponent = tabIcons[item.key] || RadixIcons.DotFilledIcon; - const isSelected = display === item.key; - return ( - handleTabSelect(item.key, false)} - isActive={isSelected} - aria-label={item.label} - aria-selected={isSelected} - role="tab"> - - ); - })} -
-
-
-
- - {/* Below 700: centered badges with text, settings dropdown on right */} -
- {/* Centered badges with text */} -
- {mainTabs.map(tab => { - const isSelected = display === tab.key; - const IconComponent = tabIcons[tab.key] || RadixIcons.DotFilledIcon; - - return ( - setDisplay(tab.key)} - isActive={isSelected} - className="px-2.5 py-0.5" - aria-label={tab.label} - aria-selected={isSelected} - role="tab"> - - ); - })} -
- - {/* Settings popover on the right */} - - - - - - -
- {allTabs.map(tab => { - const IconComponent = tabIcons[tab.key] || RadixIcons.DotFilledIcon; - const isSelected = display === tab.key; - return ( - handleTabSelect(tab.key, true)} - isActive={isSelected} - aria-label={tab.label} - aria-selected={isSelected} - role="tab"> - - ); - })} -
-
-
-
+const tabs = [ + { key: DISPLAY.DASHBOARD, label: "Dashboard", Icon: RadixIcons.HomeIcon }, + { key: DISPLAY.CALENDAR, label: "Calendar", Icon: RadixIcons.CalendarIcon }, + { key: DISPLAY.PROFILE, label: "Profile", Icon: RadixIcons.PersonIcon }, + { key: DISPLAY.STATS, label: "Performance", Icon: RadixIcons.BarChartIcon }, + { key: DISPLAY.LOOT, label: "Loot", Icon: RadixIcons.CubeIcon }, + { key: DISPLAY.HISTORY, label: "History", Icon: RadixIcons.ClockIcon }, + { key: DISPLAY.DATA_VIEW, label: "Data", Icon: RadixIcons.TableIcon }, + { key: DISPLAY.SETTINGS, label: "Settings", Icon: RadixIcons.GearIcon }, +]; + +const Header: React.FC = ({ display, setDisplay }) => ( +
+
+ {tabs.map(({ key, label, Icon }) => { + const isSelected = display === key; + return ( + setDisplay(key)} + isActive={isSelected} + className="h-10 w-10 p-0 min-[700px]:h-auto min-[700px]:w-auto min-[700px]:px-2.5 min-[700px]:py-0.5" + aria-label={label} + aria-selected={isSelected} + role="tab"> + + ); + })}
- ); -}; +
+); export default Header; diff --git a/app/panel/components/LootMap/LootTable/index.tsx b/app/panel/components/LootMap/LootTable/index.tsx index 476fa8e..ced2b8d 100644 --- a/app/panel/components/LootMap/LootTable/index.tsx +++ b/app/panel/components/LootMap/LootTable/index.tsx @@ -1,5 +1,5 @@ import { formatTimestamp } from "../helpers/lootHelpers"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, ItemImage } from "@app/components"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, ItemImage, Badge, cn } from "@app/components"; import { createColumnHelper, flexRender, @@ -7,18 +7,22 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table"; -import { memo, useState } from "react"; -import type { LootEntry, TimeFilterOption } from "../useLootMap"; +import { memo, useMemo, useState } from "react"; +import type { LootEntry, LootGroup, TimeFilterOption } from "../useLootMap"; import type { SortingState } from "@tanstack/react-table"; const columnHelper = createColumnHelper(); -const buildColumns = (timeFilter: TimeFilterOption) => [ - columnHelper.accessor("timestamp", { - header: "Time", - cell: info => formatTimestamp(info.getValue(), timeFilter), - sortingFn: "datetime", - }), +const buildColumns = (timeFilter: TimeFilterOption, combined: boolean) => [ + ...(combined + ? [] + : [ + columnHelper.accessor("timestamp", { + header: "Time", + cell: info => formatTimestamp(info.getValue(), timeFilter), + sortingFn: "datetime" as const, + }), + ]), columnHelper.display({ id: "image", header: "Item", @@ -40,6 +44,15 @@ const buildColumns = (timeFilter: TimeFilterOption) => [ ), }), + ...(combined + ? [ + columnHelper.accessor("quantity", { + header: "Qty", + cell: info => info.getValue().toLocaleString(), + meta: { align: "right" }, + }), + ] + : []), columnHelper.accessor("totalValue", { header: "Value", cell: info => @@ -52,30 +65,61 @@ const buildColumns = (timeFilter: TimeFilterOption) => [ }), ]; +/** + * Aggregate entries by item name — sums quantity and totalValue, keeps most recent timestamp + */ +const combineEntries = (entries: LootEntry[]): LootEntry[] => { + const map = new Map(); + + entries.forEach(entry => { + const existing = map.get(entry.name); + if (existing) { + existing.quantity += entry.quantity; + existing.totalValue += entry.totalValue; + if (entry.timestamp > existing.timestamp) { + existing.timestamp = entry.timestamp; + } + } else { + map.set(entry.name, { ...entry }); + } + }); + + return Array.from(map.values()).sort((a, b) => b.totalValue - a.totalValue || a.name.localeCompare(b.name)); +}; + interface LootTableProps { filteredLootEntries: LootEntry[]; + sortedAndGroupedLoot: LootGroup[]; timeFilter: TimeFilterOption; zoomLevel: number; } -const LootTable = memo(({ filteredLootEntries, timeFilter }: LootTableProps) => { - const [sorting, setSorting] = useState([{ id: "timestamp", desc: true }]); +const GroupTable = memo( + ({ + entries, + timeFilter, + combined, + sorting, + onSortingChange, + }: { + entries: LootEntry[]; + timeFilter: TimeFilterOption; + combined: boolean; + sorting: SortingState; + onSortingChange: (s: SortingState) => void; + }) => { + const columns = useMemo(() => buildColumns(timeFilter, combined), [timeFilter, combined]); - const columns = buildColumns(timeFilter); + const table = useReactTable({ + data: entries, + columns, + state: { sorting }, + onSortingChange, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + }); - const table = useReactTable({ - data: filteredLootEntries, - columns, - state: { sorting }, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - }); - - if (filteredLootEntries.length === 0) return null; - - return ( -
+ return ( {table.getHeaderGroups().map(headerGroup => ( @@ -116,6 +160,86 @@ const LootTable = memo(({ filteredLootEntries, timeFilter }: LootTableProps) => ))}
+ ); + }, +); + +GroupTable.displayName = "GroupTable"; + +const LootTable = memo(({ filteredLootEntries, sortedAndGroupedLoot, timeFilter }: LootTableProps) => { + const [sorting, setSorting] = useState([{ id: "timestamp", desc: true }]); + const [combineItems, setCombineItems] = useState(false); + const isGrouped = timeFilter !== "none"; + + if (filteredLootEntries.length === 0) return null; + + if (!isGrouped) { + return ( +
+ +
+ ); + } + + return ( +
+ {sortedAndGroupedLoot.map(group => { + if (group.entries.length === 0) return null; + + const totalValue = group.entries.reduce((sum, e) => sum + e.totalValue, 0); + const netProfit = totalValue - group.totalCost; + + return ( +
+
+
+ {group.header} + = 0 ? "bg-green-500/15 text-green-600" : "bg-red-500/15 text-red-600", + )}> + {netProfit >= 0 ? "+" : ""} + {netProfit.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} GP + +
+ setCombineItems(prev => !prev)} + isActive={combineItems} + className="cursor-pointer text-xs"> + Combine + +
+
+ +
+
+ + Total Value:{" "} + {totalValue.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} GP + + + Total Cost:{" "} + {group.totalCost.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} GP + +
+
+ ); + })}
); }); diff --git a/app/panel/components/LootMap/helpers/lootHelpers.ts b/app/panel/components/LootMap/helpers/lootHelpers.ts index 660a62c..36ba42c 100644 --- a/app/panel/components/LootMap/helpers/lootHelpers.ts +++ b/app/panel/components/LootMap/helpers/lootHelpers.ts @@ -151,3 +151,34 @@ export const formatTimestamp = (timestamp: string, timeFilter: string): string = const month = date.toLocaleString("en-US", { month: "short" }); return `${month} ${date.getDate()}`; }; + +/** + * Parse a semicolon-separated damageReceived string and return total HP damage. + * e.g. "5;7;3;8" → 23 + */ +export const parseDamageReceived = (damageReceived: string): number => { + if (!damageReceived) return 0; + return damageReceived.split(";").reduce((sum, val) => { + const num = parseInt(val.trim(), 10); + return sum + (isNaN(num) ? 0 : num); + }, 0); +}; + +/** + * Get the time group key for a timestamp based on the active time filter. + * Returns the ISO string of the period start, or "" for "none". + */ +export const getTimeGroupKey = (timestamp: string, timeFilter: string): string => { + switch (timeFilter) { + case "hour": + return getHourStart(timestamp); + case "day": + return getDayStart(timestamp); + case "month": + return getMonthStart(timestamp); + case "year": + return getYearStart(timestamp); + default: + return ""; + } +}; diff --git a/app/panel/components/LootMap/index.tsx b/app/panel/components/LootMap/index.tsx index a652beb..3ef8dcd 100644 --- a/app/panel/components/LootMap/index.tsx +++ b/app/panel/components/LootMap/index.tsx @@ -198,7 +198,12 @@ const LootMap = memo(() => {
{viewMode === "table" ? ( - + ) : ( { return filtered; }, [allLootEntries, sourceFilter, filterMonster, filterLocation]); + // Compute HP cost (damageReceived * 2.5 GP) per time group from raw data + const costByTimeGroup = useMemo(() => { + const map = new Map(); + + allData.forEach(row => { + if (!row.damageReceived) return; + + // Apply same monster/location filters as loot entries + if (filterMonster !== "none" && row.monster?.trim() !== filterMonster) return; + if (filterLocation !== "none" && row.location?.trim() !== filterLocation) return; + + const damage = parseDamageReceived(row.damageReceived); + if (damage <= 0) return; + + const key = getTimeGroupKey(row.timestamp, timeFilter); + map.set(key, (map.get(key) || 0) + damage); + }); + + return map; + }, [allData, timeFilter, filterMonster, filterLocation]); + // Sort and group loot entries const sortedAndGroupedLoot = useMemo(() => { const entries = [...filteredLootEntries]; @@ -202,76 +226,40 @@ export const useLootMap = () => { return sorted; }; - // Group by time filter - if (timeFilter === "day") { - const grouped = new Map(); - // Group entries by day - entries.forEach(entry => { - const dayStart = getDayStart(entry.timestamp); - if (!grouped.has(dayStart)) { - grouped.set(dayStart, []); - } - grouped.get(dayStart)!.push(entry); - }); - // Sort groups by day (newest first), then sort entries within each group by sortOption - return Array.from(grouped.entries()) - .sort(([a], [b]) => b.localeCompare(a)) // Sort groups by timestamp (newest first) - .map(([dayStart, groupEntries]) => ({ - header: formatDayHeader(dayStart), - entries: sortEntries(groupEntries), - })); - } else if (timeFilter === "month") { - const grouped = new Map(); - entries.forEach(entry => { - const monthStart = getMonthStart(entry.timestamp); - if (!grouped.has(monthStart)) { - grouped.set(monthStart, []); - } - grouped.get(monthStart)!.push(entry); - }); - return Array.from(grouped.entries()) - .sort(([a], [b]) => b.localeCompare(a)) - .map(([monthStart, groupEntries]) => ({ - header: formatMonthHeader(monthStart), - entries: sortEntries(groupEntries), - })); - } else if (timeFilter === "year") { + // Helper: build a group with cost attached + const buildGroup = (key: string, header: string, groupEntries: LootEntry[]): LootGroup => ({ + header, + entries: sortEntries(groupEntries), + totalCost: (costByTimeGroup.get(key) || 0) * 2.5, + }); + + // Helper: group entries by a time function, format headers, attach costs + const groupByTime = (getStart: (ts: string) => string, formatHeader: (ts: string) => string): LootGroup[] => { const grouped = new Map(); entries.forEach(entry => { - const yearStart = getYearStart(entry.timestamp); - if (!grouped.has(yearStart)) { - grouped.set(yearStart, []); - } - grouped.get(yearStart)!.push(entry); + const key = getStart(entry.timestamp); + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key)!.push(entry); }); return Array.from(grouped.entries()) .sort(([a], [b]) => b.localeCompare(a)) - .map(([yearStart, groupEntries]) => ({ - header: formatYearHeader(yearStart), - entries: sortEntries(groupEntries), - })); - } else if (timeFilter === "hour") { - const grouped = new Map(); - // Group entries by hour - entries.forEach(entry => { - const hourStart = getHourStart(entry.timestamp); - if (!grouped.has(hourStart)) { - grouped.set(hourStart, []); - } - grouped.get(hourStart)!.push(entry); - }); - // Sort groups by hour (newest first), then sort entries within each group by sortOption - return Array.from(grouped.entries()) - .sort(([a], [b]) => b.localeCompare(a)) // Sort groups by timestamp (newest first) - .map(([hourStart, groupEntries]) => ({ - header: formatHourHeader(hourStart), - entries: sortEntries(groupEntries), - })); - } else { - // No time filter - no grouping, just sort all entries - return [{ header: "", entries: sortEntries(entries) }]; + .map(([key, groupEntries]) => buildGroup(key, formatHeader(key), groupEntries)); + }; + + // Group by time filter + switch (timeFilter) { + case "hour": + return groupByTime(getHourStart, formatHourHeader); + case "day": + return groupByTime(getDayStart, formatDayHeader); + case "month": + return groupByTime(getMonthStart, formatMonthHeader); + case "year": + return groupByTime(getYearStart, formatYearHeader); + default: + return [buildGroup("", "", entries)]; } - }, [filteredLootEntries, sortOption, timeFilter]); + }, [filteredLootEntries, sortOption, timeFilter, costByTimeGroup]); // Get all unique item names for settings const allUniqueItems = useMemo(() => { diff --git a/app/styles/global.css b/app/styles/global.css index 4350ca3..1b9d1cd 100644 --- a/app/styles/global.css +++ b/app/styles/global.css @@ -107,6 +107,22 @@ @layer base { * { @apply border-border; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; + } + *::-webkit-scrollbar { + width: 6px; + height: 6px; + } + *::-webkit-scrollbar-track { + background: transparent; + } + *::-webkit-scrollbar-thumb { + background-color: var(--border); + border-radius: 9999px; + } + *::-webkit-scrollbar-thumb:hover { + background-color: var(--muted-foreground); } body { @apply bg-background text-foreground; diff --git a/build/hmr/dist/lib/plugins/watch-public-plugin.js b/build/hmr/dist/lib/plugins/watch-public-plugin.js index 555da4b..1668409 100644 --- a/build/hmr/dist/lib/plugins/watch-public-plugin.js +++ b/build/hmr/dist/lib/plugins/watch-public-plugin.js @@ -1,8 +1,10 @@ -import fg from 'fast-glob'; +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; export const watchPublicPlugin = () => ({ name: 'watch-public-plugin', async buildStart() { - const files = await fg(['public/**/*']); + const entries = await readdir('public', { recursive: true, withFileTypes: true }); + const files = entries.filter(e => e.isFile()).map(e => join(e.parentPath, e.name)); for (const file of files) { this.addWatchFile(file); } diff --git a/build/hmr/lib/lib/plugins/watch-public-plugin.ts b/build/hmr/lib/lib/plugins/watch-public-plugin.ts index ebe2258..1c3eabb 100644 --- a/build/hmr/lib/lib/plugins/watch-public-plugin.ts +++ b/build/hmr/lib/lib/plugins/watch-public-plugin.ts @@ -1,10 +1,12 @@ -import fg from "fast-glob"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; import type { PluginOption } from "vite"; export const watchPublicPlugin = (): PluginOption => ({ name: "watch-public-plugin", async buildStart() { - const files = await fg(["public/**/*"]); + const entries = await readdir("public", { recursive: true, withFileTypes: true }); + const files = entries.filter(e => e.isFile()).map(e => join(e.parentPath, e.name)); for (const file of files) { this.addWatchFile(file); diff --git a/build/hmr/lib/plugins/watch-public-plugin.ts b/build/hmr/lib/plugins/watch-public-plugin.ts index ebe2258..1c3eabb 100644 --- a/build/hmr/lib/plugins/watch-public-plugin.ts +++ b/build/hmr/lib/plugins/watch-public-plugin.ts @@ -1,10 +1,12 @@ -import fg from "fast-glob"; +import { readdir } from "node:fs/promises"; +import { join } from "node:path"; import type { PluginOption } from "vite"; export const watchPublicPlugin = (): PluginOption => ({ name: "watch-public-plugin", async buildStart() { - const files = await fg(["public/**/*"]); + const entries = await readdir("public", { recursive: true, withFileTypes: true }); + const files = entries.filter(e => e.isFile()).map(e => join(e.parentPath, e.name)); for (const file of files) { this.addWatchFile(file); diff --git a/dist.crx b/dist.crx deleted file mode 100644 index 5971b19..0000000 Binary files a/dist.crx and /dev/null differ diff --git a/dist.pem b/dist.pem deleted file mode 100644 index 0765c0c..0000000 --- a/dist.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPZu/t8PM2bc/H -rjIJBJAx4B6BM8PUHXYOOprsjbOtEy8qdlFSAb8L+VwG1ii+nrgE2oFUXKxzgdlt -Gg5b4ObG6xJVYAqfgwM3/ckAiHYqIvq3S+OjoeKCyonNIoj92QvIXUTz8ontH9+w -pOFz7IyjNNC0tY7SjYpQw3Pqy9BufDRjJqEyFubn4SPmJ/sgGg5IzyxIM54kAkb3 -hgv10Vyr9l5pVDVenBHZoN0OK3UOfROc5mkpCK8FyVFtZjNqyDL8Tg52lOePEGEu -IUBzRWgQr+Da+aqeuooQO8BR8Bq4IF4mLzZwSGWr9TzoZW6jglpsR8H9ol+0BoJe -wUvpmtU3AgMBAAECggEAIGclrxgI94T6hGjrukrzOEeanHb47mfX7OstDy4oVy77 -HHg1xkktjvJbdVn6LaXgfCmjMQn1i2pLDOeYPLSLeaJ6T8pcfoVaeGH3CFVJmNW0 -M/TwUL1XBTYdh2YQ3xAM7jXcMvy+ncRwD0oDSzg21LzIqqL++FhStsGCl7lWDByl -lHTaaobyhMtq1k+AimfCCic5x+KY5UJQPmZq5H1fwOcbAOVR1E72PWWcbCLn4Q5w -5pTk7QozPsAluq7Rs/BRR4o7KVFgnbQ2TlOnfze4eu8xab+qa/RNFzsVnpVsBARd -UuvM/JTgrW1hFr35ec6LEuqfX3rfJ2U6uB2q7PEUKQKBgQDp8II/kHhGYRbHaouS -xatzj+cW4yvo9qSeGwMG71HehRxZaQ6+iMp1Kdsoz+QHz0cgnHhdEeAETtlcttnR -9ppSlf1Ok6U2HneCx0RSWF4W4CFGSWGIThcR9G04cHXb7YH0gTyCjq5mAlqIhZEz -ffnkDl+GnqUMuYgpaYCvg/WfUwKBgQDi9ctJ0cpOH/tmdzO543aE31QlUYTM6HqZ -kSm0Hqsw4yYHgFNlviv1CUjleroJnPY+e8WkoctTW3f1CtfHRRL37jdHj51cmsFs -JvUv1vdtIEN83gZXLf36FUByFkGzJFFr7/ouQjbh7U87/wruUc/Y6jp7kyN2Rndk -2b1yc+WKDQKBgAkLmZ2nkl72RhEz1223Yue2+rJx2jLJxpeuZ7GO81VtRClY3I8V -bu6UzbqIP0LmCtQB+M1wcX75JYJmN4ncxR7IWeD96HdtOiW+Wq/sZsbvp5m2IKVK -j63MC6URBB2a01mG3bYWmjurf6CjuXeKOW1N+PeVjsAtNEyVnbormgWtAoGAZAf7 -anTJmrdj4ZyGMtb7RML93ljYlEuexZKCc/3Ua/6ho0q+9yfjTeOiumnmFxiUO6Pn -z5D0AbBd5aW6xvw9yQK4YJT+wXL/70AYbtZrRTCkLiPZXZFK0G+PcPk4SdGnBVDG -zp6me9tRQds+IR3z4uIRlqeaSKX0N4H0ir5kG/0CgYAez/8u8ISs7hT/sSRmQ96z -8whc6zccgBHEVE/+vRjr2b9lMbnaUa/WtH1fp3WZBA35m2Gb16YgVd9NML4TQEPP -atioMdSREGD5TQK1zwDdHYH+fVl7zM0vZCHtrhN0H9mIDkYKqIplRNmlAgZV+qZT -/+elNTkYf/s8F/ooJJ8htw== ------END PRIVATE KEY----- diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/CSV_TRACKING_README.md b/docs/CSV_TRACKING_README.md similarity index 100% rename from CSV_TRACKING_README.md rename to docs/CSV_TRACKING_README.md diff --git a/DATA_SCRAPING_FLOW.md b/docs/DATA_SCRAPING_FLOW.md similarity index 100% rename from DATA_SCRAPING_FLOW.md rename to docs/DATA_SCRAPING_FLOW.md diff --git a/DATA_VIEW_FEATURE.md b/docs/DATA_VIEW_FEATURE.md similarity index 100% rename from DATA_VIEW_FEATURE.md rename to docs/DATA_VIEW_FEATURE.md diff --git a/EQUIPMENT_AND_PEOPLE_FIGHTING_EXTRACTION.md b/docs/EQUIPMENT_AND_PEOPLE_FIGHTING_EXTRACTION.md similarity index 100% rename from EQUIPMENT_AND_PEOPLE_FIGHTING_EXTRACTION.md rename to docs/EQUIPMENT_AND_PEOPLE_FIGHTING_EXTRACTION.md diff --git a/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md similarity index 100% rename from QUICK_REFERENCE.md rename to docs/QUICK_REFERENCE.md diff --git a/UPDATE-PACKAGE-VERSIONS.md b/docs/UPDATE-PACKAGE-VERSIONS.md similarity index 100% rename from UPDATE-PACKAGE-VERSIONS.md rename to docs/UPDATE-PACKAGE-VERSIONS.md