Skip to content

Commit

Permalink
Merge pull request #13 from codeforjapan/wip/ui
Browse files Browse the repository at this point in the history
UI プロトタイプ実装
  • Loading branch information
yu23ki14 authored Feb 12, 2025
2 parents 09cf14f + 7291478 commit ce15d14
Show file tree
Hide file tree
Showing 61 changed files with 5,110 additions and 1,711 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

defaults:
run:
shell: bash

permissions:
contents: read

jobs:
ci:
name: Code Problem Check
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Setup Node.js and yarn
uses: ./.github/workflows/composite/setup

- name: Build
run: yarn run build

- name: Run ESLint
run: yarn run lint

- name: Run Prettier
run: yarn run format:ci

- name: Run typecheck
run: yarn run typecheck

- name: Run Vitest
run: yarn run test
39 changes: 39 additions & 0 deletions .github/workflows/composite/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Setup Node.js
description: Setup Node.js and yarn

runs:
using: composite
steps:
- name: Setup yarn
shell: bash
run: corepack enable yarn

- name: Get yarn cache directory path
id: yarn-store
shell: bash
run: echo "store_path=$(yarn cache dir)" >> "$GITHUB_OUTPUT"

- name: Setup Node.js
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version-file: package.json

- name: Restore yarn cache
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ${{ steps.yarn-store.outputs.store_path }}
key: ${{ runner.os }}-yarn-store-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-store-
- name: Install Dependencies
shell: bash
run: yarn install --frozen-lockfile

- name: Save pnpm cache if main branch
if: github.ref_name == 'main'
id: save-yarn-cache
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ${{ steps.yarn-store.outputs.store_path }}
key: ${{ runner.os }}-yarn-store-${{ hashFiles('**/yarn.lock') }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules
/.cache
/build
.env

.vscode/*
!.vscode/settings.example.json
eslint-typegen.d.ts
15 changes: 15 additions & 0 deletions .vscode/settings.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
13 changes: 13 additions & 0 deletions app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import "tailwindcss";

@utility content-visibility-auto {
content-visibility: auto;
}

body {
color: #222;
}

* {
letter-spacing: 0.05em;
}
31 changes: 31 additions & 0 deletions app/components/FormError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { MantineSize } from "@mantine/core";
import { List, ListItem } from "@mantine/core";
import { useMemo } from "react";

type FormErrorProps = {
errors: Array<string[] | null | undefined> | string[] | null | undefined;
/**
* @default "xs"
*/
size?: MantineSize;
};

export const FormError = ({ errors, size }: FormErrorProps) => {
size ??= "xs";

const flattenErrors = useMemo(
() => errors?.flat().filter((v) => v != null),
[errors],
);
if (flattenErrors == null || flattenErrors.length === 0) {
return undefined;
}

return (
<List listStyleType="none" size={size}>
{flattenErrors.map((error, index) => (
<ListItem key={index}>{error}</ListItem>
))}
</List>
);
};
17 changes: 17 additions & 0 deletions app/components/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ButtonProps, PolymorphicComponentProps } from "@mantine/core";
import { Button } from "@mantine/core";
import type React from "react";

type SubmitButtonProps = Omit<
PolymorphicComponentProps<"button", ButtonProps>,
"type"
> & {
children: React.ReactNode;
loading?: boolean;
};

export const SubmitButton = ({ disabled, ...rest }: SubmitButtonProps) => {
// React19 へアップデートしたら useFormStatus() を追加して送信中はボタンを無効化することを検討する

return <Button disabled={disabled} type="submit" {...rest} />;
};
95 changes: 95 additions & 0 deletions app/components/input/DateRangePicker.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { getFormProps, useForm } from "@conform-to/react";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

import { render, screen } from "../../../test/test-react";
import { userEvent } from "../../../test/vitest-setup";
import { DateRangePicker } from "./DateRangePicker";

type Form = {
/**
* ISO String
*/
start: string | null;
/**
* ISO String
*/
end: string | null;
};

type PageProps = {
defaultValue: Form;
};

beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
// GitHub Actions では TZ が UTC になっているため、テスト時の TZ も UTC に設定する
process.env.TZ = "UTC";
});

afterEach(() => {
vi.useRealTimers();
});

const Page = ({ defaultValue }: PageProps) => {
const [form, fields] = useForm<Form>({
defaultValue,
});

return (
<div>
<form {...getFormProps(form)}>
<DateRangePicker
convertFormValueToMantine={(value) =>
value ? new Date(value) : null
}
convertMantineValueToForm={(date) => date?.toISOString()}
fromField={fields.start}
label="Date Range"
toField={fields.end}
/>
<button type="submit">Submit</button>
</form>
<span aria-label="result">
{fields.start.value}{fields.end.value}
</span>
</div>
);
};

