Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.4",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.0.6",
"@react-hook/media-query": "^1.1.1",
"@tabler/icons-react": "^2.30.0",
Expand All @@ -47,6 +49,7 @@
"eslint": "8.44.0",
"eslint-config-next": "13.4.12",
"fast-xml-parser": "^4.2.6",
"lucide-react": "^0.522.0",
"mysql2": "^3.10.1",
"next": "^14.2.3",
"next-themes": "^0.2.1",
Expand Down
9 changes: 9 additions & 0 deletions src/components/cards/boolean-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ItemData, MuseumItem } from "@/types/items";
import { Dispatch, SetStateAction } from "react";

import { usePlayers } from "@/contexts/players-context";
import { useMultiSelect } from "@/contexts/multi-select-context";

import { NewItemBadge } from "@/components/new-item-badge";
import {
Expand Down Expand Up @@ -66,6 +67,7 @@ export const BooleanCard = ({
handleStatusChange,
}: BooleanCardProps) => {
const { activePlayer, patchPlayer } = usePlayers();
const { isMultiSelectMode, selectedItems, toggleItem } = useMultiSelect();
// let itemType = "O"; //Todo add item types to object data files, and use them here to hotswap data source
// let dataSource = objects;
let iconURL: string;
Expand Down Expand Up @@ -137,6 +139,8 @@ export const BooleanCard = ({
await patchPlayer(patch);
}

const isSelected = selectedItems.has(item.itemID.toString());

return (
<ContextMenu>
<ContextMenuTrigger asChild>
Expand All @@ -146,8 +150,13 @@ export const BooleanCard = ({
completed
? "border-green-900 bg-green-500/20 hover:bg-green-500/30 dark:bg-green-500/10 hover:dark:bg-green-500/20"
: "border-neutral-200 bg-white hover:bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800",
isMultiSelectMode && isSelected && "ring-primary ring-2",
)}
onClick={() => {
if (isMultiSelectMode) {
toggleItem(item.itemID.toString());
return;
}
if (minVersion === "1.6.0" && !show && !completed) {
setPromptOpen?.(true);
return;
Expand Down
10 changes: 10 additions & 0 deletions src/components/cards/dialog-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {

import { Stardrop } from "@/lib/parsers/general";
import { ChevronRightIcon } from "@radix-ui/react-icons";
import { useMultiSelect } from "@/contexts/multi-select-context";

interface Props {
title: string;
Expand Down Expand Up @@ -65,13 +66,18 @@ export const DialogCard = ({
}: Props) => {
const { activePlayer, patchPlayer } = usePlayers();
const [open, setOpen] = useState(false);
const { isMultiSelectMode, selectedItems, toggleItem } = useMultiSelect();

const minVersion = _type === "power" ? powersData[_id].minVersion : "1.5.0";

let checkedClass = completed
? "border-green-900 bg-green-500/20 hover:bg-green-500/30 dark:bg-green-500/10 hover:dark:bg-green-500/20"
: "border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-950 hover:bg-neutral-100 dark:hover:bg-neutral-800";

if (isMultiSelectMode && selectedItems.has(_id)) {
checkedClass += " ring-2 ring-primary";
}

async function handleStatusChange(status: boolean) {
if (!activePlayer) return;

Expand Down Expand Up @@ -156,6 +162,10 @@ export const DialogCard = ({
checkedClass,
)}
onClick={(e) => {
if (isMultiSelectMode) {
toggleItem(_id);
return;
}
if (minVersion === "1.6.0" && !show && !completed) {
e.preventDefault();
setPromptOpen?.(true);
Expand Down
58 changes: 47 additions & 11 deletions src/components/cards/recipe-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import objects from "@/data/objects.json";
import type { CraftingRecipe, Recipe } from "@/types/recipe";

import { cn } from "@/lib/utils";
import { Dispatch, SetStateAction } from "react";
import { Dispatch, SetStateAction, useRef } from "react";

import { usePlayers } from "@/contexts/players-context";
import { useMultiSelect } from "@/contexts/multi-select-context";

import { NewItemBadge } from "@/components/new-item-badge";
import {
Expand Down Expand Up @@ -41,6 +42,9 @@ interface Props<T extends Recipe> {
* @memberof Props
*/
setPromptOpen?: Dispatch<SetStateAction<boolean>>;

index: number;
allRecipes: T[];
}

export const RecipeCard = <T extends Recipe>({
Expand All @@ -50,8 +54,14 @@ export const RecipeCard = <T extends Recipe>({
setObject,
setPromptOpen,
show,
index,
allRecipes,
}: Props<T>) => {
const { activePlayer, patchPlayer } = usePlayers();
const { isMultiSelectMode, selectedItems, toggleItem, addItems } =
useMultiSelect();
const lastSelectedIndex = useRef<number | null>(null);

let colorClass = "";
switch (status) {
case 1:
Expand Down Expand Up @@ -110,24 +120,48 @@ export const RecipeCard = <T extends Recipe>({
await patchPlayer(patch);
}

const isSelected = selectedItems.has(recipe.itemID.toString());

const handleClick = (e: React.MouseEvent) => {
Copy link

Copilot AI Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The lastSelectedIndex ref isn’t reset when toggling multi-select mode, which could lead to unexpected range selections. Consider clearing lastSelectedIndex.current when exiting or entering multi-select mode.

Copilot uses AI. Check for mistakes.
if (isMultiSelectMode) {
if (e.shiftKey && lastSelectedIndex.current !== null) {
// Select range of items
const start = Math.min(lastSelectedIndex.current, index);
const end = Math.max(lastSelectedIndex.current, index);
const itemsToSelect = allRecipes
.slice(start, end + 1)
.map((r) => r.itemID.toString());
addItems(itemsToSelect);
} else {
// Single item selection
toggleItem(recipe.itemID.toString());
lastSelectedIndex.current = index;
}
return;
}

if (recipe.minVersion === "1.6.0" && !show && status < 1) {
setPromptOpen?.(true);
return;
}
setObject(recipe);
setIsOpen(true);
};

return (
<ContextMenu>
<ContextMenuTrigger asChild>
<button
className={cn(
"relative flex select-none items-center justify-between rounded-lg border px-5 py-4 text-neutral-950 shadow-sm transition-colors hover:cursor-pointer dark:text-neutral-50",
colorClass,
isMultiSelectMode && isSelected && "ring-2 ring-blue-500",
)}
onClick={() => {
if (recipe.minVersion === "1.6.0" && !show && status < 1) {
setPromptOpen?.(true);
return;
}
setObject(recipe);
setIsOpen(true);
}}
onClick={handleClick}
>
{recipe.minVersion === "1.6.0" && <NewItemBadge version={recipe.minVersion}/>}
{recipe.minVersion === "1.6.0" && (
<NewItemBadge version={recipe.minVersion} />
)}
<div
className={cn(
"flex items-center space-x-3 truncate text-left",
Expand All @@ -150,7 +184,9 @@ export const RecipeCard = <T extends Recipe>({
</p>
</div>
</div>
<IconChevronRight className="h-5 w-5 flex-shrink-0 text-neutral-500 dark:text-neutral-400" />
{!isMultiSelectMode && (
<IconChevronRight className="h-5 w-5 flex-shrink-0 text-neutral-500 dark:text-neutral-400" />
)}
</button>
</ContextMenuTrigger>
<ContextMenuContent className="w-48">
Expand Down
160 changes: 160 additions & 0 deletions src/components/dialogs/bulk-action-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { usePlayers } from "@/contexts/players-context";
import { useMultiSelect } from "@/contexts/multi-select-context";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";

interface Props {
open: boolean;
setOpen: (open: boolean) => void;
type: "cooking" | "crafting" | "shipping" | "museum";
onBulkAction?: (
status: number | null,
selectedItems: Set<string>,
close: () => void,
) => void;
}

export const BulkActionDialog = ({
open,
setOpen,
type,
onBulkAction,
}: Props) => {
const { selectedItems, clearSelection } = useMultiSelect();
const { activePlayer, patchPlayer } = usePlayers();

const close = () => {
clearSelection();
setOpen(false);
};

const handleBulkAction = async (status: number | null) => {
if (onBulkAction) {
onBulkAction(status, selectedItems, close);
return;
}
if (!activePlayer) return;

const patch: any = {};
const items = Array.from(selectedItems);

switch (type) {
case "cooking":
patch.cooking = {
recipes: Object.fromEntries(items.map((id) => [id, status])),
};
break;
case "crafting":
patch.crafting = {
recipes: Object.fromEntries(items.map((id) => [id, status])),
};
break;
case "shipping":
patch.shipping = {
shipped: Object.fromEntries(items.map((id) => [id, status])),
};
break;
case "museum":
patch.museum = {
donated: Object.fromEntries(items.map((id) => [id, status])),
};
break;
}

await patchPlayer(patch);
close();
};

let foundLabel = "Set All Selected as Completed";
let notFoundLabel = "Set All Selected as Incomplete";
if (type === "museum") {
foundLabel = "Set All Selected as Found";
notFoundLabel = "Set All Selected as Not Found";
} else if (type === "shipping") {
foundLabel = "Set All Selected as Shipped";
notFoundLabel = "Set All Selected as Unshipped";
}

if (
type === "museum" ||
type === "shipping" ||
type === "cooking" ||
type === "crafting"
) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Bulk Action</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2">
<Button
variant="secondary"
onClick={() => handleBulkAction(2)}
disabled={selectedItems.size === 0}
>
{foundLabel}
</Button>
<Button
variant="secondary"
onClick={() => handleBulkAction(0)}
disabled={selectedItems.size === 0}
>
{notFoundLabel}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Bulk Action</DialogTitle>
<DialogDescription>
{selectedItems.size} items selected
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-1 gap-2">
<Button
variant="outline"
onClick={() => handleBulkAction(null)}
disabled={!activePlayer}
>
Set Unknown
</Button>
<Button
variant="outline"
onClick={() => handleBulkAction(1)}
disabled={!activePlayer}
>
Set Known
</Button>
<Button
variant="outline"
onClick={() => handleBulkAction(2)}
disabled={!activePlayer}
>
Set Completed
</Button>
</div>
</div>
<DialogFooter>
<Button variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
Loading