Torihaji's Growth Diary

Little by little, no hurry.

初めての個人開発日記 10日目

はじめに

こんにちは、torihaziです

土日でリリース予定ですが、とりあえず今日は更新と削除まで行きたいと思います。

昨日少しやる気が云々とか言ってましたが

今日は少し大丈夫な気がしなくもない。

つべこべ言わずにやりましょう。

技術選定

[frontend]

  • Nextjs(pages router) => App Routerの理解に苦しんだため
  • MUI => 調べたランキングでtopだったため(=> approuter では動かないそう。今後は要検討)
  • react-hook-form => フォーム管理と言ったらこれでは?
  • zod => 少しだけ使い慣れてるから
  • axios => httpリクエスト送るのはこれでは?
  • recoil => 状態管理、useStateのような扱いで使いやすい
  • js-cookie => cookieを扱う上で使用。軽く、更新も頻繁にされている
  • useSWR => データ取得用、
  • Editorjs => WYSIWYGエディタ、学習のため導入。本サービスのキモ。

[backend]

  • Rails 7 => 使い慣れているため
  • devise => 定番だから
  • devise-jwt => devise-token-authが古いらしいのでこっち
  • letter_opener_web => 開発環境で確認メール確認用

[github]

GitHub - torihazi/diary_front

GitHub - torihazi/diary_back

更新

昨日やると言っといてできなかったので、まず更新を。

まずは更新画面を作る。

その後にurlにid渡して、情報持ってきて

やればいいのかな。の前にbackendか。

終わった。

  def update
    unless owner_verify
      render json: {"message": "所有者ではありません"}, status: :unprocessable_entity
          
    if @diary.update(diary_params)
      render json: {"message": "更新が完了しました", status: :ok}
    else
      Rails.logger.debug(@diary.error_messages.full_messages.join(""))
      render json: {"message": "更新に失敗しました", status: :unprocessable_entity}
    end
  end

こんなんでいいんでは?

※ 反省ポイント

今はこんなしょぼい機能くらいしかできないけど

多分やっているうちに、自分が何気なくできるレベルが必然的に上がってくるから

それまでひたすら書き続けていきたいと思う。

と思ったら rails の方で syntax-errorが出ていた。

unlessの書き方違ったっけ。

endいるんだ。恥ずかしい。

というかunlessは直感的にわかりにくいから、という理由で敬遠されているらしい。

ということで最初のowner_verifyのところをifで閉じた

一回、鬱陶しいから今のuserのdiaryをdestroy_allしてやり直す。

新しくnewの画面を開いたら

TypeError: Cannot read properties of undefined (reading 'parseAsync')

こんなこと言われた。

ググる

Cannot read properties of undefined (reading '_parse') · Issue #1193 · colinhacks/zod · GitHub

zodの何かがおかしいらしい。

あれだ

schemaの名前を変えたのに前の名前でやっていたからだ。

治したら元に戻った。

ということで適当にnewして作成して、

それを詳細画面から開く。

文章おかしいのまだ勘弁してね。

で、この編集を押したらこの詳細画面に表示されているものが表示されるはず。

実際に使用しようとしているのが下記。

注意すべきなのが、2つ

  • formに非同期で取得した値を渡すにはresetを使用し、その値で初期化する。defaultValueを使うと機能しない気がした
  • editorjsにbackendから取得したdataを渡すにはJSONのparseを通すこと。

の前になんか色々そうすると修正しないと。

JSON.parseに渡せるのはstringだから、editorjsに渡す時に型をOutputDataにしてたからそれも直さないと。

ただそうするとuseStateの時にやっていた型もOutputDataだから?

あれどうすればいいんだ。

     async onChange(api) {
          const data = await api.saver.save();
          onChange(data);
        },

この時点のdataがOutputDataで、onChangeに渡すときはすでにstringに直したやつを保存していいのか。

ということはstateの初期値もJSON.stringfyしていいのか。

  const [outputData, setOutputData] = useState<string>(
    JSON.stringify(INITIAL_EDITOR_DATA)
  );

にして。

  const onValid: SubmitHandler<diaryInputSchemaType> = (
    data: diaryInputSchemaType
  ) => {
    const newData = {
      ...data,
      content: outputData, <= stringfyをとった。
    };
    createDiary(newData);
  };

にして。

とにかく色々直して最終形態がこれ。

editorjs

import EditorJS, { OutputData } from "@editorjs/editorjs";
import { useEffect, useRef } from "react";
import { EDITOR_CONFIG } from "./editor-config";
import { Box } from "@mui/material";

