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 (
-
+ );
+ },
+);
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">
-
- {tab.label}
-
- );
- })}
-
- {/* Settings dropdown for desktop */}
-
-
-
-
- Settings
-
-
-
-
- {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">
-
- {item.label}
-
- );
- })}
-
-
-
-
-
- {/* 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">
-
- {tab.label}
-
- );
- })}
-
-
-
-
+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">
+
+ {label}
+
+ );
+ })}
- );
-};
+
+);
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