Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 簡易レジのロジックを実装 #47

Merged
merged 20 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a11f8bb
refactor: Productの作成・更新に使うフォームのコンポーネントを作成
r4ai Nov 18, 2024
675f921
fix: デザインを既存のものに合わせる
r4ai Nov 18, 2024
1583f60
feat: 商品登録フォームを作成
r4ai Nov 18, 2024
f3faae3
fix: remove default values
r4ai Nov 18, 2024
f54456e
fix: @tanstack/formが上手く動かなかったためconformに移行
r4ai Nov 18, 2024
8a2232f
fix: フォーム提出成功時にフォームをリセットする
r4ai Nov 18, 2024
b6a3720
feat: 商品一覧を追加
r4ai Nov 18, 2024
0c27096
fix: 削除・更新モーダルを修正
r4ai Nov 19, 2024
2be5d0d
refactor
r4ai Nov 19, 2024
ce86702
feat: `/register`で作成した商品登録フォームを表示するよう変更
r4ai Nov 19, 2024
085941b
refactor
r4ai Nov 19, 2024
5f17a81
feat: 計算のロジックを追加gs
r4ai Nov 19, 2024
255812c
fix: フォントサイズにより少しレイアウトが崩れていたのを修正
r4ai Nov 19, 2024
bd5a720
feat: 電卓を実装
r4ai Nov 19, 2024
718755c
fix: 2桁以上の数値を入力した際に壊れる問題を修正
r4ai Nov 19, 2024
f8c7b1c
chore: Lintエラーを解決するために、TypeScriptではreact/prop-typesルールを無効化
r4ai Nov 19, 2024
67ff14a
refactor: remove dead code
r4ai Nov 19, 2024
2d0a1f3
Merge branch 'main' into feature/issue-31-calculator-logic
r4ai Nov 19, 2024
7688ed6
refactor: ファイル名をより一般的な名称に変更
r4ai Nov 19, 2024
861be59
Merge branch 'main' into feature/issue-31-calculator-logic
r4ai Nov 20, 2024
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
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ module.exports = {
"plugin:import/recommended",
"plugin:import/typescript",
],
rules: {
"react/prop-types": "off",
},
},

// Node
Expand Down
4 changes: 4 additions & 0 deletions app/components/atoms/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { chakra } from "@chakra-ui/react"
import { Form as RemixForm } from "@remix-run/react"

export const Form = chakra(RemixForm)
19 changes: 19 additions & 0 deletions app/components/molecules/FieldInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FormErrorMessage, FormHelperText } from "@chakra-ui/react"
import { FC, ReactNode } from "react"

export type FieldInfoProps = {
errors?: string[] | undefined
helperText?: ReactNode
}

export const FieldInfo: FC<FieldInfoProps> = ({ errors, helperText }) => {
return (
<>
{errors ? (
<FormErrorMessage>{errors.join(",")}</FormErrorMessage>
) : (
<FormHelperText>{helperText}</FormHelperText>
)}
</>
)
}
88 changes: 55 additions & 33 deletions app/components/organisms/reception/Calculator.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,67 @@
import { Box, Button, HStack, Text, VStack } from "@chakra-ui/react"
import { Button, Grid, Text, Tooltip, VStack } from "@chakra-ui/react"
import { FC, memo } from "react"
import { useTokens } from "~/hooks/useToken"
import { renderToken } from "~/lib/calculate"

export type CalculatorProps = {
total: number
}

