github.com/oinume/lekcije@v0.0.0-20231017100347-5b4c5eb6ab24/frontend/src/pages/MePage.tsx (about)

     1  import React, {useState} from 'react';
     2  import {toast} from 'react-toastify';
     3  import {useQueryClient} from '@tanstack/react-query';
     4  import {PageTitle} from '../components/PageTitle';
     5  import {Loader} from '../components/Loader';
     6  import type {Teacher} from '../models/Teacher';
     7  import {ToastContainer} from '../components/ToastContainer';
     8  import type {GetViewerWithFollowingTeachersQuery} from '../graphql/generated';
     9  import {
    10    useCreateFollowingTeacherMutation, useDeleteFollowingTeachersMutation,
    11    useGetViewerWithFollowingTeachersQuery, useGetViewerWithNotificationTimeSpansQuery,
    12  } from '../graphql/generated';
    13  import type {GraphQLError} from '../http/graphql';
    14  import {createGraphQLClient, toMessage} from '../http/graphql';
    15  import type {FollowingTeacher} from '../models/FollowingTeacher';
    16  import {SubmitButton} from '../components/SubmitButton';
    17  
    18  const graphqlClient = createGraphQLClient();
    19  
    20  export const MePage = () => {
    21    const getViewerResult = useGetViewerWithFollowingTeachersQuery<GetViewerWithFollowingTeachersQuery, GraphQLError>(graphqlClient, {}, {
    22      onError(error) {
    23        toast.error(toMessage(error, 'データの取得に失敗しました'));
    24      },
    25    });
    26    const showTutorial = getViewerResult.data ? getViewerResult.data.viewer.showTutorial : false;
    27    const followingTeachers: FollowingTeacher[] = getViewerResult.data ? getViewerResult.data.viewer.followingTeachers.nodes.map(node => ({
    28      teacher: {
    29        id: node.teacher.id,
    30        name: node.teacher.name,
    31      },
    32    })) : [];
    33  
    34    return (
    35      <div id="followingForm">
    36        <ToastContainer
    37          closeOnClick={false}
    38        />
    39        <PageTitle>フォローしている講師</PageTitle>
    40        {
    41          getViewerResult.isLoading
    42            ? <Loader isLoading={getViewerResult.isLoading}/>
    43            : <MeContent followingTeachers={followingTeachers} showTutorial={showTutorial}/>
    44        }
    45      </div>
    46    );
    47  };
    48  
    49  type MeContentProps = {
    50    readonly followingTeachers: FollowingTeacher[];
    51    readonly showTutorial: boolean; // eslint-disable-line react/boolean-prop-naming
    52  };
    53  
    54  // Help URL
    55  // https://lekcije.amebaownd.com/posts/{{ if .IsUserAgentPC }}2044879{{ end }}{{ if .IsUserAgentSP }}1577091{{ end }}{{ if .IsUserAgentTablet }}1577091{{ end }}
    56  
    57  const MeContent = ({followingTeachers, showTutorial}: MeContentProps) => (
    58    <>
    59      {showTutorial ? <Tutorial/> : <div/>}
    60      <CreateForm/>
    61      <TeacherList followingTeachers={followingTeachers}/>
    62    </>
    63  );
    64  
    65  const Tutorial = () => (
    66    <div className="alert alert-success alert-dismissible" role="alert">
    67      <button type="button" className="btn-close" data-bs-dismiss="alert" aria-label="Close"/>
    68      <h4><i className="bi bi-info-square-fill"/> 講師をフォローするには</h4>
    69      <ol>
    70        <li><a href="https://eikaiwa.dmm.com/list/" className="alert-link" target="_blank" rel="noreferrer">DMM英会話</a>でお気に入りの講師のページにアクセスしましょう</li>
    71        <li>講師のURLをコピーしましょう(<a href="https://lekcije.amebaownd.com/posts/1577091" className="alert-link" target="_blank" rel="noreferrer">ヘルプ</a>)</li>
    72        <li>URLを下の入力欄にペーストしてフォローしましょう</li>
    73        <li>フォローすると、その講師の空きレッスンがあった時にメールでお知らせします</li>
    74      </ol>
    75    </div>
    76  );
    77  
    78  const CreateForm = () => {
    79    const [teacherIdOrUrl, setTeacherIdOrUrl] = useState('');
    80    const [submitDisabled, setSubmitDisabled] = useState(true);
    81    const [submitLoading, setSubmitLoading] = useState(false);
    82  
    83    const queryClient = useQueryClient();
    84  
    85    const createFollowingTeacherMutation = useCreateFollowingTeacherMutation<GraphQLError>(graphqlClient, {
    86      async onSuccess() {
    87        await queryClient.invalidateQueries(useGetViewerWithFollowingTeachersQuery.getKey());
    88        setTeacherIdOrUrl('');
    89        setSubmitDisabled(true);
    90        setSubmitLoading(false);
    91        toast.success('講師をフォローしました!');
    92      },
    93      onError(error) {
    94        // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
    95        console.error(`useCreateFollowingTeacherMutation.onError: err=${error}`);
    96        setSubmitDisabled(false);
    97        setSubmitLoading(false);
    98        toast.error(toMessage(error, '講師のフォローに失敗しました'));
    99      },
   100    });
   101  
   102    return (
   103      <form
   104        onSubmit={event => {
   105          event.preventDefault();
   106          setSubmitDisabled(true);
   107          setSubmitLoading(true);
   108          createFollowingTeacherMutation.mutate({input: {teacherIdOrUrl}});
   109        }}
   110      >
   111        <p>
   112          講師のURLまたはIDを入力してフォローします<a href="https://lekcije.amebaownd.com/posts/2044879" rel="noreferrer" target="_blank"><i className="fas fa-question-circle button-help" aria-hidden="true"/></a><br/>
   113          <small><a href="https://eikaiwa.dmm.com/list/" rel="noreferrer" target="_blank">DMM英会話で講師を検索</a></small>
   114        </p>
   115        <div className="input-group mb-3">
   116          <input
   117            required
   118            autoFocus
   119            id="teacherIdsOrUrl"
   120            type="text"
   121            className="form-control"
   122            name="teacherIdsOrUrl"
   123            placeholder="https://eikaiwa.dmm.com/teacher/index/492/"
   124            value={teacherIdOrUrl}
   125            onChange={event => {
   126              event.preventDefault();
   127              setSubmitDisabled(event.currentTarget.value === '');
   128              setTeacherIdOrUrl(event.currentTarget.value);
   129            }}
   130          />
   131          <span className="px-2"/>
   132          <SubmitButton
   133            disabled={submitDisabled}
   134            loading={submitLoading}
   135          >
   136            送信
   137          </SubmitButton>
   138        </div>
   139      </form>
   140    );
   141  };
   142  
   143  type TeacherListProps = {
   144    readonly followingTeachers: FollowingTeacher[];
   145  };
   146  
   147  const TeacherList = ({followingTeachers}: TeacherListProps) => {
   148    const [checkedIds, setCheckedIds] = useState<string[]>([]);
   149    const [deleteSubmitDisabled, setDeleteSubmitDisabled] = useState(true);
   150    const [deleteSubmitLoading, setDeleteSubmitLoading] = useState(false);
   151  
   152    const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
   153      const targetId = event.target.value;
   154      if (event.target.checked) {
   155        setCheckedIds([...checkedIds, targetId]);
   156        setDeleteSubmitDisabled(false);
   157      } else {
   158        const restIds = checkedIds.filter(id => id !== targetId);
   159        setCheckedIds(restIds);
   160        setDeleteSubmitDisabled(restIds.length === 0);
   161      }
   162    };
   163  
   164    const queryClient = useQueryClient();
   165    const deleteFollowingTeacherMutation = useDeleteFollowingTeachersMutation<GraphQLError>(graphqlClient, {
   166      async onSuccess() {
   167        await queryClient.invalidateQueries(useGetViewerWithFollowingTeachersQuery.getKey());
   168        toast.success('講師のフォローを解除しました');
   169        setDeleteSubmitDisabled(true);
   170        setDeleteSubmitLoading(false);
   171      },
   172      onError(error) {
   173        // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
   174        console.error(`deleteFollowingTeacherMutation.onError: err=${error}`);
   175        setDeleteSubmitDisabled(false);
   176        setDeleteSubmitLoading(false);
   177        // toast.error(`講師のフォロー解除に失敗しました: ${error.message}`);
   178        toast.error(toMessage(error, '講師のフォロー解除に失敗しました'));
   179      },
   180    },
   181    );
   182  
   183    return (
   184      <div id="followingTeachers">
   185        <form
   186          onSubmit={event => {
   187            event.preventDefault();
   188            setDeleteSubmitDisabled(true);
   189            setDeleteSubmitLoading(true);
   190            deleteFollowingTeacherMutation.mutate({input: {teacherIds: checkedIds}});
   191          }}
   192        >
   193          <table className="table table-striped table-hover">
   194            <thead>
   195              <tr>
   196                <th scope="col" className="col-md-1">
   197                  <SubmitButton
   198                    disabled={deleteSubmitDisabled}
   199                    loading={deleteSubmitLoading}
   200                  >
   201                    削除
   202                  </SubmitButton>
   203                </th>
   204                <th scope="col" className="col-md-11">
   205                  講師
   206                </th>
   207              </tr>
   208            </thead>
   209            <tbody>
   210              {followingTeachers.map(ft => <TeacherRow key={ft.teacher.id} teacher={ft.teacher} handleOnChange={handleCheckboxChange}/>)}
   211            </tbody>
   212          </table>
   213        </form>
   214      </div>
   215    );
   216  };
   217  
   218  type TeacherRowProps = {
   219    readonly teacher: Teacher;
   220    readonly handleOnChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
   221  };
   222  
   223  const TeacherRow = ({teacher, handleOnChange}: TeacherRowProps) => (
   224    <tr>
   225      <td className="col-md-1"><input type="checkbox" name="teacherIds" value={teacher.id} onChange={handleOnChange}/></td>
   226      <td className="col-md-8"><a href={`https://eikaiwa.dmm.com/teacher/index/${teacher.id}`} target="_blank" rel="noreferrer">{ teacher.name }</a></td>
   227    </tr>
   228  );