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

UI プロトタイプ実装 #13

Merged
merged 91 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
5709751
chore: add vscode example config
sushichan044 Jan 27, 2025
414edd2
fix: tanstack query ではなく fetch を使う
sushichan044 Jan 27, 2025
7feed7f
feat: generate zod schema
sushichan044 Jan 27, 2025
c7cd72e
chore(deps): add conform
sushichan044 Jan 27, 2025
abe9c1e
chore(deps): add ufo
sushichan044 Jan 27, 2025
02b5823
feat: 簡易検索が動く
sushichan044 Jan 27, 2025
ce43d25
wip: 詳細検索
sushichan044 Jan 27, 2025
2f0ad03
fix: モーダル内のフォームを submit した際新しい状態をページ側のフォームに反映する
sushichan044 Jan 27, 2025
e29d9e0
fix: 配列に単体を渡しても良い
sushichan044 Feb 3, 2025
4e030e2
chore: meta tag
sushichan044 Feb 3, 2025
cd0c0c0
fix: DateRange が空で input されたときの処理
sushichan044 Feb 3, 2025
18c8687
chore: tsconfig を厳しくする
sushichan044 Feb 3, 2025
a8dd657
ブラウザの言語を取得する hooks
sushichan044 Feb 3, 2025
2452d8e
Note の Topic を表示するコンポーネント
sushichan044 Feb 3, 2025
1deafb2
トピックを検索可能にする
sushichan044 Feb 3, 2025
35ecc68
Mantine MultiSelect と Conform を連携可能にする
sushichan044 Feb 3, 2025
aa16516
トピック検索を複数選択可能にする
sushichan044 Feb 3, 2025
a076215
検索遷移中は検索 UI を操作できない
sushichan044 Feb 3, 2025
d206c23
空の値を事前に判定する
sushichan044 Feb 3, 2025
bc4dfa2
refactor: フォームごとに hooks を分けて id 生成を任せる
sushichan044 Feb 3, 2025
b777914
refactor: 値の処理部分を切り出す
sushichan044 Feb 3, 2025
4e0c2d7
言語選択に Select を使う
sushichan044 Feb 3, 2025
92afc5a
feat: ノートとポストに対する文検索
sushichan044 Feb 3, 2025
5234b46
fix: 予期せぬ autoCompletion を無効化
sushichan044 Feb 3, 2025
a75880c
fix: Response を返さず data を呼び出す
sushichan044 Feb 3, 2025
032ff6f
chore: Mantine と Tailwind の乖離を減らす
sushichan044 Feb 4, 2025
e1aa14a
refactor: 自動で data-1p-ignore してくれる TextInput を自作する
sushichan044 Feb 3, 2025
f639ff0
chore: Note Status の定数を作っておく
sushichan044 Feb 3, 2025
1f1b318
chore(deps): setup tailwindcss
sushichan044 Feb 4, 2025
5874c5f
fix: loader return value structure
sushichan044 Feb 4, 2025
a9726bd
検索条件追加、Modal を Advanced Search 側に寄せる
sushichan044 Feb 4, 2025
7fd35b6
fix: value coercing
sushichan044 Feb 4, 2025
8df6c6a
fix: 詳細検索モーダルをフルスクリーンにしない
sushichan044 Feb 4, 2025
dfc1131
feat: FieldSet を使ってまとめる
sushichan044 Feb 4, 2025
933b32e
fix: Mantine をラップしたコンポーネントの置き場
sushichan044 Feb 4, 2025
35a0e27
chore: sort props
sushichan044 Feb 4, 2025
933b033
fix: Modal を分離する
sushichan044 Feb 4, 2025
b3188c8
wip: set meta tag
sushichan044 Feb 4, 2025
bed95b0
fix: typo
sushichan044 Feb 4, 2025
9f40165
chore: スタイル調整
sushichan044 Feb 4, 2025
ec8b304
feat: Note, Post を整形して表示
sushichan044 Feb 4, 2025
0c78283
chore: InputControl を spread しない
sushichan044 Feb 4, 2025
ad8b002
fix: rename arrayContainsNonNullItem to containsNonNullValues
sushichan044 Feb 4, 2025
ee92d82
chore(deps): install prettier
sushichan044 Feb 4, 2025
f087177
chore: run prettier
sushichan044 Feb 4, 2025
14427a1
refactor: DateRangePicker を切り出す
sushichan044 Feb 4, 2025
21e5d3a
refactor: LanguageSelect を切り出す
sushichan044 Feb 4, 2025
28b68a8
fix: 配列の処理
sushichan044 Feb 4, 2025
8609cf7
fix: action でも手で作ったスキーマを使う
sushichan044 Feb 4, 2025
29a1f88
chore(deps): update ESLint
sushichan044 Feb 4, 2025
e407092
refactor: TopicSelect を切り出す
sushichan044 Feb 4, 2025
e7d38e8
chore: vitest 導入
sushichan044 Feb 5, 2025
a58a43f
chore: DateRangePicker の挙動をテストする
sushichan044 Feb 5, 2025
075c1e2
fix: オーバーロードの追加
sushichan044 Feb 5, 2025
8ad2d42
chore(deps): add unplugin-icons
sushichan044 Feb 5, 2025
e5378ef
feat: ページネーションを可能にする
sushichan044 Feb 5, 2025
c746355
feat: 1 ページに表示する limit を調整可能にする
sushichan044 Feb 5, 2025
ce06768
feat: PC レイアウトではフォームと結果を横並びにする
sushichan044 Feb 5, 2025
b4a6d67
chore: スタイルの微調整
sushichan044 Feb 5, 2025
6f292c6
chore: tailwindcss の class 名を sort 可能にする
sushichan044 Feb 5, 2025
ffb9e20
chore: sort tailwind class
sushichan044 Feb 5, 2025
c7ee9d5
fix: remove default search params
sushichan044 Feb 12, 2025
9a64cbc
chore: 文字間隔調整
sushichan044 Feb 12, 2025
67fb75d
chore: add loading style for pagination button
sushichan044 Feb 12, 2025
d10cf0c
fix: set default limit to 25
sushichan044 Feb 12, 2025
80c079c
refactor: add useLanguageLiteral
sushichan044 Feb 12, 2025
e30eb26
perf: lazy media
sushichan044 Feb 12, 2025
0c3778c
chore: layout
sushichan044 Feb 12, 2025
016937b
perf: content-visibility: auto
sushichan044 Feb 12, 2025
1f50956
fix: ページレイアウト調整
sushichan044 Feb 12, 2025
7c93c13
fix: note id, note date の表示位置を上にする
sushichan044 Feb 12, 2025
707dd15
perf: cache date string calculation
sushichan044 Feb 12, 2025
8724e47
fix: useMultiSelectInputControl をそのままコンポーネントに渡さない
sushichan044 Feb 12, 2025
df583f3
feat: 詳細検索のみの条件を簡易検索画面でも保持する
sushichan044 Feb 12, 2025
d11a601
ci: add code check action
sushichan044 Feb 12, 2025
8e8b8c8
ci: add vitest reporter
sushichan044 Feb 12, 2025
5e040c1
chore: set mock locale
sushichan044 Feb 12, 2025
9b26a6d
auto: codegen
sushichan044 Feb 12, 2025
c562f79
chore: リンクラベルを短く
sushichan044 Feb 12, 2025
cb85562
fix: スマホでノートの言語が潰れていた
sushichan044 Feb 12, 2025
0fa012c
fix: スマホで大きな選択 UI が画面幅をはみ出していた
sushichan044 Feb 12, 2025
3ba704b
fix: always use UTC
sushichan044 Feb 12, 2025
46536fc
chore: add check script
sushichan044 Feb 12, 2025
c68473a
fix: ラベルの fallback は en とする
sushichan044 Feb 12, 2025
abdf607
docs: リリース前後の meta tag 操作コメント
sushichan044 Feb 12, 2025
fd1aa54
chore: remove dead file
sushichan044 Feb 12, 2025
6dfc798
fix(a11y): add alt text on media
sushichan044 Feb 12, 2025
e87098a
fix(a11y): pagination label
sushichan044 Feb 12, 2025
322b69b
fix(a11y): main tag
sushichan044 Feb 12, 2025
9bf9cab
add controls and links
yu23ki14 Feb 12, 2025
7291478
fix lint check
yu23ki14 Feb 12, 2025
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
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 })}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1password が必要以上に自動補完してくるのを防ぐ

{...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