nekochans / lgtm-cat-frontend

lgtm-cat(サービス名 LGTMeow https://lgtmeow.com) のフロントエンド用
https://lgtmeow.com
21 stars 2 forks source link

新デザイン(2024年)のUploadImageFormComponentを作成する #326

Open keitakn opened 1 month ago

keitakn commented 1 month ago

Done の定義

補足情報

https://www.figma.com/design/zkmgb1HoYkaMwitEVaHQyF/LGTMeow-UI-2024?node-id=0-1&m=dev

keitakn commented 1 month ago

フォームで画像を送信する際は useActionState を利用する。

しかしNext.js v14 + React v18.3だと利用出来ないので以下のようにする事で useActionState を利用する。 公式の useActionState が利用可能になったらそちらに置き換えればOK。

src/app/test/_types/FormState.ts

export type FormState = { errors: { word?: string }, sendWord?: string };

src/app/test/_actions/myAction.ts

'use server';

import type { FormState } from '@/app/test/_types/FormState';
import { setTimeout } from 'timers/promises';

export const myAction = async (state: FormState, payload: FormData) => {
  const newState = structuredClone(state);

  await setTimeout(1000);

  if (!payload.get('word')) {
    return {
      errors: {
        word: '入力してください',
      },
    };
  }

  newState.sendWord = payload.get('word') as string;
  newState.errors.word = '';

  return newState;
};

src/app/test/_actions/useActionState.ts

'use client';

import { useCallback, useTransition } from 'react';
import { useFormState } from 'react-dom';

/**
 * @see {@link https://github.com/DefinitelyTyped/DefinitelyTyped/blob/0b728411cd1dfb4bd26992bb35a73cf8edaa22e7/types/react/canary.d.ts#L103-L112}
 */
export function useActionState<State>(
  action: (state: Awaited<State>) => State | Promise<State>,
  initialState: Awaited<State>,
  permalink?: string,
): [state: Awaited<State>, dispatch: () => void, isPending: boolean];

export function useActionState<State, Payload>(
  action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
  initialState: Awaited<State>,
  permalink?: string,
): [state: Awaited<State>, dispatch: (payload: Payload) => void, isPending: boolean];

export function useActionState<State, Payload>(
  action: (state: Awaited<State>, payload: Payload) => State | Promise<State>,
  initialState: Awaited<State>,
  permalink?: string,
) {
  const [isPending, startTransition] = useTransition();

  const [currentState, dispatchAction] = useFormState(action, initialState, permalink);

  const finalAction = useCallback(
    (payload: Payload) => {
      startTransition(() => {
        dispatchAction(payload);
      });
    },
    [dispatchAction],
  );

  return [currentState, finalAction, isPending];
}

src/app/test/_components/MyForm.tsx

'use client';

import { useActionState } from '../_actions/useActionState';
import { myAction } from '../_actions/myAction';

export const MyForm = () => {
  const [currentState, action, isPending] = useActionState(myAction, { errors: {} });

  return (
    <form action={action}>
      <div>
        <input type="text" name="word" />
        {currentState.errors.word && <div>{currentState.errors.word}</div>}
      </div>
      <button disabled={isPending}>Submit</button>
      {isPending && <div>送信中...</div>}
      {currentState.sendWord && <div>{currentState.sendWord} が送信されました!</div>}
    </form>
  );
};

src/app/test/page.tsx

import { MyForm } from '@/app/test/_components/MyForm';

const TestPage = () => {
  return (
    <div>
      <h1>Test Page</h1>
      <MyForm />
    </div>
  );
};

export default TestPage;