const Editor = ({
  value,
  onChange,
  holder,
  readonly = false,
}: {
  value: string;
  onChange: (data: string) => void;
  holder: string;
  readonly?: boolean;
}) => {
  const editorRef = useRef<EditorJS | null>(null);

  useEffect(() => {
    if (!editorRef.current) {
      const editor = new EditorJS({
        holder: holder || "editorjs",
        placeholder: "入力してください",
        tools: EDITOR_CONFIG,
        readOnly: readonly,
        data: JSON.parse(value),
        async onChange(api) {
          const data: OutputData = await api.saver.save();
          onChange(JSON.stringify(data));
        },
      });

      editorRef.current = editor;
    }

    return () => {
      if (editorRef.current && editorRef.current.destroy) {
        editorRef.current.destroy();
        editorRef.current = null;
      }
    };
  }, []);

  return (
    <Box
      id={holder}
      p={1}
      pr={0}
      minHeight="300px"
      width="100%"
      border="1px solid #E4E7EB"
      borderRadius="10px"
    ></Box>
  );
};

export default Editor;

これがpages/diaries/new

import { DiaryTemplate } from "@/components/template/DiaryTemplate";
import { DiaryTitleForm } from "@/features/diaries/components/diary-title-form";
import { INITIAL_EDITOR_DATA } from "@/features/editorjs/constants/initial-editor-data";
import {
  diaryInputSchema,
  diaryInputSchemaType,
  useCreateDiary,
} from "@/lib/api/diaries";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@mui/material";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";

const Editor = dynamic(() => import("@/features/editorjs/editor-js"), {
  ssr: false,
});

const DiaryNew = () => {
  const [outputData, setOutputData] = useState<string>(
    JSON.stringify(INITIAL_EDITOR_DATA)
  );
  const router = useRouter();
  const { createDiary } = useCreateDiary({
    onSuccess: () => {
      router.push("/diaries");
    },
  });

  const form = useForm<diaryInputSchemaType>({
    mode: "onChange",
    resolver: zodResolver(diaryInputSchema),
  });

  const onValid: SubmitHandler<diaryInputSchemaType> = (
    data: diaryInputSchemaType
  ) => {
    const newData = {
      ...data,
      content: outputData,
    };
    createDiary(newData);
  };

  return (
    <DiaryTemplate>
      <DiaryTitleForm control={form.control} id="new-diary-title" />
      <Editor value={outputData} onChange={setOutputData} holder="editorjs" />
      <Button
        type="submit"
        form="new-diary-title"
        onClick={form.handleSubmit(onValid)}
        disabled={!form.formState.isValid}
      >
        保存する
      </Button>
    </DiaryTemplate>
  );
};

export default DiaryNew;

これでnewをやり直す。

newはいけた。

editは少し直して

import { DiaryTemplate } from "@/components/template/DiaryTemplate";
import { DiaryTitleForm } from "@/features/diaries/components/diary-title-form";
import { INITIAL_EDITOR_DATA } from "@/features/editorjs/constants/initial-editor-data";
import {
  diaryInputSchema,
  diaryInputSchemaType,
  useDiary,
} from "@/lib/api/diaries";
import { OutputData } from "@editorjs/editorjs";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@mui/material";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";

const Editor = dynamic(() => import("@/features/editorjs/editor-js"), {
  ssr: false,
});

export const DiaryEdit = () => {
  const [outputData, setOutputData] = useState<string>(
    JSON.stringify(INITIAL_EDITOR_DATA)
  );
  const router = useRouter();
  const { data: diary } = useDiary(router.query.id as string);  <= ここ
  // const { updateDiary } = useUpdateDiary({
  //   onSuccess: () => {
  //     router.push("/diaries")
  //   }
  // })

  const form = useForm<diaryInputSchemaType>({
    mode: "onChange",
    resolver: zodResolver(diaryInputSchema),
  });

  // const onValid: SubmitHandler<diaryInputSchemaType> = (
  //   data: diaryInputSchemaType
  // ) => {
  //   const newData = {
  //     ...data,
  //     content: JSON.stringify(outputData),
  //   };
  //   updateDiary(newData);
  // };

  return (
    <DiaryTemplate>
      <DiaryTitleForm control={form.control} id="update-diary-title" />
      <Editor
        value={diary?.content as string} <= ここ
        onChange={setOutputData}
        holder="editorjs"
      />
      <Button
        type="submit"
        form="new-diary-title"
        // onClick={form.handleSubmit(onValid)}
        disabled={!form.formState.isValid}
      >
        更新する
      </Button>
    </DiaryTemplate>
  );
};

