diff --git a/app/features/tier-list-maker/components/ItemPool.module.css b/app/features/tier-list-maker/components/ItemPool.module.css index 34f66af1b..3bea2ea55 100644 --- a/app/features/tier-list-maker/components/ItemPool.module.css +++ b/app/features/tier-list-maker/components/ItemPool.module.css @@ -3,3 +3,21 @@ flex-wrap: wrap; gap: var(--s-3); } + +.clickableItem { + min-height: 50px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.1s; + + &:hover:not(:disabled) { + transform: scale(1.1); + } + + &:disabled { + opacity: 0.4; + cursor: not-allowed; + } +} diff --git a/app/features/tier-list-maker/components/ItemPool.tsx b/app/features/tier-list-maker/components/ItemPool.tsx index 6170f7ee2..f815b0b45 100644 --- a/app/features/tier-list-maker/components/ItemPool.tsx +++ b/app/features/tier-list-maker/components/ItemPool.tsx @@ -1,19 +1,55 @@ import { useDroppable } from "@dnd-kit/core"; import { useTierListState } from "../contexts/TierListContext"; +import type { TierListItem } from "../tier-list-maker-schemas"; +import { tierListItemId } from "../tier-list-maker-utils"; import { DraggableItem } from "./DraggableItem"; import styles from "./ItemPool.module.css"; +import { TierListItemImage } from "./TierListItemImage"; export function ItemPool() { - const { availableItems } = useTierListState(); + const { availableItems, placementMode, selectedTierId, handleAddItemToTier } = + useTierListState(); const { setNodeRef } = useDroppable({ id: "item-pool", }); return (
- {availableItems.map((item) => ( - - ))} + {availableItems.map((item) => + placementMode === "click" ? ( + + selectedTierId && handleAddItemToTier(item, selectedTierId) + } + /> + ) : ( + + ), + )}
); } + +function ClickableItem({ + item, + onClick, + disabled, +}: { + item: TierListItem; + onClick: () => void; + disabled: boolean; +}) { + return ( + + ); +} diff --git a/app/features/tier-list-maker/components/TierRow.module.css b/app/features/tier-list-maker/components/TierRow.module.css index 7514762ef..ea99a1d72 100644 --- a/app/features/tier-list-maker/components/TierRow.module.css +++ b/app/features/tier-list-maker/components/TierRow.module.css @@ -66,6 +66,21 @@ background: var(--color-bg-higher); } +.targetZoneSelectable { + cursor: pointer; +} + + +.targetZoneSelectable:hover { + background-color: var(--color-bg-higher); +} + +.targetZoneSelected { + background: var(--color-bg-higher); + outline: var(--border-style-high); + outline-offset: -2px; +} + .emptyMessage { width: 100%; height: 100%; diff --git a/app/features/tier-list-maker/components/TierRow.tsx b/app/features/tier-list-maker/components/TierRow.tsx index c4cc36b33..647bde496 100644 --- a/app/features/tier-list-maker/components/TierRow.tsx +++ b/app/features/tier-list-maker/components/TierRow.tsx @@ -5,6 +5,7 @@ import { } from "@dnd-kit/sortable"; import clsx from "clsx"; import { ChevronDown, ChevronUp, Trash } from "lucide-react"; +import type { KeyboardEvent } from "react"; import { useLayoutEffect, useRef } from "react"; import { Button } from "react-aria-components"; import { useTranslation } from "react-i18next"; @@ -38,6 +39,9 @@ export function TierRow({ tier }: TierRowProps) { handleMoveTierDown, showTierHeaders, screenshotMode, + placementMode, + selectedTierId, + setSelectedTierId, } = useTierListState(); const items = getItemsInTier(tier.id); @@ -55,6 +59,23 @@ export function TierRow({ tier }: TierRowProps) { const isFirstTier = tierIndex === 0; const isLastTier = tierIndex === state.tiers.length - 1; + const isClickMode = placementMode === "click"; + const isSelected = isClickMode && selectedTierId === tier.id; + + const selectTierProps = isClickMode + ? { + role: "button", + tabIndex: 0, + onClick: () => setSelectedTierId(tier.id), + onKeyDown: (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + setSelectedTierId(tier.id); + } + }, + } + : {}; + return (
{showTierHeaders ? ( @@ -134,11 +155,16 @@ export function TierRow({ tier }: TierRowProps) { }} className={clsx(styles.targetZone, { [styles.targetZoneOver]: isOver, + [styles.targetZoneSelectable]: isClickMode, + [styles.targetZoneSelected]: isSelected, })} + {...selectTierProps} > {items.length === 0 && !screenshotMode ? (
- {t("tier-list-maker:dropItems")} + {isClickMode + ? t("tier-list-maker:clickToAdd") + : t("tier-list-maker:dropItems")}
) : items.length > 0 ? ( ({ @@ -47,6 +49,17 @@ export function useTierList() { useSearchParamTiersState(); const [activeItem, setActiveItem] = React.useState(null); + const [placementMode, setPlacementMode] = + React.useState("click"); + const [selectedTierId, setSelectedTierId] = React.useState( + () => tiers.tiers[0]?.id ?? null, + ); + + const handleChangePlacementMode = (mode: TierListPlacementMode) => { + setPlacementMode(mode); + setSelectedTierId(mode === "click" ? (tiers.tiers[0]?.id ?? null) : null); + }; + const [hideAltKits, setHideAltKits] = useSearchParamState({ name: "hideAltKits", defaultValue: false, @@ -278,6 +291,14 @@ export function useTierList() { persistTiersStateToParams(tiers); }; + const handleAddItemToTier = (item: TierListItem, tierId: string) => { + const newState = addItemToTier(tiers, tierId, item); + if (newState === tiers) return; + + setTiers(newState); + persistTiersStateToParams(newState); + }; + const handleAddTier = () => { const newTier: TierListMakerTier = { id: `tier-${Date.now()}`, @@ -294,6 +315,10 @@ export function useTierList() { }; const handleRemoveTier = (tierId: string) => { + if (selectedTierId === tierId) { + setSelectedTierId(null); + } + const newTierItems = new Map(tiers.tierItems); newTierItems.delete(tierId); @@ -455,6 +480,7 @@ export function useTierList() { handleDragOver, handleDragEnd, handleAddTier, + handleAddItemToTier, handleRemoveTier, handleRenameTier, handleChangeTierColor, @@ -475,6 +501,10 @@ export function useTierList() { setTitle, selectedModes, setSelectedModes, + placementMode, + setPlacementMode: handleChangePlacementMode, + selectedTierId, + setSelectedTierId, }; } diff --git a/app/features/tier-list-maker/routes/tier-list-maker.tsx b/app/features/tier-list-maker/routes/tier-list-maker.tsx index 6485d7cb9..29586a05a 100644 --- a/app/features/tier-list-maker/routes/tier-list-maker.tsx +++ b/app/features/tier-list-maker/routes/tier-list-maker.tsx @@ -18,6 +18,10 @@ import { useTranslation } from "react-i18next"; import type { MetaFunction } from "react-router"; import { Avatar } from "~/components/Avatar"; import { SendouButton } from "~/components/elements/Button"; +import { + SendouChipRadio, + SendouChipRadioGroup, +} from "~/components/elements/ChipRadio"; import { SendouPopover } from "~/components/elements/Popover"; import { SendouSwitch } from "~/components/elements/Switch"; import { @@ -42,9 +46,12 @@ import { TierListProvider, useTierListState, } from "../contexts/TierListContext"; +import type { TierListPlacementMode } from "../hooks/useTierList"; import type { TierListItem } from "../tier-list-maker-schemas"; import styles from "./tier-list-maker.module.css"; +const PLACEMENT_MODES: TierListPlacementMode[] = ["click", "track"]; + export const meta: MetaFunction = (args) => { return metaTags({ title: "Tier List Maker", @@ -109,6 +116,8 @@ function TierListMakerContent() { setScreenshotMode, selectedModes, setSelectedModes, + placementMode, + setPlacementMode, } = useTierListState(); const sensors = useSensors( @@ -199,7 +208,24 @@ function TierListMakerContent() { ))}
-
+
+ + {PLACEMENT_MODES.map((mode) => ( + + setPlacementMode(value as TierListPlacementMode) + } + > + {t( + `tier-list-maker:placementMode${mode === "track" ? "Track" : "Click"}`, + )} + + ))} + = {}, +): TierListState { + return { + tiers: [ + { id: "tier-a", name: "A", color: "#ffffff" }, + { id: "tier-b", name: "B", color: "#000000" }, + ], + tierItems: new Map(Object.entries(tierItems)), + }; +} + +const splattershot: TierListItem = { type: "main-weapon", id: 40 }; +const splatRoller: TierListItem = { type: "main-weapon", id: 1010 }; + +describe("addItemToTier", () => { + it("appends the item to the target tier", () => { + const state = makeState(); + + const result = addItemToTier(state, "tier-a", splattershot); + + expect(result.tierItems.get("tier-a")).toEqual([splattershot]); + }); + + it("appends to the end keeping existing items", () => { + const state = makeState({ "tier-a": [splattershot] }); + + const result = addItemToTier(state, "tier-a", splatRoller); + + expect(result.tierItems.get("tier-a")).toEqual([splattershot, splatRoller]); + }); + + it("does not mutate the original state", () => { + const state = makeState({ "tier-a": [splattershot] }); + + addItemToTier(state, "tier-a", splatRoller); + + expect(state.tierItems.get("tier-a")).toEqual([splattershot]); + }); + + it("leaves other tiers untouched", () => { + const state = makeState({ "tier-b": [splatRoller] }); + + const result = addItemToTier(state, "tier-a", splattershot); + + expect(result.tierItems.get("tier-b")).toEqual([splatRoller]); + }); + + it("returns the same state reference when the tier does not exist", () => { + const state = makeState(); + + const result = addItemToTier(state, "tier-missing", splattershot); + + expect(result).toBe(state); + }); +}); + +describe("getNextNthForItem", () => { + it("returns 1 when the item is not yet placed", () => { + const state = makeState(); + + expect(getNextNthForItem(splattershot, state)).toBe(1); + }); + + it("returns max nth + 1 across all tiers", () => { + const state = makeState({ + "tier-a": [splattershot], + "tier-b": [{ ...splattershot, nth: 2 }], + }); + + expect(getNextNthForItem(splattershot, state)).toBe(3); + }); +}); + +describe("tierListItemId", () => { + it("omits nth when it is not set", () => { + expect(tierListItemId(splattershot)).toBe("main-weapon:40"); + }); + + it("includes nth when set", () => { + expect(tierListItemId({ ...splattershot, nth: 2 })).toBe( + "main-weapon:40:2", + ); + }); +}); diff --git a/app/features/tier-list-maker/tier-list-maker-utils.ts b/app/features/tier-list-maker/tier-list-maker-utils.ts index 25d5b0a8a..e56babbea 100644 --- a/app/features/tier-list-maker/tier-list-maker-utils.ts +++ b/app/features/tier-list-maker/tier-list-maker-utils.ts @@ -4,6 +4,30 @@ export function tierListItemId(item: TierListItem) { return `${item.type}:${item.id}${item.nth ? `:${item.nth}` : ""}`; } +/** + * Returns a new tier list state with the given item appended to the end of the + * specified tier. Used by the "click" placement mode. If the tier does not + * exist the state is returned unchanged. + */ +export function addItemToTier( + state: TierListState, + tierId: string, + item: TierListItem, +): TierListState { + if (!state.tiers.some((tier) => tier.id === tierId)) { + return state; + } + + const newTierItems = new Map(state.tierItems); + const tierItems = newTierItems.get(tierId) ?? []; + newTierItems.set(tierId, [...tierItems, item]); + + return { + ...state, + tierItems: newTierItems, + }; +} + /** * Finds the next nth value for a duplicate item in the tier list. * Searches through all tiers to find the maximum nth value for items diff --git a/locales/da/tier-list-maker.json b/locales/da/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/da/tier-list-maker.json +++ b/locales/da/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/de/tier-list-maker.json b/locales/de/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/de/tier-list-maker.json +++ b/locales/de/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/en/tier-list-maker.json b/locales/en/tier-list-maker.json index 2de1cc94f..7cdbf723a 100644 --- a/locales/en/tier-list-maker.json +++ b/locales/en/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "Stage + Modes", "abilities": "Abilities", "dropItems": "Drop items here", + "clickToAdd": "Click items to add here", + "placementModeTrack": "Drag & drop", + "placementModeClick": "Click to place", "editingTier": "Editing tier", "hideAltKits": "Hide alt kits", "hideAltSkins": "Hide alt skins", diff --git a/locales/es-ES/tier-list-maker.json b/locales/es-ES/tier-list-maker.json index 486c451c4..44bdc6d61 100644 --- a/locales/es-ES/tier-list-maker.json +++ b/locales/es-ES/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "Escenario + Modos", "abilities": "", "dropItems": "Suelta los elementos aquí", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "Editando tier", "hideAltKits": "Ocultar kits alternativos", "hideAltSkins": "Ocultar skins alternativas", diff --git a/locales/es-US/tier-list-maker.json b/locales/es-US/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/es-US/tier-list-maker.json +++ b/locales/es-US/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/fr-CA/tier-list-maker.json b/locales/fr-CA/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/fr-CA/tier-list-maker.json +++ b/locales/fr-CA/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/fr-EU/tier-list-maker.json b/locales/fr-EU/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/fr-EU/tier-list-maker.json +++ b/locales/fr-EU/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/he/tier-list-maker.json b/locales/he/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/he/tier-list-maker.json +++ b/locales/he/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/it/tier-list-maker.json b/locales/it/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/it/tier-list-maker.json +++ b/locales/it/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/ja/tier-list-maker.json b/locales/ja/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/ja/tier-list-maker.json +++ b/locales/ja/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/ko/tier-list-maker.json b/locales/ko/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/ko/tier-list-maker.json +++ b/locales/ko/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/nl/tier-list-maker.json b/locales/nl/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/nl/tier-list-maker.json +++ b/locales/nl/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/pl/tier-list-maker.json b/locales/pl/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/pl/tier-list-maker.json +++ b/locales/pl/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/pt-BR/tier-list-maker.json b/locales/pt-BR/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/pt-BR/tier-list-maker.json +++ b/locales/pt-BR/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/ru/tier-list-maker.json b/locales/ru/tier-list-maker.json index e2bee98ce..cca1a4fbf 100644 --- a/locales/ru/tier-list-maker.json +++ b/locales/ru/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "", "abilities": "", "dropItems": "", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "", "hideAltKits": "", "hideAltSkins": "", diff --git a/locales/zh/tier-list-maker.json b/locales/zh/tier-list-maker.json index 5bf21fd67..acc25f8df 100644 --- a/locales/zh/tier-list-maker.json +++ b/locales/zh/tier-list-maker.json @@ -9,6 +9,9 @@ "stageModes": "场地 + 模式", "abilities": "装备能力", "dropItems": "将项目拖拽至此处", + "clickToAdd": "", + "placementModeTrack": "", + "placementModeClick": "", "editingTier": "正在编辑分级", "hideAltKits": "隐藏贴牌武器", "hideAltSkins": "隐藏异色武器",