describe("DateRangePicker", () => {
test("convert で指定した処理を用いて UI の値をフォームに反映できる", async () => {
// 確実に 2025 年 1 月のカレンダーを表示するため、システム時刻を固定する
vi.setSystemTime(new Date("2025-01-15T00:00:00Z"));
render(<Page defaultValue={{ start: null, end: null }} />);

const button = screen.getByRole("button", { name: "Date Range" });
await userEvent.click(button);

// start: 2025-01-09T15:00:00.000Z
const date1 = screen.getByRole("button", { name: "10 1月 2025" });
await userEvent.click(date1);

// end: 2025-01-14T15:00:00.000Z
const date2 = screen.getByRole("button", { name: "15 1月 2025" });
await userEvent.click(date2);

const span = screen.getByLabelText("result");
expect(span).toHaveTextContent(
"2025-01-10T00:00:00.000Z – 2025-01-15T00:00:00.000Z",
);
});

test("convert で指定した処理を用いてフォームの値を UI に反映できる", () => {
render(
<Page
defaultValue={{
start: "2025-01-09T15:00:00.000Z",
end: "2025-01-14T15:00:00.000Z",
}}
/>,
);

const button = screen.getByRole("button", { name: "Date Range" });
expect(button).toHaveTextContent("2025.01.09 (木) – 2025.01.14 (火)");
});
});
62 changes: 62 additions & 0 deletions app/components/input/DateRangePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DatePickerInput } from "@mantine/dates";

import { useDateRangeInputControl } from "../../hooks/useDateRangeInputControl";
import { containsNonNullValues } from "../../utils/array";
import { FormError } from "../FormError";

type DateRangePickerProps = Parameters<typeof useDateRangeInputControl>[0] & {
disabled?: boolean;
label: string;
/**
* 入力した値を表示する際のフォーマット
*
* @default "YYYY.MM.DD (ddd)"
*
* @see {@link https://day.js.org/docs/en/display/format Day.js のフォーマット仕様}
*/
valueFormat?: string;
};

export const DateRangePicker = ({
disabled,
toField,
fromField,
label,
valueFormat,
convertMantineValueToForm,
convertFormValueToMantine,
}: DateRangePickerProps) => {
valueFormat ??= "YYYY.MM.DD (ddd)";

const {
value,
change: onChange,
focus: onFocus,
blur: onBlur,
} = useDateRangeInputControl({
fromField,
toField,
convertMantineValueToForm,
convertFormValueToMantine,
});

return (
<DatePickerInput
clearable
disabled={disabled}
error={
containsNonNullValues(fromField.errors, toField.errors) && (
<FormError errors={[fromField.errors, toField.errors]} />
)
}
errorProps={{ component: "div" }}
label={label}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}
type="range"
value={value}
valueFormat={valueFormat}
/>
);
};
11 changes: 11 additions & 0 deletions app/components/mantine/Fieldset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import "./fieldset.css";

import type { FieldsetProps as MantineFieldsetProps } from "@mantine/core";
// このファイルは no-restricted-imports で提案される代替コンポーネントなので問題ない
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { Fieldset as MantineFieldSet } from "@mantine/core";
type FieldSetProps = Omit<MantineFieldsetProps, "variant">;

export const Fieldset = (props: FieldSetProps) => {
return <MantineFieldSet variant="unstyled" {...props} />;
};
21 changes: 21 additions & 0 deletions app/components/mantine/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { TextInputProps as MantineTextInputProps } from "@mantine/core";
// このファイルは no-restricted-imports で提案される代替コンポーネントなので問題ない
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { TextInput as MantineTextInput } from "@mantine/core";

import { mantineInputOrder } from "../../config/mantine";

type TextInputProps = Omit<MantineTextInputProps, "inputWrapperOrder">;

export const TextInput = (props: TextInputProps) => {
const { autoComplete, ...rest } = props;

return (
<MantineTextInput
autoComplete={autoComplete}
inputWrapperOrder={mantineInputOrder}
{...(autoComplete === "off" && { "data-1p-ignore": true })}
{...rest}
/>
);
};
4 changes: 4 additions & 0 deletions app/components/mantine/fieldset.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.mantine-Fieldset-legend {
font-size: 1.25em;
font-weight: 700;
}
Loading

0 comments on commit ce15d14

Please sign in to comment.