export default DiaryEdit;

この画面の編集を開くとEditorjsにh2で"投稿二つ目"という文字が表示されるはず。

おー開いた。

あ、titleを忘れてた。

非同期でdiaryのtitleを取得するのでformのresetを使おうと思っていましたが

なんかreact-hook-formのv7.4 ~ から valuesとかいうものが出たらしく

Release Version 7.40.0-next.0 · react-hook-form/react-hook-form · GitHub

これこそまさに求めていたもの?

自分のもののverを調べてみると 7.5だったので早速使ってみる。

  const form = useForm<diaryInputSchemaType>({
    mode: "onChange",
    resolver: zodResolver(diaryInputSchema),
    values: {
      title: diary?.title as string,
    },
  });

こんな感じ。

見てみると.

いけてるけど行けてない?

タイトルのlabelがえぐいことに。

いや行けてた。

時折出るこれは何。

まぁいいや飛ばそ。

次はfrontからbackに飛ばす用のaxiosを作る。

//
// update
//

export const postUpdateDiaryInput = async (
  id: Diary["id"],
  data: diaryInputSchemaType
): Promise<Diary> => {
  const updateData = { diary: { ...data } };
  return api.post(`/api/v1/diaries/${id}`, updateData);
};

export const useUpdateDiary = ({
  onSuccess,
  onError,
}: {
  onSuccess?: () => void;
  onError?: () => void;
}) => {
  const setSnackBarState = useSetRecoilState(snackbarAtom);
  const { mutate } = useSWRConfig();

  const updateDiary = async ({
    data,
    id,
  }: {
    data: diaryInputSchemaType;
    id: Diary["id"];
  }) => {
    try {
      const updatedDiary = await postUpdateDiaryInput(id, data);
      mutate("/api/v1/diaries");
      setSnackBarState(successState("更新が完了しました"));
      if (onSuccess) {
        onSuccess();
      }
      return updatedDiary;
    } catch (err) {
      setSnackBarState(errorState("更新が失敗しました"));
      if (onError) {
        onError();
      }
      throw err;
    }
  };

  return { updateDiary };
};

こんな感じ。いけるかな。ほぼ変わってないんだけど。

更新の時のmutateってどのキー指定したらいいんだろうか。

まぁいいや。一旦先に進もう。

とりあえず更新をやってみる。

これから変えてみる。

変わってないし。

なんで。routingerrorと出てた。

バカじゃん。api.putでしょ。

リベンジ。

違うエラーが。

あれ、

@diary.user.idってダメなの

diaryにbelongs_toつけてるんだけど。

バカだ。

before_actionにupdateつけるの忘れた.

ということでリベンジ。

いけた。

現時点で色々バグ見つかってるけど、ま、いいや。

更新終了。

※ 反省ポイント

バグをissueで管理することにした。

気が向いた時に原因とその解決策を残す感じでやっていこう。

削除

ついにCRUDの最後。削除。

これは簡単そう。

削除ボタンあるから、それにonClick設定して、云々やればいいか。

今気づいた。error_messageのfull_messagesの後join(",")でやらないと。

full_messagesの戻り値は配列でjoinは区切り文字で配列を展開するから。

後でやろ。

とりあえずbackendはこんな

  def destory
    if !owner_verify
      render json: {"message": "所有者ではありません"}, status: :unprocessable_entity
    end

    if @diary.destory
      render json: {"message": "削除が成功しました"}, status: :ok
    else
      Rails.logger.debug(@diary.error_messages.full_messages.join(","))
      render json: {"message": "削除に失敗しました"}, status: :ok
    end
  end

次、front、

削除ボタンは詳細画面にあるから。

これ押したら削除のconfirm出して、okで削除みたいな流れでいいか。

ゆくゆくはmodal出すで。

で肝心のボタンだけど、この際、編集アイコンとか、削除とか

全部ボタンにしよう。

できた。

後モーダルも作った。

import { Diary } from "@/types/type";
import { Box, Modal, Typography } from "@mui/material";

