Skip to content

Commit

Permalink
/cashier-v2 useReducer を使用して state 管理のリファクタリング (#178)
Browse files Browse the repository at this point in the history
Closes #176
  • Loading branch information
toririm authored Oct 1, 2024
1 parent 278ffcf commit b503406
Show file tree
Hide file tree
Showing 8 changed files with 372 additions and 192 deletions.
46 changes: 46 additions & 0 deletions app/components/molecules/AttractiveTextBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
type ChangeEventHandler,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { Input, type InputProps } from "../ui/input";

type props = InputProps & {
onTextSet: (text: string) => void;
focus: boolean;
};

// focus が true のときにフォーカスを当てるテキストボックス
const AttractiveTextBox = ({ focus, onTextSet, ...props }: props) => {
const [text, setText] = useState("");
const DOMRef = useRef<HTMLInputElement>(null);

const onChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
(event) => setText(event.target.value),
[],
);

useEffect(() => {
onTextSet(text);
}, [text, onTextSet]);

useEffect(() => {
if (focus) {
DOMRef.current?.focus();
}
}, [focus]);

return (
<Input
value={text}
onChange={onChangeHandler}
ref={DOMRef}
disabled={!focus}
{...props}
/>
);
};

export { AttractiveTextBox };
58 changes: 51 additions & 7 deletions app/components/organisms/DiscountInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,70 @@ import {
type ComponentPropsWithoutRef,
type ElementRef,
forwardRef,
useEffect,
useMemo,
useState,
} from "react";
import type { WithId } from "~/lib/typeguard";
import type { OrderEntity } from "~/models/order";
import { OrderEntity } from "~/models/order";
import { ThreeDigitsInput } from "../molecules/ThreeDigitsInput";

const findByOrderId = (
orders: WithId<OrderEntity>[] | undefined,
orderId: number,
): WithId<OrderEntity> | undefined => {
return orders?.find((order) => order.orderId === orderId);
};