export const Calculator: FC<CalculatorProps> = memo(({ total }) => {
const { onInput, clear, tokens, input, calculate } = useTokens()

export const Calculator: FC = memo(() => {
return (
<Box
w="300px"
h="250px"
<VStack
w="fit-content"
bg="blackAlpha.500"
borderRadius="10px"
shadow="lg"
p={4}
gap={4}
>
<VStack>
<Text></Text>
<HStack>
<Button>7</Button>
<Button>8</Button>
<Button>9</Button>
<Button>*</Button>
</HStack>
<HStack>
<Button>4</Button>
<Button>5</Button>
<Button>6</Button>
<Button>-</Button>
</HStack>
<HStack>
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>+</Button>
</HStack>
<HStack>
<Button>0</Button>
<Button>T</Button>
<Button>C</Button>
<Button>=</Button>
</HStack>
<VStack
gap={0}
py={2}
px={4}
minH={20}
justifyContent="center"
alignItems="end"
bg="InfoBackground"
rounded="md"
width="full"
>
<Text color="GrayText">{tokens.map(renderToken).join(" ")}</Text>
<Text color="InfoText" fontSize="xl">
{input}
</Text>
</VStack>
</Box>
<Grid
w="fit-content"
gap={4}
templateColumns="repeat(4, 1fr)"
placeItems="center"
placeContent="center"
>
<Button onClick={() => onInput(7)}>7</Button>
<Button onClick={() => onInput(8)}>8</Button>
<Button onClick={() => onInput(9)}>9</Button>
<Button onClick={() => onInput("*")}>×</Button>
<Button onClick={() => onInput(4)}>4</Button>
<Button onClick={() => onInput(5)}>5</Button>
<Button onClick={() => onInput(6)}>6</Button>
<Button onClick={() => onInput("-")}>−</Button>
<Button onClick={() => onInput(1)}>1</Button>
<Button onClick={() => onInput(2)}>2</Button>
<Button onClick={() => onInput(3)}>3</Button>
<Button onClick={() => onInput("+")}>+</Button>
<Button onClick={() => onInput(0)}>0</Button>
<Tooltip hasArrow label="合計金額">
<Button onClick={() => onInput(total)}>T</Button>
</Tooltip>
<Button onClick={() => clear()}>C</Button>
<Button onClick={() => calculate()}>=</Button>
</Grid>
</VStack>
)
})

Calculator.displayName = "Calculator"
10 changes: 7 additions & 3 deletions app/components/organisms/register/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Button, Stack, Text } from "@chakra-ui/react"
import { Box, Button, Image, Stack, Text } from "@chakra-ui/react"
import { FC, memo } from "react"
import { TypeProduct } from "~/type/typeproduct"
import PropTypes from "prop-types"
Expand All @@ -18,7 +18,7 @@ export const ProductCard: FC<Props> = memo((props) => {
maxW="300px"
h="fit-content"
bg={"white"}
borderRadius="10px"
borderRadius="lg"
shadow="md"
p={4}
>
Expand All @@ -27,7 +27,11 @@ export const ProductCard: FC<Props> = memo((props) => {
<Text>{product.product_name}</Text>
<Text>価格:{product.price}</Text>
<Text>在庫:{product.stock}</Text>
<img src={product.image} alt={`${product.product_name}の商品画像`} />
<Image
src={product.image}
alt={`${product.product_name}の商品画像`}
borderRadius="md"
/>
<Button onClick={() => clickDelete(product)} colorScheme="red">
削除
</Button>
Expand Down
156 changes: 156 additions & 0 deletions app/components/organisms/register/ProductForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Form } from "~/components/atoms/Form"
import { type FC } from "react"
import { TypeProduct } from "~/type/typeproduct"
import { Button, FormControl, FormLabel, Input, VStack } from "@chakra-ui/react"
import { FieldInfo } from "~/components/molecules/FieldInfo"
import * as v from "valibot"
import { getValibotConstraint, parseWithValibot } from "conform-to-valibot"
import {
getFormProps,
getInputProps,
SubmissionResult,
useForm,
} from "@conform-to/react"
import { useNavigation } from "@remix-run/react"

export type ProductFormValues = (
| Omit<TypeProduct, "product_id">
| TypeProduct
) & {
_method: "create" | "update"
}

export const createProductSchema = v.object({
_method: v.literal("create"),
product_name: v.pipe(
v.string("商品名は文字列で入力してください"),
v.minLength(1, "商品名は1文字以上で入力してください"),
v.maxLength(255, "商品名は255文字以下で入力してください"),
),
price: v.pipe(
v.number("価格は数字で入力してください"),
v.integer("価格は整数で入力してください"),
v.minValue(0, "価格は0以上で入力してください"),
v.maxValue(
Number.MAX_SAFE_INTEGER,
`価格は${Number.MAX_SAFE_INTEGER}以下で入力してください`,
),
),
stock: v.pipe(
v.number("在庫数は数字で入力してください"),
v.integer("在庫数は整数で入力してください"),
v.minValue(0, "在庫数は0以上で入力してください"),
v.maxValue(
Number.MAX_SAFE_INTEGER,
`在庫数は${Number.MAX_SAFE_INTEGER}以下で入力してください`,
),
),
image: v.pipe(
v.string("商品画像は文字列で入力してください"),
v.url("商品画像は画像URLで入力してください"),
),
})

export const updateProductSchema = v.object({
...createProductSchema.entries,
_method: v.literal("update"),
product_id: v.pipe(
v.number("製品IDは数字で入力してください"),
v.integer("製品IDは整数で入力してください"),
v.minValue(0, "製品IDは0以上で入力してください"),
v.maxValue(
Number.MAX_SAFE_INTEGER,
`製品IDは${Number.MAX_SAFE_INTEGER}以下で入力してください`,
),
),
})

export type ProductFormProps = {
_method: "create" | "update"
defaultValue?: Partial<ProductFormValues> | null | undefined
submitText: string
lastResult?: SubmissionResult<string[]> | null | undefined
}

export const ProductForm: FC<ProductFormProps> = ({
submitText,
lastResult,
_method,
defaultValue,
}) => {
const navigation = useNavigation()
const schema =
_method === "update" ? updateProductSchema : createProductSchema
const [form, fields] = useForm<ProductFormValues>({
defaultValue: {
_method,
...defaultValue,
},
constraint: getValibotConstraint(schema),
lastResult: navigation.state === "idle" ? lastResult : null,
shouldValidate: "onBlur",
shouldRevalidate: "onInput",
onValidate: ({ formData }) => parseWithValibot(formData, { schema }),
})

return (
<Form {...getFormProps(form)} method="post" w="full" noValidate>
<VStack gap={4}>
<Input
{...getInputProps(fields._method, { type: "text" })}
key={fields.product_id.key}
hidden
/>
{_method === "update" && (
<Input
{...getInputProps(fields.product_id, { type: "number" })}
key={fields.product_id.key}
hidden
/>
)}
<FormControl isInvalid={!fields.product_name.valid}>
<FormLabel>商品名</FormLabel>
<Input
{...getInputProps(fields.product_name, { type: "text" })}
key={fields.product_name.key}
placeholder="焼きそば"
/>
<FieldInfo errors={fields.product_name.errors} />
</FormControl>
<FormControl isInvalid={!fields.price.valid}>
<FormLabel>価格</FormLabel>
<Input
{...getInputProps(fields.price, { type: "number" })}
key={fields.price.key}
placeholder="500"
/>
<FieldInfo errors={fields.price.errors} />
</FormControl>
<FormControl isInvalid={!fields.stock.valid}>
<FormLabel>在庫</FormLabel>
<Input
{...getInputProps(fields.stock, { type: "number" })}
key={fields.stock.key}
placeholder="100"
/>
<FieldInfo errors={fields.stock.errors} />
</FormControl>
<FormControl isInvalid={!fields.image.valid}>
<FormLabel>商品画像</FormLabel>
<Input
{...getInputProps(fields.image, { type: "text" })}
key={fields.image.key}
placeholder="https://example.com/image.jpg"
/>
<FieldInfo
errors={fields.image.errors}
helperText="商品画像は画像URLで入力してください"
/>
</FormControl>
<Button type="submit" alignSelf="end">
{submitText}
</Button>
</VStack>
</Form>
)
}
46 changes: 46 additions & 0 deletions app/hooks/useToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react"
import { calculate as calculateTokens, Token } from "~/lib/calculate"

export const useTokens = () => {
const [tokens, setTokens] = useState<Token[]>([])
const [input, setInput] = useState<number>(0)

const pushToken = (token: Token) => setTokens((prev) => [...prev, token])
const popToken = () => setTokens((prev) => prev.slice(0, -1))
const clearTokens = () => setTokens([])

const onInput = (token: Token) => {
if (typeof token === "number") {
setInput((prev) => Number.parseInt(prev.toString() + token.toString()))
} else {
pushToken(input)
setInput(0)
pushToken(token)
}
}

const clear = () => {
setInput(0)
clearTokens()
}

const calculate = () => {
const output = calculateTokens([...tokens, input])
clearTokens()
setInput(output)
return output
}

return {
tokens,
setTokens,
pushToken,
popToken,
clearTokens,
input,
setInput,
onInput,
clear,
calculate,
} as const
}
Loading