mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-03 08:38:42 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
324
apps/web-react/app/form/fields/WeaponPoolFormField.tsx
Normal file
324
apps/web-react/app/form/fields/WeaponPoolFormField.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import type { DragEndEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import clsx from "clsx";
|
||||
import { Star, Trash } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { WeaponImage } from "~/components/Image";
|
||||
import { WeaponSelect } from "~/components/WeaponSelect";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { weaponIdToArrayWithAlts } from "~/modules/in-game-lists/weapon-ids";
|
||||
import type { FormFieldProps } from "../types";
|
||||
import { FormFieldWrapper } from "./FormFieldWrapper";
|
||||
|
||||
import styles from "./WeaponPoolFormField.module.css";
|
||||
|
||||
export type WeaponPoolItem = {
|
||||
id: MainWeaponId;
|
||||
isFavorite: boolean;
|
||||
};
|
||||
|
||||
type WeaponPoolFormFieldProps = FormFieldProps<"weapon-pool"> & {
|
||||
value: WeaponPoolItem[];
|
||||
onChange: (value: WeaponPoolItem[]) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function WeaponPoolFormField({
|
||||
name,
|
||||
label,
|
||||
bottomText,
|
||||
error,
|
||||
maxCount,
|
||||
disableSorting,
|
||||
disableFavorites,
|
||||
disableAltSkinDuplicates,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
disabled,
|
||||
}: WeaponPoolFormFieldProps) {
|
||||
const { t } = useTranslation(["forms"]);
|
||||
const id = React.useId();
|
||||
const isFull = value.length >= maxCount;
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const disabledWeaponIds = disableAltSkinDuplicates
|
||||
? value.flatMap((weapon) => weaponIdToArrayWithAlts(weapon.id))
|
||||
: value.map((weapon) => weapon.id);
|
||||
|
||||
const handleSelect = (weaponId: MainWeaponId) => {
|
||||
const newWeapon = {
|
||||
id: weaponId,
|
||||
isFavorite: false,
|
||||
};
|
||||
|
||||
const newValue = [...value, newWeapon];
|
||||
|
||||
if (disableSorting) {
|
||||
newValue.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
onChange(newValue);
|
||||
onBlur(newValue);
|
||||
};
|
||||
|
||||
const handleToggleFavorite = (weaponId: MainWeaponId) => {
|
||||
onChange(
|
||||
value.map((weapon) =>
|
||||
weapon.id === weaponId
|
||||
? { ...weapon, isFavorite: !weapon.isFavorite }
|
||||
: weapon,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemove = (weaponId: MainWeaponId) => {
|
||||
const newValue = value.filter((weapon) => weapon.id !== weaponId);
|
||||
onChange(newValue);
|
||||
onBlur(newValue);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = value.findIndex((weapon) => weapon.id === active.id);
|
||||
const newIndex = value.findIndex((weapon) => weapon.id === over.id);
|
||||
|
||||
const newValue = [...value];
|
||||
const [removed] = newValue.splice(oldIndex, 1);
|
||||
newValue.splice(newIndex, 0, removed);
|
||||
|
||||
onChange(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
const weaponList = (
|
||||
<ul className={styles.list}>
|
||||
{value.map((weapon) =>
|
||||
disableSorting || disabled ? (
|
||||
<StaticWeaponItem
|
||||
key={weapon.id}
|
||||
weapon={weapon}
|
||||
showFavoriteToggle={!disableFavorites}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onRemove={handleRemove}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<SortableWeaponItem
|
||||
key={weapon.id}
|
||||
weapon={weapon}
|
||||
showFavoriteToggle={!disableFavorites}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormFieldWrapper
|
||||
id={id}
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
bottomText={bottomText}
|
||||
>
|
||||
<WeaponSelect
|
||||
key={value.length}
|
||||
onChange={(weaponId) => {
|
||||
if (weaponId !== null) {
|
||||
handleSelect(weaponId);
|
||||
}
|
||||
}}
|
||||
disabledWeaponIds={disabledWeaponIds}
|
||||
clearable
|
||||
isDisabled={isFull || disabled}
|
||||
placeholder={
|
||||
isFull ? t("forms:placeholders.weaponPoolFull") : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{value.length > 0 ? (
|
||||
disableSorting ? (
|
||||
weaponList
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={value.map((weapon) => weapon.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{weaponList}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)
|
||||
) : null}
|
||||
</FormFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function StaticWeaponItem({
|
||||
weapon,
|
||||
showFavoriteToggle,
|
||||
onToggleFavorite,
|
||||
onRemove,
|
||||
disabled,
|
||||
}: {
|
||||
weapon: WeaponPoolItem;
|
||||
showFavoriteToggle: boolean;
|
||||
onToggleFavorite: (id: MainWeaponId) => void;
|
||||
onRemove: (id: MainWeaponId) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons"]);
|
||||
|
||||
return (
|
||||
<li className={styles.item}>
|
||||
<WeaponImage
|
||||
weaponSplId={weapon.id}
|
||||
variant={
|
||||
showFavoriteToggle && weapon.isFavorite ? "badge-5-star" : "badge"
|
||||
}
|
||||
size={32}
|
||||
className={styles.weaponImage}
|
||||
/>
|
||||
<span className={styles.weaponName}>
|
||||
{t(`weapons:MAIN_${weapon.id}`)}
|
||||
</span>
|
||||
<div className={styles.actions}>
|
||||
{showFavoriteToggle ? (
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
size="small"
|
||||
icon={
|
||||
weapon.isFavorite ? (
|
||||
<Star className={styles.starIconFilled} fill="currentColor" />
|
||||
) : (
|
||||
<Star className={styles.starIconOutlined} />
|
||||
)
|
||||
}
|
||||
aria-label="Toggle favorite"
|
||||
onPress={() => onToggleFavorite(weapon.id)}
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
) : null}
|
||||
<SendouButton
|
||||
variant="minimal-destructive"
|
||||
size="small"
|
||||
icon={<Trash />}
|
||||
aria-label="Delete"
|
||||
onPress={() => onRemove(weapon.id)}
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function SortableWeaponItem({
|
||||
weapon,
|
||||
showFavoriteToggle,
|
||||
onToggleFavorite,
|
||||
onRemove,
|
||||
}: {
|
||||
weapon: WeaponPoolItem;
|
||||
showFavoriteToggle: boolean;
|
||||
onToggleFavorite: (id: MainWeaponId) => void;
|
||||
onRemove: (id: MainWeaponId) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons"]);
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: weapon.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={clsx(styles.item, { [styles.isDragging]: isDragging })}
|
||||
{...attributes}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.dragHandle}
|
||||
aria-label="Drag to reorder"
|
||||
{...listeners}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<WeaponImage
|
||||
weaponSplId={weapon.id}
|
||||
variant={
|
||||
showFavoriteToggle && weapon.isFavorite ? "badge-5-star" : "badge"
|
||||
}
|
||||
size={32}
|
||||
className={styles.weaponImage}
|
||||
/>
|
||||
<span className={styles.weaponName}>
|
||||
{t(`weapons:MAIN_${weapon.id}`)}
|
||||
</span>
|
||||
<div className={styles.actions}>
|
||||
{showFavoriteToggle ? (
|
||||
<SendouButton
|
||||
variant="minimal"
|
||||
size="small"
|
||||
icon={
|
||||
weapon.isFavorite ? (
|
||||
<Star className={styles.starIconFilled} fill="currentColor" />
|
||||
) : (
|
||||
<Star className={styles.starIconOutlined} />
|
||||
)
|
||||
}
|
||||
aria-label="Toggle favorite"
|
||||
onPress={() => onToggleFavorite(weapon.id)}
|
||||
/>
|
||||
) : null}
|
||||
<SendouButton
|
||||
variant="minimal-destructive"
|
||||
size="small"
|
||||
icon={<Trash />}
|
||||
aria-label="Delete"
|
||||
onPress={() => onRemove(weapon.id)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user