Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into issue/145
Browse files Browse the repository at this point in the history
  • Loading branch information
Lailai0477 committed Oct 4, 2024
2 parents 7bffc53 + 439fad9 commit 47f563f
Show file tree
Hide file tree
Showing 36 changed files with 1,630 additions and 354 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-assign.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ permissions:

jobs:
assign:
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' || toJSON(github.event.pull_request.assignees) == '[]' }}
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'renovate[bot]' && toJSON(github.event.pull_request.assignees) == '[]' }}
runs-on: ubuntu-latest
timeout-minutes: 1
steps:
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"editor.formatOnSaveMode": "file",
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit",
"source.fixAll.biome": "always",
"source.addMissingImports.ts": "explicit"
},
"editor.tabSize": 2,
Expand Down
20 changes: 20 additions & 0 deletions app/components/functional/useFocusRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useRef } from "react";

/**
* focus が true に変化した際に ref が指す DOM にフォーカスを当てる
* @param focus フォーカスを当てるかどうか
* @returns
*/
const useFocusRef = (focus: boolean) => {
const DOMRef = useRef<HTMLInputElement>(null);

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

return DOMRef;
};

export { useFocusRef };
40 changes: 40 additions & 0 deletions app/components/functional/useInputStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useMemo, useState } from "react";

const InputStatusList = [
"discount",
"items",
"received",
"description",
"submit",
] as const;

const inputLen = InputStatusList.length;

const narrowInLen = (idx: number) => Math.min(Math.max(idx, 0), inputLen - 1);

/**
* CashierV2 のドメイン固有のフック
*
* 入力ステータスを管理する
*/
const useInputStatus = () => {
const [idx, setIdx] = useState(0);

const proceedStatus = useCallback(() => {
setIdx((prev) => narrowInLen(prev + 1));
}, []);

const previousStatus = useCallback(() => {
setIdx((prev) => narrowInLen(prev - 1));
}, []);

const inputStatus = useMemo(() => InputStatusList[idx], [idx]);

const resetStatus = useCallback(() => {
setIdx(0);
}, []);

return { inputStatus, proceedStatus, previousStatus, resetStatus };
};

export { useInputStatus };
19 changes: 19 additions & 0 deletions app/components/functional/useLatestOrderId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMemo } from "react";
import type { WithId } from "~/lib/typeguard";
import type { OrderEntity } from "~/models/order";

/**
* オーダーのIDの最大値と次のIDを取得する
* @param orders オーダーのリスト
* @returns オーダーIDの最大値と次のID
*/
const useLatestOrderId = (orders: WithId<OrderEntity>[] | undefined) => {
const latestOrderId = useMemo(
() => orders?.reduce((acc, cur) => Math.max(acc, cur.orderId), 0) ?? 0,
[orders],
);
const nextOrderId = useMemo(() => latestOrderId + 1, [latestOrderId]);

return { latestOrderId, nextOrderId };
};
export { useLatestOrderId };
142 changes: 142 additions & 0 deletions app/components/functional/useOrderState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { useReducer } from "react";
import type { WithId } from "~/lib/typeguard";
import type { ItemEntity } from "~/models/item";
import { OrderEntity } from "~/models/order";

type BaseAction<TypeName extends string> = { type: TypeName };
type Action<
TypeName extends string,
U extends Record<string, unknown> = Record<never, never>,
> = BaseAction<TypeName> & U;

type Clear = Action<"clear", { effectFn?: () => void }>;
type UpdateOrderId = Action<"updateOrderId", { orderId: number }>;
type AddItem = Action<"addItem", { item: WithId<ItemEntity> }>;
type RemoveItem = Action<"removeItem", { idx: number }>;
type MutateItem = Action<
"mutateItem",
{ idx: number; action: (prev: WithId<ItemEntity>) => WithId<ItemEntity> }
>;
type ApplyDiscount = Action<
"applyDiscount",
{ discountOrder: WithId<OrderEntity> }
>;
type RemoveDiscount = Action<"removeDiscount">;
type SetReceived = Action<"setReceived", { received: string }>;
type SetDescription = Action<"setDescription", { description: string }>;
/**
* オーダーの状態を更新するためのアクション型
*/
export type OrderAction =
| Clear
| UpdateOrderId
| AddItem
| RemoveItem
| MutateItem
| ApplyDiscount
| RemoveDiscount
| SetReceived
| SetDescription;

type OrderReducer<T extends OrderAction> = (
state: OrderEntity,
action: T,
) => OrderEntity;

const clear: OrderReducer<Clear> = (state, action) => {
const effectFn = action.effectFn;
if (effectFn) {
effectFn();
}
return OrderEntity.createNew({ orderId: state.orderId });
};

const updateOrderId: OrderReducer<UpdateOrderId> = (state, action) => {
const updated = state.clone();
updated.orderId = action.orderId;
return updated;
};

