Repository: VRChatGram-developers/VRChatGram
Pull request #62: アカウント設定画面への導線と退会処理の修正
Author: login: Kurehei4216
Created: 2025-04-06T09:20:42Z | Merged: 2025-04-17T18:11:05Z

### 概要
アカウント設定画面の修正とアカウント削除の修正

### 実装内容
- アカウント設定画面から各種設定を変更できるようになったこと
  - メールアドレスの変更(Firebase)
  - パスワードの変更( Firebase)
- firebaseのパスワードとメールアドレスを変更できるようにした
- 退会処理を追加
  - usersテーブルのstatusをdeletedに変更


Diff:
diff --git a/nextjs/app/api/v1/users/account-settings/email/route.ts b/nextjs/app/api/v1/users/account-settings/email/route.ts
new file mode 100644
index 00000000..0352e884
--- /dev/null
+++ b/nextjs/app/api/v1/users/account-settings/email/route.ts
@@ -0,0 +1,63 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/libs/firebase/auth";
+import prisma from "@/prisma/client";
+import { updateUser } from "@/libs/firebase/firebase-auth-for-edge";
+
+export const runtime = "edge";
+
+const updateEmailToFirebase = async (uid: string, newEmail: string) => {
+  try {
+    await updateUser(uid, {
+      email: newEmail,
+    });
+    console.log("メールアドレスが更新されました:");
+  } catch (error) {
+    console.error("メールアドレスの更新に失敗しました:", error);
+  }
+};
+
+export async function PATCH(request: Request) {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    console.log("通ってる１");
+    const { currentEmail, newEmail } = await request.json();
+
+    const userByCurrentEmail = await prisma.users.findFirst({
+      where: {
+        email: currentEmail,
+      },
+    });
+
+    if (!userByCurrentEmail) {
+      return NextResponse.json({ error: "メールアドレスが見つかりません" }, { status: 404 });
+    }
+    if (currentEmail && newEmail) {
+      await prisma.$transaction(async (tx) => {
+        await updateEmailToFirebase(session.user.uid, newEmail);
+        await tx.users.update({
+          where: { id: userByCurrentEmail.id },
+          data: {
+            email: newEmail,
+          },
+        });
+      });
+    }
+
+    return NextResponse.json({ message: "メールアドレスが更新されました" }, { status: 200 });
+  } catch (error: unknown) {
+    if (error instanceof Error) {
+      console.error(error.message);
+      return NextResponse.json(
+        { error: `データベースへの接続に失敗しました: ${error.message}` },
+        { status: 500 }
+      );
+    }
+    return NextResponse.json({ error: "予期せぬエラーが発生しました" }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
diff --git a/nextjs/app/api/v1/users/account-settings/password/route.ts b/nextjs/app/api/v1/users/account-settings/password/route.ts
new file mode 100644
index 00000000..3ba7097d
--- /dev/null
+++ b/nextjs/app/api/v1/users/account-settings/password/route.ts
@@ -0,0 +1,74 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/libs/firebase/auth";
+import prisma from "@/prisma/client";
+import { signInWithEmailAndPassword } from "firebase/auth";
+import { auth as authClient } from "@/libs/firebase/client";
+import { updateUser } from "@/libs/firebase/firebase-auth-for-edge";
+
+export const runtime = "edge";
+
+const updatePasswordToFirebase = async (
+  uid: string,
+  email: string,
+  currentPassword: string,
+  newPassword: string
+) => {
+  // 現在のパスワードを比較する
+  const token = await signInWithEmailAndPassword(authClient, email, currentPassword);
+
+  if (!token) {
+    return;
+  }
+
+  try {
+    await updateUser(uid, {
+      password: newPassword,
+    });
+    console.log("パスワードが更新されました:");
+  } catch (error) {
+    console.error("パスワードの更新に失敗しました:", error);
+  }
+};
+
+export async function PATCH(request: Request) {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    const userByFetchedDb = await prisma.users.findFirst({
+      where: {
+        uid: session.user.uid,
+      },
+    });
+
+    if (!userByFetchedDb) {
+      return NextResponse.json({ error: "ユーザーが見つかりません" }, { status: 404 });
+    }
+
+    const { currentPassword, newPassword } = await request.json();
+
+    if (newPassword && currentPassword != newPassword) {
+      await updatePasswordToFirebase(
+        session.user.uid,
+        userByFetchedDb.email,
+        currentPassword,
+        newPassword
+      );
+    }
+
+    return NextResponse.json({ message: "パスワードが更新されました" }, { status: 200 });
+  } catch (error: unknown) {
+    if (error instanceof Error) {
+      console.error(error.message);
+      return NextResponse.json(
+        { error: `データベースへの接続に失敗しました: ${error.message}` },
+        { status: 500 }
+      );
+    }
+    return NextResponse.json({ error: "予期せぬエラーが発生しました" }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
diff --git a/nextjs/app/api/v1/users/account-settings/route.ts b/nextjs/app/api/v1/users/account-settings/route.ts
new file mode 100644
index 00000000..9949357e
--- /dev/null
+++ b/nextjs/app/api/v1/users/account-settings/route.ts
@@ -0,0 +1,87 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/libs/firebase/auth";
+import prisma from "@/prisma/client";
+
+export const runtime = "edge";
+
+export async function GET() {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    let currentUser;
+    if (session) {
+      currentUser = await prisma.users.findUnique({
+        where: {
+          uid: session.user.uid,
+        },
+        select: {
+          id: true,
+          email: true,
+          show_sensitive_type: true,
+          gender: true,
+        },
+      });
+    }
+
+    return NextResponse.json(currentUser);
+  } catch (error) {
+    return NextResponse.json({ error: `Failed to connect to database ${error}` }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+export async function PATCH(request: Request) {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    const { show_sensitive_type, gender } = await request.json();
+
+    const user = await prisma.users.update({
+      where: {
+        uid: session.user.uid,
+      },
+      data: {
+        show_sensitive_type: show_sensitive_type,
+        gender: gender,
+      },
+    });
+
+    return NextResponse.json(user);
+  } catch (error) {
+    if (error instanceof Error) {
+      console.error(error.message);
+    }
+    return NextResponse.json({ error: `Failed to connect to database ${error}` }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+export async function DELETE() {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    await prisma.users.update({
+      where: {
+        uid: session.user.uid,
+      },
+      data: {
+        status: "deleted",
+      },
+    });
+
+    return NextResponse.json("アカウントの削除に成功しました");
+  } catch (error) {
+    console.error("アカウントの削除に成功しましたに失敗しました:", error);
+  }
+}
diff --git a/nextjs/app/api/v1/users/check/password/route.ts b/nextjs/app/api/v1/users/check/password/route.ts
new file mode 100644
index 00000000..646ce525
--- /dev/null
+++ b/nextjs/app/api/v1/users/check/password/route.ts
@@ -0,0 +1,26 @@
+import { NextResponse } from "next/server";
+import prisma from "@/prisma/client";
+import { auth } from "@/libs/firebase/auth";
+import { auth as authClient } from "@/libs/firebase/client";
+import { signInWithEmailAndPassword } from "firebase/auth";
+
+export const runtime = "edge";
+
+export async function POST(request: Request) {
+  const { password } = await request.json();
+
+  const session = await auth();
+  if (!session) {
+    return NextResponse.json({ error: "ログインしていません" }, { status: 401 });
+  }
+
+  const userHasEmail = await prisma.users.findUniqueOrThrow({
+    where: { uid: session.user.uid },
+    select: {
+      email: true,
+    },
+  });
+
+  const token = await signInWithEmailAndPassword(authClient, userHasEmail.email, password);
+  return NextResponse.json({ isPasswordValid: token ? true : false });
+}
diff --git a/nextjs/app/notifications/page.tsx b/nextjs/app/notifications/page.tsx
index 0eee8ff6..dbbcaa8f 100644
--- a/nextjs/app/notifications/page.tsx
+++ b/nextjs/app/notifications/page.tsx
@@ -37,7 +37,7 @@ export default async function Page({ searchParams }: { searchParams: Promise<{ p
 
   // 公開日が過ぎていない通知をフィルタリング
   const notifications = response.contents.filter(
-    (notification: Notification) => notification.publishedAt > currentDateISOString
+    (notification: Notification) => notification.publishedAt < currentDateISOString
   );
 
   // 日付のフォーマット
diff --git a/nextjs/app/users/account-settings/page.tsx b/nextjs/app/users/account-settings/page.tsx
new file mode 100644
index 00000000..3973a7a9
--- /dev/null
+++ b/nextjs/app/users/account-settings/page.tsx
@@ -0,0 +1,11 @@
+import { AccountSetting } from "@/features/users/components/account-setting";
+import { fetchByAccountSettings } from "@/features/users/endpoint";
+import { headers } from "next/headers";
+export default async function Page() {
+  const user = await fetchByAccountSettings(await headers());
+  return (
+    <>
+      <AccountSetting accountSetting={user} />
+    </>
+  );
+}
diff --git a/nextjs/components/layouts/header.tsx b/nextjs/components/layouts/header.tsx
index 8c1ee5f4..31ca14b5 100644
--- a/nextjs/components/layouts/header.tsx
+++ b/nextjs/components/layouts/header.tsx
@@ -44,6 +44,11 @@ export const Header = () => {
       fetchUser();
     }
   }, [session]);
+  
+
+  const handleRedirectToAccountSettings = () => {
+    router.push("/users/account-settings");
+  };
 
   const handleToMyViewsPosts = () => {
     router.push(`/users/views`);
@@ -210,7 +215,12 @@ export const Header = () => {
                     <div className={styles.moduleDrawerMenuSection}>
                       <p>Language</p>
                       <p className={`${styles.moduleDrawerMenutext}`}>日本語</p>
-                      <p className={`${styles.moduleDrawerMenutext}`}>アカウント設定</p>
+                      <p
+                        className={`${styles.moduleDrawerMenutext}`}
+                        onClick={handleRedirectToAccountSettings}
+                      >
+                        アカウント設定
+                      </p>
                     </div>
                     <div className={styles.moduleDrawerMenuSection}>
                       <button className={`${styles.moduleDrawerMenutext}`}>ログアウト</button>
diff --git a/nextjs/features/home/components/popular-tag.tsx b/nextjs/features/home/components/popular-tag.tsx
index 9e1bbf4f..953785a0 100644
--- a/nextjs/features/home/components/popular-tag.tsx
+++ b/nextjs/features/home/components/popular-tag.tsx
@@ -2,15 +2,23 @@
 import Image from "next/image";
 import { Tag } from "../types/index";
 import styles from "../styles/popular-tag.module.scss";
+import { useSearchStore } from "@/libs/store/search-store";
 import { useRouter } from "next/navigation";
+import { createQueryParams } from "@/utils/queryParams";
 
 export const PopularTag = ({ popularTagList }: { popularTagList: Tag[] }) => {
   const router = useRouter();
+  const { setSearchQuery } = useSearchStore();
 
   const handleToPostSearchList = () => {
     router.push(`/posts?${new URLSearchParams({ page: "1" })}`);
   };
 
+  const redirectToPostSearchListByTagName = (tagName: string) => {
+    setSearchQuery(tagName);
+    router.push(`/posts?${createQueryParams({ tag: tagName, page: 1 })}`);
+  };
+
   return (
     <div className={styles.popularTagContainer}>
       <div className={styles.popularTagTitleWrapper}>
@@ -96,7 +104,10 @@ export const PopularTag = ({ popularTagList }: { popularTagList: Tag[] }) => {
               fill
               className={styles.tagImage}
             />
-            <div className={styles.tagListTextContainer}>
+            <div
+              className={styles.tagListTextContainer}
+              onClick={() => redirectToPostSearchListByTagName(tag.name)}
+            >
               <p className={styles.tagListText}>#{tag.name}</p>
             </div>
           </div>
diff --git a/nextjs/features/users/components/account-delete-modal.tsx b/nextjs/features/users/components/account-delete-modal.tsx
new file mode 100644
index 00000000..6837e335
--- /dev/null
+++ b/nextjs/features/users/components/account-delete-modal.tsx
@@ -0,0 +1,53 @@
+"use client";
+import { deleteAccount, checkPassword } from "../endpoint";
+import { logOutWithFirebaseAuth } from "@/libs/firebase/firebase-auth";
+import { useState } from "react";
+
+export const AccountDeleteModal = ({ onClose }: { onClose: () => void }) => {
+  const [password, setPassword] = useState("");
+  const [errorPasswordMessage, setPasswordErrorPasswordMessage] = useState("");
+
+  const handleDeleteAccount = async () => {
+    const isValidPassword = await checkPassword(password);
+
+    if (!isValidPassword) {
+      setPasswordErrorPasswordMessage("パスワードが正しくありません");
+      return;
+    }
+
+    try {
+      await deleteAccount();
+      logOutWithFirebaseAuth();
+    } catch {
+      console.error("削除に失敗しました");
+    }
+    onClose();
+  };
+
+  return (
+    <div className="flex flex-col items-center justify-center h-full bg-white">
+      <h1>アカウント削除</h1>
+      <p>削除すると、以下の情報がすべて失われます。</p>
+      <div>
+        <div>・プロフィール情報</div>
+        <div>・写真データ</div>
+        <div>・お気に入り</div>
+        <div>・フォロー/フォロワー</div>
+      </div>
+      <div>
+        <p>続けるには、VRCSSのパスワードを入力してください</p>
+      </div>
+      <div>
+        <input
+          type="password"
+          value={password}
+          onChange={(e) => {
+            setPassword(e.target.value);
+          }}
+        />
+      </div>
+      {errorPasswordMessage && <p className="text-red-500">{errorPasswordMessage}</p>}
+      <button onClick={handleDeleteAccount}>アカウントを削除</button>
+    </div>
+  );
+};
diff --git a/nextjs/features/users/components/account-setting.tsx b/nextjs/features/users/components/account-setting.tsx
new file mode 100644
index 00000000..976d01e3
--- /dev/null
+++ b/nextjs/features/users/components/account-setting.tsx
@@ -0,0 +1,146 @@
+"use client";
+
+import { useState } from "react";
+import { updateUser } from "../endpoint";
+import { useRouter } from "next/navigation";
+import { useModal } from "@/provider/modal-provider";
+import { AccountDeleteModal } from "./account-delete-modal";
+import { PasswordSetting } from "./password-setting";
+import { AccountSetting as AccountSettingType } from "../types/index";
+import { EmailSetting } from "./email-setting";
+import styles from "../styles/account-setting.module.scss";
+
+export const AccountSetting = ({ accountSetting }: { accountSetting: AccountSettingType }) => {
+  const { openModal, closeModal } = useModal();
+  const [showSensitiveType, setShowSensitiveType] = useState(accountSetting.show_sensitive_type);
+  const [gender, setGender] = useState(accountSetting.gender);
+  const router = useRouter();
+
+  const genderList = [
+    {
+      value: "male",
+      label: "男性",
+    },
+    {
+      value: "female",
+      label: "女性",
+    },
+    {
+      value: "other",
+      label: "どちらでもない",
+    },
+  ];
+
+  const handleShowSensitiveType = () => {
+    const newValue = showSensitiveType === "all" ? "safe" : "all";
+    setShowSensitiveType(newValue);
+  };
+
+  const handleUpdate = async () => {
+    try {
+      await updateUser({
+        show_sensitive_type: showSensitiveType,
+        gender,
+      });
+      router.refresh();
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
+  return (
+    <div className={styles.accountSettingContainer}>
+      <div className={styles.accountSettingTitle}>アカウント設定</div>
+      <div className={styles.accountSettingContentContainerWrapper}>
+        <div className={styles.accountSettingContentContainer}>
+          <div className={styles.emailSettingConatiner}>
+            <div className={styles.emailSettingLabel}>メールアドレス</div>
+            <div
+              className={styles.emailSettingChangeButton}
+              onClick={() =>
+                openModal(<EmailSetting onClose={closeModal} currentEmail={accountSetting.email} />)
+              }
+            >
+              <div>変更する</div>
+            </div>
+          </div>
+          <div className={styles.passwordSettingConatiner}>
+            <div className={styles.passwordSettingLabel}>パスワード</div>
+            <div
+              className={styles.passwordSettingChangeButton}
+              onClick={() => openModal(<PasswordSetting onClose={closeModal} />)}
+            >
+              <div>変更する</div>
+            </div>
+          </div>
+          <div className={styles.genderSettingContainer}>
+            <div className={styles.genderSettingLabel}>性別</div>
+            <div className={styles.genderSettingRadioButtonContainer}>
+              {genderList.map((genderItem) => (
+                <div key={genderItem.value} className={styles.genderSettingRadioButtonItem}>
+                  <input
+                    type="radio"
+                    value={genderItem.value}
+                    checked={gender == genderItem.value}
+                    onChange={(e) => setGender(e.target.value)}
+                  />
+                  <div className={styles.genderSettingRadioButtonLabel}>{genderItem.label}</div>
+                </div>
+              ))}
+            </div>
+          </div>
+
+          <div className={styles.sensitiveTypeSettingContainer}>
+            <div className={styles.sensitiveTypeSettingLabel}>
+              センシティブな内容の表示設定を変更する
+            </div>
+            <div className={styles.sensitiveTypeSettingRadioButtonContainer}>
+              <div
+                className={styles.sensitiveTypeSettingRadioButtonItem}
+                onClick={handleShowSensitiveType}
+                role="switch"
+                aria-checked={showSensitiveType === "all"}
+                tabIndex={0}
+                onKeyDown={(e) => {
+                  if (e.key === "Enter" || e.key === " ") handleShowSensitiveType();
+                }}
+              >
+                <div className={styles.sensitiveTypeSettingRadioButtonItemInputSlider}>
+                  <div
+                    className={`${styles.sliderThumb} ${
+                      showSensitiveType === "all" ? styles.sliderThumbChecked : ""
+                    }`}
+                  />
+                </div>
+                <div className={styles.sensitiveTypeSettingRadioButtonItemLabel}>
+                  センシティブな内容を表示する
+                </div>
+              </div>
+              <div className={styles.sensitiveTypeSettingRadioButtonItemInputDescription}>
+                VRCSSを閲覧する際に、R-18の投稿が表示されるようになります
+              </div>
+            </div>
+          </div>
+          <div className={styles.accountSettingSaveButtonContainer}>
+            <button
+              type="submit"
+              className={styles.accountSettingSaveButton}
+              onClick={handleUpdate}
+            >
+              設定を保存する
+            </button>
+          </div>
+        </div>
+
+        <div className={styles.accountSettingDeleteButtonContainer}>
+          <button
+            className={styles.accountSettingDeleteButton}
+            onClick={() => openModal(<AccountDeleteModal onClose={closeModal} />)}
+          >
+            アカウントを削除する
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/nextjs/features/users/components/email-setting.tsx b/nextjs/features/users/components/email-setting.tsx
new file mode 100644
index 00000000..a5261248
--- /dev/null
+++ b/nextjs/features/users/components/email-setting.tsx
@@ -0,0 +1,87 @@
+"use client";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { updateUserEmail } from "../endpoint";
+
+type EmailSettingProps = {
+  onClose: () => void;
+  currentEmail: string;
+};
+
+export const EmailSetting = ({ onClose, currentEmail }: EmailSettingProps) => {
+  const [email, setEmail] = useState(currentEmail);
+  const [newEmail, setNewEmail] = useState("");
+  const [errorEmail, setErrorEmail] = useState("");
+  const [errorNewEmail, setErrorNewEmail] = useState("");
+  const router = useRouter();
+
+  const isValidEmail = () => {
+    setErrorEmail("");
+    if (email === "") {
+      setErrorEmail("メールアドレスを入力してください");
+      return false;
+    }
+
+    const emailRegex = /^[a-zA-Z0-9._%+-]+@[^@.]+\.[^@]+$/;
+    if (!emailRegex.test(email)) {
+      setErrorEmail("正しいメールアドレスを入力してください");
+      return false;
+    }
+
+    return true;
+  };
+
+  const isValidNewEmail = () => {
+    setErrorNewEmail("");
+    if (newEmail === "") {
+      setErrorNewEmail("メールアドレスを入力してください");
+      return false;
+    }
+
+    const emailRegex = /^[a-zA-Z0-9._%+-]+@[^@.]+\.[^@]+$/;
+    if (!emailRegex.test(email)) {
+      setErrorEmail("正しいメールアドレスを入力してください");
+      return false;
+    }
+
+    return true;
+  };
+
+  const handleUpdateEmail = async () => {
+    const isValidEmailResult = isValidEmail();
+    const isValidNewEmailResult = isValidNewEmail();
+    if (!isValidEmailResult || !isValidNewEmailResult) {
+      return;
+    }
+
+    try {
+      await updateUserEmail(email, newEmail);
+    } catch (error) {
+      console.error(error);
+    }
+
+    onClose();
+    router.refresh();
+  };
+
+  return (
+    <div className="flex flex-col items-center justify-center h-full bg-white">
+      <div>メールアドレスの変更</div>
+      <div>
+        <div>現在のメールアドレス</div>
+        <div>
+          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
+          {errorEmail.length > 0 && <div>{errorEmail}</div>}
+        </div>
+      </div>
+      <div>
+        <div>新しいメールアドレス</div>
+        <div>
+          <input type="email" value={newEmail} onChange={(e) => setNewEmail(e.target.value)} />
+          {errorNewEmail.length > 0 && <div>{errorNewEmail}</div>}
+        </div>
+      </div>
+      <button onClick={handleUpdateEmail}>変更する</button>
+    </div>
+  );
+};
diff --git a/nextjs/features/users/components/password-setting.tsx b/nextjs/features/users/components/password-setting.tsx
new file mode 100644
index 00000000..8b24fe1c
--- /dev/null
+++ b/nextjs/features/users/components/password-setting.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { updateUserPassword } from "../endpoint";
+
+type PasswordSettingProps = {
+  onClose: () => void;
+};
+
+export const PasswordSetting = ({ onClose }: PasswordSettingProps) => {
+  const [currentPassword, setCurrentPassword] = useState("");
+  const [newPassword, setNewPassword] = useState("");
+  const [errorCurrentPassword, setErrorCurrentPassword] = useState("");
+  const [errorNewPassword, setErrorNewPassword] = useState("");
+  const router = useRouter();
+
+  const isValidCurrentPassword = () => {
+    setErrorCurrentPassword("");
+    if (currentPassword === "") {
+      return true;
+    }
+
+    if (currentPassword.length < 6) {
+      setErrorNewPassword("パスワードは6文字以上で入力してください");
+      return false;
+    }
+    return true;
+  };
+
+  const isValidNewPassword = () => {
+    setErrorNewPassword("");
+    if (newPassword === "") {
+      return true;
+    }
+
+    if (newPassword.length < 6) {
+      setErrorNewPassword("パスワードは6文字以上で入力してください");
+      return false;
+    }
+
+    // 半角英字、数字、記号をすべて含むかチェック
+    const hasLetter = /[a-zA-Z]/.test(newPassword);
+    const hasNumber = /[0-9]/.test(newPassword);
+    const hasSymbol = /[!@#$%^&*()_+{}\[\]:;<>,.?~\\/-]/.test(newPassword);
+
+    if (!(hasLetter && hasNumber && hasSymbol)) {
+      setErrorNewPassword("パスワードは半角英字、数字、記号を含めてください");
+      return false;
+    }
+    return true;
+  };
+
+  const handleUpdatePassword = async () => {
+    const isValidCurrentPasswordResult = isValidCurrentPassword();
+    const isValidNewPasswordResult = isValidNewPassword();
+    if (!isValidCurrentPasswordResult || !isValidNewPasswordResult) {
+      return;
+    }
+
+    try {
+      await updateUserPassword(currentPassword, newPassword);
+    } catch (error) {
+      console.error(error);
+    }
+
+    onClose();
+    router.refresh();
+  };
+
+  return (
+    <div className="flex flex-col items-center justify-center h-full bg-white">
+      <div>パスワードの変更</div>
+      <div>
+        <div>現在のパスワード</div>
+        <div>
+          <input
+            type="password"
+            value={currentPassword}
+            onChange={(e) => setCurrentPassword(e.target.value)}
+          />
+          {errorCurrentPassword.length > 0 && <div>{errorCurrentPassword}</div>}
+        </div>
+      </div>
+      <div>
+        <div>新しいパスワード</div>
+        <div>
+          <input
+            type="password"
+            value={newPassword}
+            onChange={(e) => setNewPassword(e.target.value)}
+          />
+          {errorNewPassword.length > 0 && <div>{errorNewPassword}</div>}
+        </div>
+      </div>
+      <button onClick={handleUpdatePassword}>変更する</button>
+    </div>
+  );
+};
diff --git a/nextjs/features/users/components/users.tsx b/nextjs/features/users/components/users.tsx
index dab37a20..0af759f0 100644
--- a/nextjs/features/users/components/users.tsx
+++ b/nextjs/features/users/components/users.tsx
@@ -181,7 +181,7 @@ export const Users = ({ user }: { user: User }) => {
                 <p>{user.totalViews}閲覧</p>
               </div>
               <div className={styles.profilePostCount}>
-                <p>投稿{user.posts[0].length}件</p>
+                <p>投稿{user.posts[0]?.length ?? 0}件</p>
               </div>
               <div className={styles.profileLikeCount}>
                 <p>{user.totalLikes} いいね</p>
diff --git a/nextjs/features/users/endpoint.ts b/nextjs/features/users/endpoint.ts
index 63f5c8f0..46c5fee4 100644
--- a/nextjs/features/users/endpoint.ts
+++ b/nextjs/features/users/endpoint.ts
@@ -5,6 +5,7 @@ import {
   ViewsPostList,
   requestCreateUser,
   requestUpdateUserProfile,
+  requestUpdateUser
 } from "./types/index";
 import { createUserWithEmailAndPassword } from "firebase/auth";
 import { auth } from "@/libs/firebase/client";
@@ -109,6 +110,91 @@ export const checkEmail = async (email: string): Promise<boolean | string> => {
   return data.isRegisteredEnail;
 };
 
+export const fetchByAccountSettings = async (headers: Headers) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    headers: headers,
+  });
+  if (!response.ok) {
+    throw new Error("Failed to fetch account settings");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const updateUser = async (user: requestUpdateUser) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    method: "PATCH",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify(user),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const updateUserEmail = async (currentEmail: string, newEmail: string) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings/email`, {
+    method: "PATCH",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({ currentEmail: currentEmail, newEmail: newEmail }),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const updateUserPassword = async (currentPassword: string, newPassword: string) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings/password`, {
+    method: "PATCH",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({ currentPassword: currentPassword, newPassword: newPassword }),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const checkPassword = async (password: string) => {
+  const response = await fetch(`${API_URL}/api/v1/users/check/password`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({ password }),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const deleteAccount = async () => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    method: "DELETE",
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
 export const checkDuplicateMyId = async (myId: string): Promise<boolean | string> => {
   const response = await fetch(`${API_URL}/api/v1/users/check_duplicate_my_id`, {
     method: "POST",
@@ -173,4 +259,4 @@ export const fetchMyLikePostList = async (headers: Headers): Promise<FavoritePos
   }
   const data = await response.json();
   return data;
-};
\ No newline at end of file
+};
diff --git a/nextjs/features/users/styles/account-setting.module.scss b/nextjs/features/users/styles/account-setting.module.scss
new file mode 100644
index 00000000..f9fa773e
--- /dev/null
+++ b/nextjs/features/users/styles/account-setting.module.scss
@@ -0,0 +1,198 @@
+.accountSettingContainer {
+    display: flex;
+    flex-direction: column;
+    gap: 1.5rem;
+    padding: 6rem 0;
+    align-items: center;
+
+    .accountSettingTitle {
+        justify-self: center;
+        font-size: 2rem;
+        font-weight: bold;
+        font-family: "Noto Sans JP";
+    }
+
+    .accountSettingContentContainerWrapper {
+        display: grid;
+        grid-template-columns: 1fr;
+        gap: 2rem;
+        border-radius: 1rem;
+
+
+        .accountSettingContentContainer {
+            padding: 1.5rem 8rem 1.5rem 8rem;
+            background-color: #fff;
+            display: grid;
+            gap: 1rem;
+
+            .emailSettingConatiner {
+                display: flex;
+                align-items: center;
+                gap: 1.5rem;
+
+                .emailSettingLabel {
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                }
+
+                .emailSettingChangeButton {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                    cursor: pointer;
+                    background-color: #69beef;
+                    color: white;
+                    border-radius: 2rem;
+                    padding: 0.5rem 1rem;
+                }
+            }
+
+            .passwordSettingConatiner {
+                display: flex;
+                align-items: center;
+                gap: 1.5rem;
+
+                .passwordSettingLabel {
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                }
+
+                .passwordSettingChangeButton {
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                    cursor: pointer;
+                    background-color: #69beef;
+                    color: white;
+                    border-radius: 2rem;
+                    padding: 0.5rem 1rem;
+                }
+            }
+
+            .genderSettingContainer {
+                display: grid;
+                grid-template-columns: 1fr;
+                gap: 0.5rem;
+
+                .genderSettingLabel {
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                }
+
+                .genderSettingRadioButtonContainer {
+                    font-size: 1rem;
+                    display: flex;
+                    align-items: center;
+                    gap: 3rem;
+
+                    .genderSettingRadioButtonItem {
+                        display: flex;
+                        gap: 0.5rem;
+                    }
+                }
+            }
+
+            .sensitiveTypeSettingContainer {
+                display: grid;
+                grid-template-columns: 1fr;
+                gap: 1rem;
+
+                .sensitiveTypeSettingLabel {
+                    font-size: 1rem;
+                    font-weight: bold;
+                    font-family: "Noto Sans JP";
+                }
+
+                .sensitiveTypeSettingRadioButtonItem {
+                    display: inline-flex;
+                    align-items: center;
+                    cursor: pointer;
+                    outline: none;
+                }
+
+                .sensitiveTypeSettingRadioButtonItemInputSlider {
+                    position: relative;
+                    width: 44px;
+                    height: 24px;
+                    background-color: #ccc;
+                    border-radius: 9999px;
+                    transition: background-color 0.3s;
+                    flex-shrink: 0;
+                }
+
+                .sliderThumb {
+                    position: absolute;
+                    top: 2px;
+                    left: 2px;
+                    width: 20px;
+                    height: 20px;
+                    background-color: white;
+                    border-radius: 50%;
+                    transition: transform 0.3s;
+                    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+                }
+
+                .sliderThumbChecked {
+                    transform: translateX(20px);
+                }
+
+                .sensitiveTypeSettingRadioButtonItem[aria-checked="true"] .sensitiveTypeSettingRadioButtonItemInputSlider {
+                    background-color: #69beef; // red-500
+                }
+
+                .sensitiveTypeSettingRadioButtonItemInputLabel {
+                    margin-left: 0.5rem;
+                    font-size: 0.8rem;
+                    font-family: "Noto Sans JP";
+                }
+            }
+
+            .sensitiveTypeSettingRadioButtonItemInputDescription {
+                font-size: 0.8rem;
+                font-family: "Noto Sans JP";
+            }
+        }
+
+        .accountSettingSaveButtonContainer {
+            margin-top: 5rem;
+            background-color: #9BEA4D;
+            color: white;
+            border-radius: 2rem;
+            cursor: pointer;
+            display: flex;
+            justify-content: center;
+            font-size: 1rem;
+            font-weight: bold;
+            font-family: "Noto Sans JP";
+
+            .accountSettingSaveButton {
+                padding: 0.5rem 1rem;
+            }
+        }
+
+        .accountSettingDeleteButtonContainer {
+            width: 100%;
+            display: flex;
+            justify-content: flex-end;
+            font-size: 1rem;
+            font-weight: bold;
+            font-family: "Noto Sans JP";
+            cursor: pointer;
+
+            .accountSettingDeleteButton {
+                background-color: #D9D9D9;
+                border-radius: 2rem;
+                padding: 0.5rem 1rem;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/nextjs/features/users/types/index.ts b/nextjs/features/users/types/index.ts
index 33831e25..9c8446e5 100644
--- a/nextjs/features/users/types/index.ts
+++ b/nextjs/features/users/types/index.ts
@@ -25,6 +25,18 @@ export type Post = {
   user: User;
 };
 
+export type AccountSetting = {
+  email: string;
+  show_sensitive_type: string;
+  gender: string;
+  currentPassword: string;
+  newPassword: string;
+};
+
+export type requestUpdateUser = {
+  show_sensitive_type: string;
+  gender: string;
+};
 export type Image = {
   id: bigint;
   url: string;
@@ -85,4 +97,4 @@ export type FavoritePostList = {
   totalPages: number;
   posts: Post[][];
   postCount: number;
-};
\ No newline at end of file
+};
diff --git a/nextjs/libs/firebase/firebase-auth-for-edge.ts b/nextjs/libs/firebase/firebase-auth-for-edge.ts
index cf61283d..9f08a52a 100644
--- a/nextjs/libs/firebase/firebase-auth-for-edge.ts
+++ b/nextjs/libs/firebase/firebase-auth-for-edge.ts
@@ -13,10 +13,10 @@ export const {
   verifyAndRefreshExpiredIdToken,
   setCustomUserClaims,
 } = getFirebaseAuth({
-  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
+  apiKey: process.env.FIREBASE_API_KEY ?? "",
   serviceAccount: {
-    projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
-    clientEmail: process.env.NEXT_PUBLIC_CLIENT_EMAIL!,
-    privateKey: process.env.NEXT_PUBLIC_PRIVATE_KEY!,
+    projectId: process.env.FIREBASE_PROJECT_ID?? "",
+    clientEmail: process.env.FIREBASE_CLIENT_EMAIL ?? "",
+    privateKey: process.env.FIREBASE_PRIVATE_KEY ? process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n") : "",
   },
 });


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**nextjs/features/users/types/index.ts:61**
* [nitpick] Consider renaming 'requestUpdateUser' to 'RequestUpdateUser' to follow typical PascalCase conventions for type names.
```
export type requestUpdateUser = {
```
**nextjs/features/users/endpoint.ts:140**
* The error message in checkPassword should accurately reflect the operation; consider changing it to 'Failed to check password'.
```
throw new Error("Failed to update user");
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T10:53:55Z

Review comments:
diff_hunk: @@ -102,6 +102,61 @@ export const checkEmail = async (email: string): Promise<boolean | string> => {
   return data.isRegisteredEnail;
 };
 
+export const fetchByAccountSettings = async (headers: Headers) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    headers: headers,
+  });
+  if (!response.ok) {
+    throw new Error("Failed to fetch account settings");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const updateUser = async (user: requestUpdateUser) => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    method: "PATCH",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify(user),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const checkPassword = async (password: string) => {
+  const response = await fetch(`${API_URL}/api/v1/users/check/password`, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({ password }),
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
+  }
+  const data = await response.json();
+  return data;
+};
+
+export const deleteAccount = async () => {
+  const response = await fetch(`${API_URL}/api/v1/users/account-settings`, {
+    method: "DELETE",
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+  if (!response.ok) {
+    throw new Error("Failed to update user");
path: nextjs/features/users/endpoint.ts
user: login: Copilot
body: The error message in deleteAccount likely should be updated to 'Failed to delete account' to better describe the failure.
```suggestion
    throw new Error("Failed to delete account");
```
created_at: 2025-04-06T10:53:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,189 @@
+"use client";
+
+import { useState } from "react";
+import { updateUser } from "../endpoint";
+import { useRouter } from "next/navigation";
+import { AccountSetting } from "../types";
+import { useModal } from "@/provider/modal-provider";
+import { AccountDeleteModal } from "./account-delete-modal";
+export const AccountEdit = ({ accountSetting }: { accountSetting: AccountSetting }) => {
+  const [email, setEmail] = useState(accountSetting.email);
+  const { openModal, closeModal } = useModal();
+  const [showSensitiveType, setShowSensitiveType] = useState(accountSetting.show_sensitive_type);
+  const [gender, setGender] = useState(accountSetting.gender);
+  const [currentPassword, setCurrentPassword] = useState(accountSetting.currentPassword || "");
+  const [newPassword, setNewPassword] = useState("");
+  const [errorEmail, setErrorEmail] = useState("");
+  const [errorCurrentPassword, setErrorCurrentPassword] = useState("");
+  const [errorNewPassword, setErrorNewPassword] = useState("");
+  const router = useRouter();
+
+  const genderList = [
+    {
+      value: "male",
+      label: "男性",
+    },
+    {
+      value: "female",
+      label: "女性",
+    },
+    {
+      value: "other",
+      label: "どちらでもない",
+    },
+  ];
+
+  const handleShowSensitiveType = () => {
+    const newValue = showSensitiveType === "all" ? "safe" : "all";
+    setShowSensitiveType(newValue);
+  };
+
+  const isValidEmail = () => {
+    setErrorEmail("");
+    if (email === "") {
+      setErrorEmail("メールアドレスを入力してください");
+      return false;
+    }
+
+    const emailRegex = /^[a-zA-Z0-9._%+-]+@[^@.]+\.[^@]+$/;
+    if (!emailRegex.test(email)) {
+      setErrorEmail("正しいメールアドレスを入力してください");
+      return false;
+    }
+
+    return true;
+  };
+
+  const isValidCurrentPassword = () => {
+    setErrorCurrentPassword("");
+    if (currentPassword === "") {
+      return true;
+    }
+
+    if (currentPassword.length < 6) {
+      setErrorNewPassword("パスワードは6文字以上で入力してください");
path: nextjs/features/users/components/account-edit.tsx
user: login: Copilot
body: In isValidCurrentPassword, the error setter should be setErrorCurrentPassword instead of setErrorNewPassword to correctly report the validation error.
```suggestion
      setErrorCurrentPassword("パスワードは6文字以上で入力してください");
```
created_at: 2025-04-06T10:53:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/libs/firebase/auth";
+import prisma from "@/prisma/client";
+import { signInWithEmailAndPassword } from "firebase/auth";
+import { auth as authClient } from "@/libs/firebase/client";
+import { updateUser } from "@/libs/firebase/firebase-auth-for-edge";
+
+export const runtime = "edge";
+
+const updateEmailToFirebase = async (uid: string, newEmail: string) => {
+  try {
+    await updateUser(uid, {
+      email: newEmail,
+    });
+    console.log("メールアドレスが更新されました:");
+  } catch (error) {
+    console.error("メールアドレスの更新に失敗しました:", error);
+  }
+};
+
+const updatePasswordToFirebase = async (
+  uid: string,
+  email: string,
+  currentPassword: string,
+  newPassword: string
+) => {
+  // 現在のパスワードを比較する
+  const token = signInWithEmailAndPassword(authClient, email, currentPassword);
+
path: nextjs/app/api/v1/users/account-settings/route.ts
user: login: Copilot
body: The call to signInWithEmailAndPassword should be awaited to ensure proper token resolution before proceeding.
```suggestion
  const token = await signInWithEmailAndPassword(authClient, email, currentPassword);
```
created_at: 2025-04-06T10:53:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,139 @@
+import { NextResponse } from "next/server";
+import { auth } from "@/libs/firebase/auth";
+import prisma from "@/prisma/client";
+import { signInWithEmailAndPassword } from "firebase/auth";
+import { auth as authClient } from "@/libs/firebase/client";
+import { updateUser } from "@/libs/firebase/firebase-auth-for-edge";
+
+export const runtime = "edge";
+
+const updateEmailToFirebase = async (uid: string, newEmail: string) => {
+  try {
+    await updateUser(uid, {
+      email: newEmail,
+    });
+    console.log("メールアドレスが更新されました:");
+  } catch (error) {
+    console.error("メールアドレスの更新に失敗しました:", error);
+  }
+};
+
+const updatePasswordToFirebase = async (
+  uid: string,
+  email: string,
+  currentPassword: string,
+  newPassword: string
+) => {
+  // 現在のパスワードを比較する
+  const token = signInWithEmailAndPassword(authClient, email, currentPassword);
+
+  if (!token) {
+    return;
+  }
+
+  try {
+    await updateUser(uid, {
+      password: newPassword,
+    });
+    console.log("パスワードが更新されました:");
+  } catch (error) {
+    console.error("パスワードの更新に失敗しました:", error);
+  }
+};
+
+export async function GET() {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    let currentUser;
+    if (session) {
+      currentUser = await prisma.users.findUnique({
+        where: {
+          uid: session.user.uid,
+        },
+        select: {
+          id: true,
+          email: true,
+          show_sensitive_type: true,
+          gender: true,
+        },
+      });
+    }
+
+    return NextResponse.json(currentUser);
+  } catch (error) {
+    return NextResponse.json({ error: `Failed to connect to database ${error}` }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+export async function PATCH(request: Request) {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    const { email, show_sensitive_type, gender, currentPassword, newPassword } =
+      await request.json();
+
+    const userByFetchedDb = await prisma.users.findFirst({
+      where: {
+        uid: session.user.uid,
+      },
+    });
+
+    // メールアドレスのパラメーターが存在または、DBとのメールアドレスを比較して変更があった場合、更新する
+    if (email && userByFetchedDb?.email != email) {
+      await updateEmailToFirebase(session.user.uid, email);
+    }
+
+    if (newPassword && currentPassword != newPassword) {
+      await updatePasswordToFirebase(session.user.uid, email, currentPassword, newPassword);
+    }
+
+    const user = await prisma.users.update({
+      where: {
+        uid: session.user.uid,
+      },
+      data: {
+        email: email,
+        show_sensitive_type: show_sensitive_type,
+        gender: gender,
+      },
+    });
+
+    return NextResponse.json(user);
+  } catch (error) {
+    console.error(error.message);
+    return NextResponse.json({ error: `Failed to connect to database ${error}` }, { status: 500 });
+  } finally {
+    await prisma.$disconnect();
+  }
+}
+
+export async function DELETE(request: Request) {
+  try {
+    const session = await auth();
+    if (!session) {
+      return NextResponse.json({ error: "ユーザーがログインしていません" }, { status: 401 });
+    }
+
+    await prisma.users.update({
+      where: {
+        uid: session.user.uid,
+      },
+      data: {
+        status: "deleted",
+      },
+    });
+
+    return NextResponse.json("アカウントの削除に成功しました");
+  } catch (error) {
+    console.error("アカウントの削除に成功しましたに失敗しました:", error);
path: nextjs/app/api/v1/users/account-settings/route.ts
user: login: Copilot
body: The error message appears to contain a typo; consider revising it to 'アカウントの削除に失敗しました:' for clarity.
```suggestion
    console.error("アカウントの削除に失敗しました:", error);
```
created_at: 2025-04-06T10:53:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-06T09:20:47Z
body: [vc]: #UEvurtS1+RFgAyv4mItX2p/xtO+UiytXFo7WQSdzxns=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ2cmNzcyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ2cmNzcy1naXQtZmVhdHVyZS1jcmVhdGUtYWNjb3V0LWVkaXQtcGFnZS1yZWNyZWF0ZS12cmNzcy10ZWFtLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3ZyY3NzLXRlYW0vdnJjc3MveDc0ekQ3NlBDa2hzVXh2WlhWNHdnMnY4SmZaSiIsInByZXZpZXdVcmwiOiJ2cmNzcy1naXQtZmVhdHVyZS1jcmVhdGUtYWNjb3V0LWVkaXQtcGFnZS1yZWNyZWF0ZS12cmNzcy10ZWFtLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjoibmV4dGpzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **vrcss** | ✅ Ready ([Inspect](https://vercel.com/vrcss-team/vrcss/x74zD76PCkhsUxvZXV4wg2v8JfZJ)) | [Visit Preview](https://vrcss-git-feature-create-accout-edit-page-recreate-vrcss-team.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vrcss-git-feature-create-accout-edit-page-recreate-vrcss-team.vercel.app?via=pr-comment-feedback-link) | Apr 17, 2025 6:12pm |


author_association: NONE
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-06T09:20:50Z
body: ## Deploying vrchatgram with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>67d204c</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>⚡️&nbsp; Build in progress...</td></tr>
</table>

[View logs](https://dash.cloudflare.com/840665956265a75bb65de632c92b16e7/pages/view/vrchatgram/32d8abaa-85b9-4c64-a691-6d48bca3339b)

author_association: NONE