export const DiaryDeleteModal = ({
  id,
  open,
  onClose,
}: {
  id: Diary["id"];
  open: boolean;
  onClose: () => void;
}) => {
  return (
    <Modal
      open={open}
      onClose={onClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box>
        <Typography id="modal-modal-title" variant="h6" component="h2">
          この日記を削除します
        </Typography>
        <Typography id="modal-modal-description" sx={{ mt: 2 }}>
          よろしいですか?
        </Typography>
      </Box>
    </Modal>
  );
};

表示してみると

イカれた。デフォルトのデザインだとこうなのね。

一旦これでいいや。

でaxiosを作成する。

あとメソッド名もなおした

createDiary, handleCreateDiary、

updateDiaryとhandleUpdateDiary、

deleteDiaryとhandleDeleteDiaryみたいな感じ。

//
// delete
//

export const deleteDiary = ({ id }: { id: Diary["id"] }): Promise<Diary> => {
  return api.delete(`/api/v1/diaries/${id}`);
};

export const useDeleteDiary = ({
  onSuccess,
  onError,
}: {
  onSuccess?: () => void;
  onError?: () => void;
}) => {
  const setSnackBarState = useSetRecoilState(snackbarAtom);
  const { mutate } = useSWRConfig();

  const handleDeleteDiary = async ({ id }: { id: Diary["id"] }) => {
    try {
      const deletedDiary = await deleteDiary({ id });
      mutate("/api/v1/diaries");
      setSnackBarState(successState("削除に成功しました"));
      if (onSuccess) {
        onSuccess();
      }
      return deleteDiary;
    } catch (err) {
      setSnackBarState(errorState("削除に失敗しました"));
      if (onError) {
        onError();
      }
      throw err;
    }
  };

  return { handleDeleteDiary };
};

axiosはこんな感じ

んでmodalはこんな感じに直した。

import { CancelButton } from "@/components/ui/cancel-button";
import { DeleteButton } from "@/components/ui/delete-button";
import { useDeleteDiary } from "@/lib/api/diaries";
import { Diary } from "@/types/type";
import { Box, Modal, Typography } from "@mui/material";
import { useRouter } from "next/router";

export const DiaryDeleteModal = ({
  id,
  open,
  onClose,
}: {
  id: Diary["id"];
  open: boolean;
  onClose: () => void;
}) => {
  const router = useRouter();
  const { handleDeleteDiary: destroy } = useDeleteDiary({
    onSuccess: () => {
      router.push("/api/vi/diaries");
    },
  });
  return (
    <Modal
      open={open}
      onClose={onClose}
      aria-labelledby="modal-modal-title"
      aria-describedby="modal-modal-description"
    >
      <Box
        sx={{
          position: "absolute" as "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -50%)",
          width: 400,
          bgcolor: "background.paper",
          boxShadow: 24,
          p: 4,
        }}
      >
        <Typography id="modal-modal-title" variant="h6" component="h2">
          この日記を削除します
        </Typography>
        <Typography id="modal-modal-description" sx={{ mt: 2 }}>
          よろしいですか?
        </Typography>
        <Box display="flex" justifyContent="end" gap={2}>
          <CancelButton onClick={onClose} />
          <DeleteButton
            onClick={() => {
              destroy({ id });
            }}
          />
        </Box>
      </Box>
    </Modal>
  );
};

これで行ってほしい。

動かない。

タイポ。

deletemodalの中のonSuccess、/diariesだ。

えーと消えない。

idがundefiendらしい。

なぜ。

色々見直してタイポが見つかった。

handleDeleteDiaryの成功した時の戻り値がdeletedDiary、

railsの方のアクションがdestroy。

恥ずかしい。

リベンジ。

before_action のset_diaryに:destroyを追加することを忘れてた。


成功。消えた。

あとは詳細画面の方だけか。

こいつ。だけど一応crudは終わった。

エラー時の対応とか多分スッカスカだけど、一旦いいや。

これから見直していこう。

詳細画面の見直し

はい、あのダサい詳細画面を見直します。

まず最初はEditorjsのreadonlyがどんな見た目になるか試してみます

ほー。

これでもいいんじゃね。

borderだけ外せばなんか良さげになるんでは?

ただ外すにはあれかこれoptionalにしないといけないのかな?

これさ、もう少し左によらないのかな。

別にこれでもborder外したらいいっちゃいいんだけど、

少し気持ち悪い?

気のせい?

終わりに

昨日は開発合宿で他のことやってました

色々考えた結果、今自分が通っているスクールの方の課題を個人開発でやる方が

断然良さそうなのでそっちにシフトしたいと思います

ということでNextjs x RailsCRUDアプリ1つ目、できてなんとなく全体の要領が掴めたので

個人開発これにて終了でございます笑

ローンチするとか言っていましたが、そこは勘弁してください

明日は反省会ですね。というか今日になるんですかね。