const addItem: OrderReducer<AddItem> = (state, action) => {
const updated = state.clone();
updated.items = [...updated.items, action.item];
return updated;
};

const removeItem: OrderReducer<RemoveItem> = (state, action) => {
const updated = state.clone();
updated.items = updated.items.filter((_, idx) => idx !== action.idx);
return updated;
};

const mutateItem: OrderReducer<MutateItem> = (state, action) => {
const updated = state.clone();
updated.items[action.idx] = action.action(updated.items[action.idx]);
return updated;
};

const applyDiscount: OrderReducer<ApplyDiscount> = (state, action) => {
const updated = state.clone();
updated.applyDiscount(action.discountOrder);
return updated;
};

const removeDiscount: OrderReducer<RemoveDiscount> = (state, action) => {
const updated = state.clone();
updated.removeDiscount();
return updated;
};

const setReceived: OrderReducer<SetReceived> = (state, action) => {
const updated = state.clone();
updated.received = Number(action.received);
return updated;
};

const setDescription: OrderReducer<SetDescription> = (state, action) => {
const updated = state.clone();
updated.description = action.description;
return updated;
};

const reducer: OrderReducer<OrderAction> = (state, action): OrderEntity => {
switch (action.type) {
case "clear":
return clear(state, action);
case "applyDiscount":
return applyDiscount(state, action);
case "removeDiscount":
return removeDiscount(state, action);
case "addItem":
return addItem(state, action);
case "removeItem":
return removeItem(state, action);
case "mutateItem":
return mutateItem(state, action);
case "setReceived":
return setReceived(state, action);
case "setDescription":
return setDescription(state, action);
case "updateOrderId":
return updateOrderId(state, action);
}
};

/**
* オーダーの状態を管理する
*
* reducer が受け付ける状態には下記がある:
* - clear
* - applyDiscount
* - removeDiscount
* - addItem
* - mutateItem
* - setReceived
* - setDescription
* - updateOrderId
* @returns オーダーの状態とそれを更新する関数
*/
const useOrderState = () =>
useReducer(reducer, OrderEntity.createNew({ orderId: -1 }));

export { useOrderState };
23 changes: 23 additions & 0 deletions app/components/functional/usePreventNumberKeyUpDown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from "react";

/**
* 上下キーで数値を増減させないEffect
*/
const usePreventNumberKeyUpDown = () => {
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
// 上下キーで数値を増減させない
event.preventDefault();
}
};
window.addEventListener("keydown", handler);
window.addEventListener("keyup", handler);
return () => {
window.removeEventListener("keydown", handler);
window.removeEventListener("keyup", handler);
};
}, []);
};

export { usePreventNumberKeyUpDown };
32 changes: 32 additions & 0 deletions app/components/functional/useUISession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useCallback, useMemo, useState } from "react";

type UISession = {
date: Date;
key: string;
};

/**
* UI のセッションを管理するためのフック
*
* renewUISession を呼ぶことでセッションを更新できる
*
* UISession.key を DOM の key に指定することで、セッションが変更されたときに再描画される
*/
const useUISession = (): [UISession, () => void] => {
const [date, setDate] = useState(new Date());

const UISession = useMemo(() => {
return {
date,
key: date.toJSON(),
};
}, [date]);

const renewUISession = useCallback(() => {
setDate(new Date());
}, []);

return [UISession, renewUISession];
};

export { useUISession };
42 changes: 42 additions & 0 deletions app/components/molecules/AttractiveTextBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
type ChangeEventHandler,
useCallback,
useEffect,
useState,
} from "react";
import { useFocusRef } from "../functional/useFocusRef";
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 = useFocusRef(focus);

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

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

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

export { AttractiveTextBox };
30 changes: 30 additions & 0 deletions app/components/molecules/ThreeDigitsInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { REGEXP_ONLY_DIGITS } from "input-otp";
import {
type ComponentPropsWithoutRef,
type ElementRef,
forwardRef,
} from "react";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../ui/input-otp";

/**
* 3桁の数字を入力するためのコンポーネント
*/
const ThreeDigitsInput = forwardRef<
ElementRef<typeof InputOTP>,
Omit<
ComponentPropsWithoutRef<typeof InputOTP>,
"maxLength" | "pattern" | "render"
>
>(({ ...props }, ref) => {
return (
<InputOTP ref={ref} maxLength={3} pattern={REGEXP_ONLY_DIGITS} {...props}>
<InputOTPGroup>
<InputOTPSlot index={0} className="font-mono text-3xl" />
<InputOTPSlot index={1} className="font-mono text-3xl" />
<InputOTPSlot index={2} className="font-mono text-3xl" />
</InputOTPGroup>
</InputOTP>
);
});

export { ThreeDigitsInput };
Loading

0 comments on commit 47f563f

Please sign in to comment.