// 割引券番号を入力するためのコンポーネント
const DiscountInput = forwardRef<
ElementRef<typeof ThreeDigitsInput>,
ComponentPropsWithoutRef<typeof ThreeDigitsInput> & {
discountOrder: WithId<OrderEntity> | undefined;
lastPurchasedCups: number;
orders: WithId<OrderEntity>[] | undefined;
onDiscountOrderFind: (discountOrder: WithId<OrderEntity>) => void;
onDiscountOrderRemoved: () => void;
}
>(({ discountOrder, lastPurchasedCups, ...props }, ref) => {
>(({ orders, onDiscountOrderFind, onDiscountOrderRemoved, ...props }, ref) => {
const [discountOrderId, setDiscountOrderId] = useState("");

const isComplete = useMemo(
() => discountOrderId.length === 3,
[discountOrderId],
);

const discountOrder = useMemo(() => {
if (!isComplete) return;
const discountOrderIdNum = Number(discountOrderId);
return findByOrderId(orders, discountOrderIdNum);
}, [orders, isComplete, discountOrderId]);

const lastPurchasedCups = useMemo(
() => discountOrder?.getCoffeeCount() ?? 0,
[discountOrder],
);

useEffect(() => {
if (isComplete && discountOrder) {
onDiscountOrderFind(discountOrder);
}
return onDiscountOrderRemoved;
}, [isComplete, discountOrder, onDiscountOrderFind, onDiscountOrderRemoved]);

return (
<div>
<p>割引券番号</p>
<ThreeDigitsInput ref={ref} {...props} />
<ThreeDigitsInput
ref={ref}
value={discountOrderId}
onChange={(value) => setDiscountOrderId(value)}
{...props}
/>
<p>
{discountOrder === undefined ? "見つかりません" : null}
{discountOrder && `有効杯数: ${lastPurchasedCups}`}
{!isComplete && "3桁の割引券番号を入力してください"}
{isComplete &&
(discountOrder instanceof OrderEntity
? `有効杯数: ${lastPurchasedCups}`
: "見つかりません")}
</p>
</div>
);
Expand Down
52 changes: 24 additions & 28 deletions app/components/organisms/ItemAssign.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
type Dispatch,
type SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { WithId } from "~/lib/typeguard";
import { cn } from "~/lib/utils";
import { type ItemEntity, type2label } from "~/models/item";
Expand All @@ -14,46 +7,49 @@ import { Input } from "../ui/input";
type props = {
item: WithId<ItemEntity>;
idx: number;
setOrderItems: Dispatch<SetStateAction<WithId<ItemEntity>[]>>;
mutateItem: (
idx: number,
action: (prev: WithId<ItemEntity>) => WithId<ItemEntity>,
) => void;
focus: boolean;
};

const ItemAssign = ({ item, idx, setOrderItems, focus }: props) => {
const [edit, setEdit] = useState(false);
const ItemAssign = ({ item, idx, mutateItem, focus }: props) => {
const [editable, setEditable] = useState(false);
const [assignee, setAssinee] = useState<string | null>(null);

const assignInputRef = useRef<HTMLInputElement>(null);

const closeAssignInput = useCallback(() => {
setOrderItems((prevItems) => {
const newItems = [...prevItems];
newItems[idx].assignee = assignee;
return newItems;
mutateItem(idx, (prev) => {
const copy = structuredClone(prev);
copy.assignee = assignee;
return copy;
});
setEdit(false);
}, [idx, assignee, setOrderItems]);
setEditable(false);
}, [assignee, idx, mutateItem]);

// edit の状態に応じて assign 入力欄を開くか閉じる
const change = useCallback(() => {
if (edit) {
const switchEditable = useCallback(() => {
if (editable) {
closeAssignInput();
} else {
setEdit(true);
setEditable(true);
}
}, [edit, closeAssignInput]);
}, [editable, closeAssignInput]);

// focus が変化したときに assign 入力欄を閉じる
useEffect(() => {
if (!focus) {
if (!focus && editable) {
closeAssignInput();
}
}, [focus, closeAssignInput]);
}, [focus, editable, closeAssignInput]);

// Enter が押されたときに assign 入力欄を開く
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === "Enter") {
change();
switchEditable();
}
};
if (focus) {
Expand All @@ -62,14 +58,14 @@ const ItemAssign = ({ item, idx, setOrderItems, focus }: props) => {
return () => {
window.removeEventListener("keydown", handler);
};
}, [focus, change]);
}, [focus, switchEditable]);

// edit が true に変化したとき assign 入力欄にフォーカスする
useEffect(() => {
if (edit) {
if (editable) {
assignInputRef.current?.focus();
}
}, [edit]);
}, [editable]);

return (
<div className={cn("grid grid-cols-2", focus && "bg-orange-500")}>
Expand All @@ -78,7 +74,7 @@ const ItemAssign = ({ item, idx, setOrderItems, focus }: props) => {
<p>{item.name}</p>
<p>{item.price}</p>
<p>{type2label[item.type]}</p>
{edit ? (
{editable ? (
<Input
ref={assignInputRef}
value={assignee ?? ""}
Expand Down
78 changes: 69 additions & 9 deletions app/components/organisms/OrderItemView.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,88 @@
import { useCallback, useEffect, useState } from "react";
import type { WithId } from "~/lib/typeguard";
import type { ItemEntity } from "~/models/item";
import type { OrderEntity } from "~/models/order";
import { ItemAssign } from "./ItemAssign";

const keys = ["a", "s", "d", "f", "g", "h", "j", "k", "l", ";"];

type props = {
order: OrderEntity;
setOrderItems: React.Dispatch<React.SetStateAction<WithId<ItemEntity>[]>>;
inputStatus: "items" | "discount" | "received" | "description" | "submit";
itemFocus: number;
setItemFocus: React.Dispatch<React.SetStateAction<number>>;
items: WithId<ItemEntity>[] | undefined;
focus: boolean;
onAddItem: (item: WithId<ItemEntity>) => void;
mutateItem: (
idx: number,
action: (prev: WithId<ItemEntity>) => WithId<ItemEntity>,
) => void;
discountOrder: boolean;
};

// オーダーのアイテムや割引情報を表示するコンポーネント
const OrderItemView = ({
inputStatus,
focus,
discountOrder,
setOrderItems,
itemFocus,
onAddItem,
mutateItem,
order,
items,
}: props) => {
const [itemFocus, setItemFocus] = useState<number>(0);

const proceedItemFocus = useCallback(() => {
setItemFocus((prev) => (prev + 1) % order.items.length);
}, [order.items]);

const prevousItemFocus = useCallback(() => {
setItemFocus(
(prev) => (prev - 1 + order.items.length) % order.items.length,
);
}, [order.items]);

useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (!focus) {
return;
}
if (event.key === "ArrowUp") {
prevousItemFocus();
}
if (event.key === "ArrowDown") {
proceedItemFocus();
}
};
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}, [proceedItemFocus, prevousItemFocus, focus]);

useEffect(() => {
const handlers = items?.map((item, idx) => {
const handler = (event: KeyboardEvent) => {
if (!focus) {
return;
}
if (event.key === keys[idx]) {
onAddItem(item);
}
};
return handler;
});
for (const handler of handlers ?? []) {
window.addEventListener("keydown", handler);
}

return () => {
for (const handler of handlers ?? []) {
window.removeEventListener("keydown", handler);
}
};
}, [items, focus, onAddItem]);

return (
<>
{inputStatus === "items" && (
{focus && (
<>
<p>商品を追加: キーボードの a, s, d, f, g, h, j, k, l, ;</p>
<p>↑・↓でアイテムのフォーカスを移動</p>
Expand All @@ -34,7 +94,7 @@ const OrderItemView = ({
key={`${idx}-${item.id}`}
item={item}
idx={idx}
setOrderItems={setOrderItems}
mutateItem={mutateItem}
focus={idx === itemFocus}
/>
))}
Expand Down
Loading

0 comments on commit b503406

Please sign in to comment.