麹に特化したレシピ共有アプリ【麹帳】リリース
投稿日: 2025年02月14日
また一つオリジナルアプリを開発しました。
今回は麹レシピに特化したレシピの共有アプリです。
今までは子持ち限定、しかも年齢的にも限られるようなアプリばかりでしたが、今回は初めて誰でも利用できるようなアプリになっていると思います。
2024年1月から、米麹を使って自分で麹調味料を作って普段の料理に使うことをしています。
娘が通っていた幼稚園が自然派寄りだったので健康志向な親御さんが多く、ママ友から甘酒作り等教わって始めたのがきっかけです。
麹調味料の魅力は、それだけでおいしい料理が作れることです。
私は主婦なので毎日料理しますが、料理することは決して好きでやっているわけではなく(得意ですけど😀w)抜ける手は全力で抜きたいと考えています。
やりたくないけどやらないわけにはいかないので、手作りの調味料使って調理したり、お気に入りのフライパン(リバーライト推し)や鍋(ジオプロダクト推し)を使って料理することでモチベーションをギリ保っています。
また、腸活になるのも麹調味料の魅力です(私はあまり効果感じてないですがw)
突然の重い話ですが、私が20歳の時に母が大腸がんで3年半の闘病の末、49歳で他界しました。
大腸がんは、腸活で多少予防が出来るのでは?と思うところもあったり、大切な子供たちの健康を守りたいという思いもありますし、作るのも大変なことではないので麹調味料が自分の大切な人の健康を守ってくれるならという思いもあります。
健康は腸からみたいな言葉(本?)もありますよね。
私の麹調味料の布教活動はママ友のみならず、ShiftBにも広がっています。
初めにぶべさんが麹欲しいと申し出てこられて、昨年の夏に開催された東京オフ会の時に神奈川にある姉の家でたくさん作って持っていきました。
さらにこうすけさんも遊びに来た時にあげました。(もうないって聞いたので来月会う時に持っていきます🔥)
さらにさらにともえさんにも大阪オフ会の時にあげました。(ともえさん明日会う予定😊楽しみ♪)
みんな美味しいと言ってくれて。(ニンニク麹が人気👏)
こうすけさんとともえさんは、作った料理の写真を送って下さり🥺
そんな使い方もあるのね!と新しい発見もたくさんありました。
二人とも料理上手で美味しそうな料理みて私も頑張ろうと思えたり、モチベーションになります。
やはり自分が作るものには偏りも出て来るので、人の作った料理にはヒントが詰まっていたり真似しよう🎵って思うことが多々ありました。実際真似しまくりですw
ありがたい限りです。
そして「これ、個別DMに収めてるの勿体なくない?」という思いが芽生えてきました。
みんなで共有したらもっと楽しい麹ライフが送れるような気がしました。
ともえさんは麹調味料作りのためにヨーグルトメーカー奥から出してきたと聞きまして、各レシピも共有したいなと思ったり、ShiftBのLPデザイン担当された(学習アプリのデザインもこれから)AKARIさんにも麹調味料のレシピは共有しましたが、誰でも見れるように情報をまとめたい! !
そして、その調味料を使ってどんなものを作ったら美味しいか麹調味料に紐づけたい、、という私個人の希望を詰め込んだアプリを作りたいと思い、開発スタートしました!!
いつも通りです。
TypeScript
Next.js 15
React 19
React Hook Form
React Select
SWR
Tailwind CSS 3
React Hot Toast
React Loading Skeleton
React Modal
React Paginate
Zod
React Table
TypeScript 5
Next.js 15
Prisma 6
PostgreSQL(Supabase)
@supabase/supabase-js
基本的にレシピの閲覧はだれでも出来るけど投稿しようと思ったら登録が必要になっています。
麹調味料レシピの閲覧
麹調味料を使ったレシピの閲覧
各レシピにいいね(連打非対応)
ブックマーク機能
各レシピの投稿
麹調味料は、例えば「塩麹」のレシピが乱立すると関連レシピが探しにくくなり、ややこしいことになりそうだと思ったので、投稿は申請制にし、メイン、サブとロールを分けて自由に投稿は出来ないようにしました。ユーザーのロールがADMINの人のみ申請中から公開に変更できるようにしようと思い、管理画面作る予定です。(現状supabase上で操作)→対応済
トップページではSSRでデータ取得するようにしました。
また、下層ページの一覧画面では基本的に出力するデータは更新を伴わないので、SSRでデータを取得して、その後SWRでデータ取得するフックに初期値として取得したデータを渡して、ログイン済の場合はいいね済、ブックマーク済がわかるようにデータを更新する、ログイン済ではない場合はそのままの画面となるようにしました。
これでいいのかわからないけど、、体感表示が早いような気がしています(多分)。
関連記事↓
今度作ってみようというレシピが見つかった時には保存しておけるようにしました。
ログイン中じゃない場合はユーザー登録、ログインを促すモーダルが表示されます。
今回モーダルはいまのところここでだけ登場しています。
何度でも押せる、誰でも押せるがこだわりです。
ログイン中の場合は誰がどの投稿にいいねしたかデータ管理していますが、ログイン中じゃない場合はデータのlikesカラムだけ増やす処理を行います。
このブログのように連打には対応していません。一度押すごとにAPI叩き、更新が完了するまでdisabledで動作を制御しています。→連打対応済
このあたり処理はこれが正しいのかわからないですが、バックエンドでログイン中の場合とそうでない場合のレスポンスを変える形で対応してみました。
import { NextRequest, NextResponse } from "next/server";
import { buildPrisma } from "@/app/_utils/prisma";
import { buildError } from "@/app/api/_utils/buildError";
import { supabase } from "@/app/_utils/supabase";
interface Props {
params: Promise<{
id: string;
}>;
}
//認証情報あってもなくても良いけど処理が分かれる
export const POST = async (request: NextRequest, { params }: Props) => {
const prisma = await buildPrisma();
const token = request.headers.get("Authorization") ?? "";
try {
const { data } = await supabase.auth.getUser(token);
const { id } = await params;
//likesをインクリメント
await prisma.maltArticle.update({
where: {
id,
},
data: {
likes: {
increment: 1,
},
},
});
//ログインしてなければreturn
if (!data.user) {
return NextResponse.json(
{
message: "success!",
},
{ status: 200 }
);
}
//ログイン中の人で一回目のいいねならuserActionをcreate
const user = await prisma.user.findUnique({
where: {
supabaseUserId: data.user.id,
},
});
if (!user) {
return NextResponse.json(
{
error: "user is not found!",
},
{ status: 404 }
);
}
const action = await prisma.maltUserAction.findUnique({
where: {
userId_actionType_maltArticleId: {
userId: user.id,
actionType: "LIKE",
maltArticleId: id,
},
},
});
if (action === null) {
await prisma.maltUserAction.create({
data: {
actionType: "LIKE",
userId: user.id,
maltArticleId: id,
},
});
}
return NextResponse.json(
{
message: "success!",
},
{ status: 200 }
);
} catch (e) {
return buildError(e);
}
};
投稿したレシピやお気に入りブックマークしたレシピを管理します。
下書きの状態もあるので編集したりしやすくすることや、投稿者名も表示するようにしているのでユーザーネームは変更できるようにしています。
メールアドレスも変更可能で、ログイン用のメールアドレスが変わることになりますが、そのような機能も作りました。
保存前に画像データの圧縮、使っていない画像は削除するようにしています。
ストレージには画像残しててもいいのですが、ゴミで容量食うのが嫌だったのでDBに保存する画像のみストレージに存在するようにしました。
画像を追加するとDBの保存に関わらずストレージに保存されるので、どうしようかなとロジック考えたのですが、ステート(string[])で画面上から削除された画像のurlを管理して、保存実行時にステートの配列の長さが1以上ならまとめて削除処理、キャンセルされたら元のデータ以外の条件でfilterかけて1以上ならまとめて削除処理するようにしました。
関連記事↓
色んな画面で、ログイン中か否か、またその記事が自分の記事なのか否かでボタンの出力の有無を変えるような処理がありました。
user情報はtokenの有無に関わらずリクエストする必要があるのでユーザー情報の取得をするAPIは下記のようにしました。
import { NextRequest, NextResponse } from "next/server";
import { IndexResponse } from "@/app/_types/Mypage/IndexResponse";
import { getCurrentUser } from "../_utils/getCurrentUser";
export const GET = async (request: NextRequest) => {
try {
const token = request.headers.get("Authorization");
if (!token) {
return NextResponse.json<IndexResponse>(
{
user: null,
},
{ status: 200 }
);
}
const { user, email } = await getCurrentUser({ request });
return NextResponse.json<IndexResponse>(
{
user,
email,
},
{ status: 200 }
);
} catch (e) {
if (e instanceof Error) {
return NextResponse.json({ error: e.message }, { status: 400 });
}
}
};
tokenなければリクエストは成功で、null返すという感じです。
そもそもリクエストしなければいいのですが、共通のフック使ってfetchしているのでリクエストはしてnull返すという方法にしました。
今回はpublicなAPIとprivateなAPIが混ざっているのでtokenあってもなくても一旦リクエストしてログイン中なのか否かでバックエンドの処理を変えるようにしました。
実務でやったらボコられることしてる可能性もあるので(正しいのかわからないw私にできるのは処理が成立するレベルなので・・)、参考程度にしてください!!
const { user, email } = await getCurrentUser({ request });
このuser
はユーザーに紐づく情報を全部含んでいます。
レスポンスは下記のような型です。
import { User, MaltArticle, RecipeArticle } from "@prisma/client";
export type UserData = {
user: User & {
// User型にmaltArticlesを含めた形にする
maltArticles: MaltArticle[];
recipeArticles: RecipeArticle[];
};
email: string | undefined;
};
export type IndexResponse = { user: null } | UserData;
emailはsupabaseから取得しているのでuserテーブルのレスポンスには含まれていません。
自分の記事なのかどうかはすべてuserオブジェクトで判断できるようにしました。
figmaの先生ともえさんが高速で作ってくださいました!!
コーディングをスムーズにすることを優先してUI変えてしまっている部分もありますが、フォントや配色はそのまま使わせていただきました🥺
私の苦手分野なのでホントに爆速で作ってくださり救われました!感謝です!
1日?2日?隙間時間使ってる感じでこのクオリティ・・神ですか?
もう少しデザインに寄せていきたいですw
関連記事↓
LPのようにしようと思っていましたが、レシピの閲覧は誰でも可能ということで、説明部分がかなり短めです。
また、こうすけさんから投稿どこからするかわからない、ログインしてもログイン済であることがわかりにくという声をいただき、説明部分はログイン済の時は表示しないようにし、投稿ボタンをトップページにも置きました。
キッチンでボウルのままとかフランパンのままとかの写真ばかりですみませんw
1歳の後追い息子と過ごしているのでご容赦ください🤣
麹調味料は必須項目で、公開されている麹調味料(ロールがMAIN)から選択します
ここに例えば醤油麹が二つとか出てきたら嫌なので麹調味料は申請制にしたという感じです・・!
検索機能は一旦レシピの一覧ページのみにしています。
ここではonChangeイベントで検索実行し、300ミリ秒でデバウンス処理、文字が入っていたら✕出現、ページ番号と検索ワードはURL直接変更で検索実行という、実務課題でやったままの感じで作りました。(コード丸っととってきたのは秘密)
このあたりはもう仮デザイン未満ですが、まぁよしとしてまたちょいちょい変えてみますw
まずはsupabase上で操作するの辞めたいので、投稿のステータス管理を行う管理画面を作りたいです。
モニター的にすでにともえさんとこうすけさんに使って頂いていて、「美味しそう!」とかコメントしたいって声をいただいたので(私が言わせたのか?)、コメントの機能も付けたいなって思っています!
盛り上がりそうでワクワクします・・🥰
学習アプリの方にも掛かりながらやっていて、その期間含めて4週間弱くらいの開発期間でしょうか・・リファクタの余地めちゃくちゃあるのでコード綺麗にする作業しつつ、まだまだ規模小さすぎると思うので機能も追加していきたいです。
閲覧だけでも使えるし、いいねは押せるので、ユーザー登録は手間やわという方にも使っていただけると思います!
塩麹等どこでも買えてお手軽な麹調味料もあるので手作りはできないという方も、麹調味料使って作ってみたって時はどんどん投稿してほしいです!
料理は家族の健康を支える大切な家事ですが、ホントに毎日大変なのでモチベーションが必要ですよね・・頑張れるきっかけになればいいなと思います💪
バグがあったら優しく教えてください・・
admin向け機能です。
コメント、コメントへの返信とコメントの通知(メール、プッシュ通知)
誰でも何度でも押せるいいねボタンにしました。
更新しました。