diff --git a/__test__/LandingSection.test.tsx b/__test__/LandingSection.test.tsx
deleted file mode 100644
index 1f90126..0000000
--- a/__test__/LandingSection.test.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { render, screen, act } from "@testing-library/react";
-import LandingSection from "app/components/LandingSection";
-import { mockData } from "./mock/mock_CmsData";
-
-describe("", () => {
- test("LandingSectionコンポーネントが正しく表示されるかのテスト", async () => {
- render();
-
- await act(async () => {});
-
- expect(screen.getByText(mockData.title || "")).toBeInTheDocument();
- });
-
- test("アプリケーション名がレンダリングされる", async () => {
- render();
-
- await act(async () => {});
-
- expect(
- screen.getByText(mockData.applicationName || "")
- ).toBeInTheDocument();
- });
-
- test("各サブタイトルとテキストがレンダリングされる", async () => {
- render();
-
- await act(async () => {});
-
- expect(screen.getByText(mockData.subTitle1 || "")).toBeInTheDocument();
- expect(screen.getByText(mockData.text1)).toBeInTheDocument();
- expect(screen.getByText(mockData.subTitle2 || "")).toBeInTheDocument();
- expect(screen.getByText(mockData.text2)).toBeInTheDocument();
- expect(screen.getByText(mockData.subTitle3 || "")).toBeInTheDocument();
- expect(screen.getByText(mockData.text3)).toBeInTheDocument();
- });
-
- test("最後のメッセージがレンダリングされる", async () => {
- await act(async () => {
- render();
- });
- expect(screen.getByText(mockData.lastMessage || "")).toBeInTheDocument();
- });
-
- test("画像が正しいsrc属性でレンダリングされる", async () => {
- render();
-
- const imageElement = screen.getByAltText("Home");
- const imageElement_img1 = screen.getByAltText("Image 1");
- const imageElement_img2 = screen.getByAltText("Image 2");
- const imageElement_img3 = screen.getByAltText("Image 3");
-
- await act(async () => {});
-
- expect(imageElement).toHaveAttribute("src");
- expect(imageElement.getAttribute("src")).toContain(
- encodeURIComponent(mockData.icon?.url ?? "")
- );
-
- expect(imageElement_img1).toHaveAttribute("src");
- expect(imageElement_img1.getAttribute("src")).toContain(
- mockData.img1?.url ?? ""
- );
-
- expect(imageElement_img2).toHaveAttribute("src");
- expect(imageElement_img2.getAttribute("src")).toContain(
- mockData.img2?.url ?? ""
- );
-
- expect(imageElement_img3).toHaveAttribute("src");
- expect(imageElement_img3.getAttribute("src")).toContain(
- mockData.img3?.url ?? ""
- );
- });
-});
diff --git a/__test__/UpdatePage.test.tsx b/__test__/UpdatePage.test.tsx
deleted file mode 100644
index 3836abe..0000000
--- a/__test__/UpdatePage.test.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { render, waitFor, screen, act } from "@testing-library/react";
-import { config } from "lib/config";
-import UpdatePage from "app/updatePlan/[id]/page";
-import { mockData } from "./mock/mock_UpdatePage";
-
-jest.mock("next/headers", () => ({
- headers: jest.fn(() => ({
- get: jest.fn(() => "test-host"),
- })),
-}));
-
-jest.mock("next/navigation", () => ({
- useRouter: jest.fn(),
-}));
-
-global.fetch = jest.fn(); //fetch関数をモック化
-
-describe("UpdatePageコンポーネントに関するテスト", () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- test("APIから正しくデータを取得する", async () => {
- (global.fetch as jest.Mock).mockResolvedValueOnce({
- json: jest.fn().mockResolvedValueOnce(mockData),
- });
-
- render(await UpdatePage({ params: { id: 1 } }));
- await act(async () => {});
-
- await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledWith(
- `${config.apiPrefix}test-host/api/plan/update/1`, //仮としてidが1のデータを取得する
- expect.objectContaining({
- cache: "no-store",
- method: "GET",
- headers: {
- "x-api-key": expect.any(String),
- },
- })
- );
- });
- });
-
- test("UpdatePageCoreに正しいpropsを渡し、それが画面に表示されていることを確認する", async () => {
- (global.fetch as jest.Mock).mockResolvedValueOnce({
- json: jest.fn().mockResolvedValueOnce(mockData),
- });
-
- render(await UpdatePage({ params: { id: 1 } }));
-
- await act(async () => {});
-
- expect(screen.getByDisplayValue("テストタイトル")).toBeInTheDocument();
- expect(screen.getByDisplayValue("テスト内容")).toBeInTheDocument();
- expect(screen.getByDisplayValue("講座1")).toBeInTheDocument();
- expect(screen.getByDisplayValue("講座2")).toBeInTheDocument();
- });
-});
diff --git a/app/allPost/[id]/components/CourseReview.tsx b/app/allPost/[id]/components/CourseReview.tsx
index 9e33a1e..3deb1af 100644
--- a/app/allPost/[id]/components/CourseReview.tsx
+++ b/app/allPost/[id]/components/CourseReview.tsx
@@ -1,5 +1,4 @@
"use client";
-import useUser from "app/hooks/useUser";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
@@ -9,8 +8,9 @@ import toast from "react-hot-toast";
interface CourseReviewProps {
id: number;
+ auth_id: string;
}
-const CourseReview = ({ id }: CourseReviewProps) => {
+const CourseReview = ({ id, auth_id }: CourseReviewProps) => {
const {
register,
handleSubmit,
@@ -21,7 +21,6 @@ const CourseReview = ({ id }: CourseReviewProps) => {
authorId: 0,
},
});
- const { user } = useUser();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
@@ -36,8 +35,8 @@ const CourseReview = ({ id }: CourseReviewProps) => {
method: "POST",
body: JSON.stringify({
title: data.title,
- id: Number(id),
- authorId: user?.id,
+ planId: Number(id),
+ auth_id: auth_id,
}),
headers: {
"Content-Type": "application/json",
diff --git a/app/allPost/[id]/components/ReviewSection.tsx b/app/allPost/[id]/components/ReviewSection.tsx
index 4ecbcd2..3bf9f5c 100644
--- a/app/allPost/[id]/components/ReviewSection.tsx
+++ b/app/allPost/[id]/components/ReviewSection.tsx
@@ -3,8 +3,9 @@ import ParticleReview from "./ParticleReview";
interface ReviewSectionProps {
id: number;
+ auth_id: string;
}
-const ReviewSection = ({ id }: ReviewSectionProps) => {
+const ReviewSection = ({ id, auth_id }: ReviewSectionProps) => {
return (
<>
@@ -13,7 +14,7 @@ const ReviewSection = ({ id }: ReviewSectionProps) => {
>
diff --git a/app/allPost/[id]/page.tsx b/app/allPost/[id]/page.tsx
index d6cb4e6..55c6967 100644
--- a/app/allPost/[id]/page.tsx
+++ b/app/allPost/[id]/page.tsx
@@ -6,6 +6,7 @@ import UserInfoCard from "./components/UserInfoCard";
import PlanDetails from "./components/PlanDetails";
import CourseListSection from "./components/CourseListSection";
import ReviewSection from "./components/ReviewSection";
+import useSeverUser from "app/hooks/useSeverUser";
async function getDetailData(id: number, host: string) {
const res = await fetch(`${config.apiPrefix}${host}/api/plan/${id}`, {
@@ -21,6 +22,8 @@ const SpecificPage = async ({ params }: { params: { id: number } }) => {
const host = headers().get("host");
const CourseData = await getDetailData(params.id, host!);
const { title, content, user, courses } = CourseData;
+ const { session } = useSeverUser();
+ const auth_id = await session();
return (
<>
@@ -58,7 +61,7 @@ const SpecificPage = async ({ params }: { params: { id: number } }) => {
{/* レビューセクション */}
-
+
>
);
diff --git a/app/api/auth/[id]/route.ts b/app/api/auth/[id]/route.ts
new file mode 100644
index 0000000..d0b2bb8
--- /dev/null
+++ b/app/api/auth/[id]/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from "next/server";
+import prisma from "utils/prisma/prismaClient";
+
+//サーバーサイドからプロフィール情報を取得するAPI(動作確認済み)
+export const GET = async (req: NextRequest) => {
+ try {
+ await prisma.$connect();
+ const auth_id: string = req.url.split("/auth/")[1];
+ const user = await prisma.user.findUnique({
+ where: { auth_id },
+ select: {
+ id: true,
+ auth_id: true,
+ name: true,
+ email: true,
+ university: true,
+ faculty: true,
+ department: true,
+ grade: true,
+ },
+ });
+ return NextResponse.json({ message: "Success", user }, { status: 200 });
+ } catch (error) {
+ return NextResponse.json({ message: "Error", error }, { status: 500 });
+ } finally {
+ await prisma.$disconnect();
+ }
+};
diff --git a/app/api/auth/confirm/route.ts b/app/api/auth/confirm/route.ts
new file mode 100644
index 0000000..b77494e
--- /dev/null
+++ b/app/api/auth/confirm/route.ts
@@ -0,0 +1,28 @@
+import { type EmailOtpType } from "@supabase/supabase-js";
+import { type NextRequest } from "next/server";
+
+import { redirect } from "next/navigation";
+import { createClient } from "utils/supabase/sever";
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+ const token_hash = searchParams.get("token_hash");
+ const type = searchParams.get("type") as EmailOtpType | null;
+ const next = searchParams.get("next") ?? "/";
+
+ if (token_hash && type) {
+ const supabase = createClient();
+
+ const { error } = await supabase.auth.verifyOtp({
+ type,
+ token_hash,
+ });
+ if (!error) {
+ // redirect user to specified redirect URL or root of app
+ redirect(next);
+ }
+ }
+
+ // redirect the user to an error page with some instructions
+ redirect("/error");
+}
diff --git a/app/api/plan/detail/[id]/route.ts b/app/api/plan/detail/[id]/route.ts
index ef01bea..61b8774 100644
--- a/app/api/plan/detail/[id]/route.ts
+++ b/app/api/plan/detail/[id]/route.ts
@@ -5,13 +5,14 @@ export async function GET(
req: Request,
{ params }: { params: { id: string } }
) {
- const userId = params.id;
+ const auth_id = params.id;
const CourseDetailData = await prisma.user.findUnique({
where: {
- id: parseInt(userId),
+ auth_id: auth_id,
},
- include: {
+ select: {
plans: true,
+ auth_id: true,
},
});
return NextResponse.json(CourseDetailData);
diff --git a/app/api/plan/route.ts b/app/api/plan/route.ts
index 3720bde..c1d72e0 100644
--- a/app/api/plan/route.ts
+++ b/app/api/plan/route.ts
@@ -4,9 +4,23 @@ import { NextResponse } from "next/server";
// Plan投稿用API
export const POST = async (req: Request) => {
try {
- const { title, content, userId } = await req.json();
+ const { title, content, auth_id } = await req.json();
await prisma.$connect();
+
+ const getUser = await prisma.user.findUnique({
+ where: {
+ auth_id: auth_id,
+ },
+ select: {
+ id: true,
+ },
+ });
+ if (!getUser?.id) {
+ return NextResponse.json({ message: "User not found" }, { status: 404 });
+ }
+ const userId = getUser.id;
+
const post = await prisma.plan.create({
data: {
title,
diff --git a/app/api/post/[id]/route.ts b/app/api/post/[id]/route.ts
new file mode 100644
index 0000000..dd0b419
--- /dev/null
+++ b/app/api/post/[id]/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from "next/server";
+import prisma from "utils/prisma/prismaClient";
+
+// ユーザーが投稿したレビューを取得するAPI
+export async function GET(
+ req: Request,
+ { params }: { params: { id: string } }
+) {
+ const auth_id = params.id;
+ const data = await prisma.user.findUnique({
+ where: {
+ auth_id: auth_id,
+ },
+ select: {
+ posts: {
+ select: {
+ id: true,
+ title: true,
+ content: true,
+ createdAt: true,
+ },
+ },
+ },
+ });
+
+ return NextResponse.json(data);
+}
diff --git a/app/api/post/coursepost/route.ts b/app/api/post/coursepost/route.ts
index f89f6a1..bd5f7e6 100644
--- a/app/api/post/coursepost/route.ts
+++ b/app/api/post/coursepost/route.ts
@@ -3,19 +3,32 @@ import prisma from "utils/prisma/prismaClient";
export const POST = async (req: Request) => {
try {
- const { title, id, authorId } = await req.json();
+ const { title, planId, auth_id } = await req.json();
await prisma.$connect();
+
+ const author = await prisma.user.findUnique({
+ where: {
+ auth_id: auth_id,
+ },
+ select: {
+ id: true,
+ },
+ });
+
const post = await prisma.post.create({
data: {
title: title,
- planId: id,
- authorId: authorId,
+ planId: planId,
+ authorId: author?.id,
},
});
return NextResponse.json({ message: "Success", post }, { status: 201 });
} catch (error) {
- return NextResponse.json({ message: "Error", error }, { status: 500 });
+ return NextResponse.json(
+ { message: "エラーが発生しました", error },
+ { status: 500 }
+ );
} finally {
await prisma.$disconnect();
}
diff --git a/app/components/Session.tsx b/app/components/Session.tsx
index 87da1ff..8577bc2 100644
--- a/app/components/Session.tsx
+++ b/app/components/Session.tsx
@@ -1,14 +1,13 @@
-"use client";
import Link from "next/link";
import React from "react";
-import useUser from "../hooks/useUser";
-
-const HomeSession = () => {
- const { session } = useUser();
+import useSeverUser from "app/hooks/useSeverUser";
+const HomeSession = async () => {
+ const { session } = useSeverUser();
+ const isSession = await session();
return (
- {session ? (
+ {isSession ? (
) : (
-
-
+
+
+
)}
diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx
index 716f17b..6d90c8d 100644
--- a/app/components/layout/Header.tsx
+++ b/app/components/layout/Header.tsx
@@ -1,46 +1,46 @@
-"use client";
+import useSeverUser from "app/hooks/useSeverUser";
import Link from "next/link";
-import { Sling as Hamburger } from "hamburger-react";
-import { useState } from "react";
-import ProfileDrawer from "./ProfileDrawer";
-import useUser from "app/hooks/useUser";
+import HamburgerMenu from "./Modal/HamburgerMenu";
-const Header = () => {
- const [isOpen, setOpen] = useState(false);
- const { session } = useUser();
+const Header = async () => {
+ const { session } = useSeverUser();
+ const sessionId = await session();
return (
-
+
-
+
ClassPlanner
-
+
diff --git a/app/components/layout/Modal/HamburgerMenu.tsx b/app/components/layout/Modal/HamburgerMenu.tsx
new file mode 100644
index 0000000..90d20ea
--- /dev/null
+++ b/app/components/layout/Modal/HamburgerMenu.tsx
@@ -0,0 +1,21 @@
+"use client";
+import { Sling as Hamburger } from "hamburger-react";
+
+import ProfileDrawer from "../ProfileDrawer";
+import { useState } from "react";
+
+const HamburgerMenu = ({ sessionId }: { sessionId: string | undefined }) => {
+ const [isOpen, setOpen] = useState(false);
+ return (
+
+
+
setOpen(false)}
+ sessionId={sessionId}
+ />
+
+ );
+};
+
+export default HamburgerMenu;
diff --git a/app/components/layout/ProfileDrawer.tsx b/app/components/layout/ProfileDrawer.tsx
index f948e20..dc9d9f2 100644
--- a/app/components/layout/ProfileDrawer.tsx
+++ b/app/components/layout/ProfileDrawer.tsx
@@ -2,15 +2,15 @@ import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { IoClose } from "react-icons/io5";
import Link from "next/link";
-import useUser from "app/hooks/useUser";
import Image from "next/image";
+
interface ProfileDrawerProps {
isOpen: boolean;
onClose: () => void;
+ sessionId: string | undefined;
}
-const ProfileDrawer = ({ isOpen, onClose }: ProfileDrawerProps) => {
- const { session } = useUser();
+const ProfileDrawer = ({ isOpen, onClose, sessionId }: ProfileDrawerProps) => {
return (
@@ -81,7 +81,7 @@ const ProfileDrawer = ({ isOpen, onClose }: ProfileDrawerProps) => {
全ての投稿を見る
- {session ? (
+ {sessionId ? (
<>
{
- {session ? (
+ {sessionId ? (
<>
ログイン中です
diff --git a/app/create/components/CreatePlanTitle.tsx b/app/create/components/CreatePlanTitle.tsx
new file mode 100644
index 0000000..3b126e1
--- /dev/null
+++ b/app/create/components/CreatePlanTitle.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
+import toast from "react-hot-toast";
+import { useState } from "react";
+import { postPlanSchema } from "./validate";
+import { PlanTittle } from "../components/index";
+
+const CreatePlanTitle = ({ sessionId }: { sessionId: string | undefined }) => {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(postPlanSchema),
+ });
+
+ const onSubmit = async (data: PlanTittle) => {
+ setIsLoading(true);
+ try {
+ const res = await fetch("/api/plan", {
+ method: "POST",
+ body: JSON.stringify({
+ title: data.title,
+ content: data.description,
+ auth_id: sessionId,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
+ },
+ });
+ if (!res.ok) {
+ throw new Error("エラーが発生しました");
+ }
+ const responseJson = await res.json();
+ const planId = responseJson.planId;
+ const params = new URLSearchParams();
+ params.append("planId", planId.toString());
+ const href = `/create/create-plan?${params}`;
+ router.push(href);
+ setIsLoading(false);
+ } catch (error) {
+ toast.error("エラーが発生しました。もう一度お試しください。");
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ {errors.title && (
+
+ {errors.title.message?.toString()}
+
+ )}
+
+
+
+
+ {errors.description && (
+
+ {errors.description.message?.toString()}
+
+ )}
+
+
+ {isLoading ? (
+
+ ) : (
+ "投稿する"
+ )}
+
+
+
+
+ );
+};
+
+export default CreatePlanTitle;
diff --git a/app/create/components/index.ts b/app/create/components/index.ts
new file mode 100644
index 0000000..9962a74
--- /dev/null
+++ b/app/create/components/index.ts
@@ -0,0 +1,4 @@
+export type PlanTittle = {
+ title: string;
+ description: string;
+};
diff --git a/app/create/components/validate.ts b/app/create/components/validate.ts
new file mode 100644
index 0000000..8a12309
--- /dev/null
+++ b/app/create/components/validate.ts
@@ -0,0 +1,6 @@
+import { z } from "zod";
+
+export const postPlanSchema = z.object({
+ title: z.string().min(1, "タイトルを入力してください"),
+ description: z.string().min(1, "内容を入力してください"),
+});
diff --git a/app/create/create-plan/components/Course.tsx b/app/create/create-plan/components/Course.tsx
index 8c694b4..28e5c1c 100644
--- a/app/create/create-plan/components/Course.tsx
+++ b/app/create/create-plan/components/Course.tsx
@@ -1,14 +1,10 @@
"use client";
import { useSearchParams } from "next/navigation";
import CourseCreateForm from "./CourseCreateForm";
-import useUser from "app/hooks/useUser";
-import NotAllowPage from "app/components/NotAllowPage";
const Course = () => {
const params = useSearchParams();
const planId = Number(params.get("planId"));
- const { session } = useUser();
- if (!session) return ;
return (
diff --git a/app/create/editplan/[id]/components/EditPlanCore.tsx b/app/create/editplan/[id]/components/EditPlanCore.tsx
index e7e8e7f..6c7631f 100644
--- a/app/create/editplan/[id]/components/EditPlanCore.tsx
+++ b/app/create/editplan/[id]/components/EditPlanCore.tsx
@@ -1,24 +1,22 @@
-"use client";
import SpecificCourseCore from "./SpecificCourseCore";
-import { SpecificCourseType } from "../types/SpecificCourseType";
+import { GetDetailCourseDataResponse, SpecificCourseType } from "../types";
import Link from "next/link";
-import useUser from "app/hooks/useUser";
-import NotAllowPage from "app/components/NotAllowPage";
+import useSeverUser from "app/hooks/useSeverUser";
+import { redirect } from "next/navigation";
interface SpecificCourseDateProps {
- SpecificCourseDate: SpecificCourseType[];
+ SpecificCourseDate: GetDetailCourseDataResponse;
}
-const EditPlanCore = ({ SpecificCourseDate }: SpecificCourseDateProps) => {
- const { user } = useUser();
- const currentUserId = user?.id;
- const userIds = SpecificCourseDate.map((course) => course.userId);
- const isCurrentUserIdInArray = userIds.includes(currentUserId);
-
- if (!isCurrentUserIdInArray) {
- return
;
+const EditPlanCore = async ({
+ SpecificCourseDate,
+}: SpecificCourseDateProps) => {
+ const { plans, auth_id } = SpecificCourseDate;
+ const { session } = useSeverUser();
+ const sessionId = await session();
+ if (sessionId !== auth_id) {
+ redirect("/profile");
}
-
return (
@@ -28,7 +26,7 @@ const EditPlanCore = ({ SpecificCourseDate }: SpecificCourseDateProps) => {
- {SpecificCourseDate.map((course: SpecificCourseType) => (
+ {plans.map((course: SpecificCourseType) => (
{
const res = await fetch(`${config.apiPrefix}${host}/api/plan/detail/${id}`, {
cache: "no-store", //ssr
method: "GET",
headers: { "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "" },
});
const data = await res.json();
- return data.plans;
+ return data;
}
-const EditCoursePage = async ({ params }: { params: { id: number } }) => {
+const EditCoursePage = async ({ params }: { params: { id: string } }) => {
const host = headers().get("host");
+
const SpecificCourseDate = await getDetailCourseData(params.id, host!);
return ;
diff --git a/app/create/editplan/[id]/types/SpecificCourseType.ts b/app/create/editplan/[id]/types/index.ts
similarity index 51%
rename from app/create/editplan/[id]/types/SpecificCourseType.ts
rename to app/create/editplan/[id]/types/index.ts
index 4145c3a..c93ba0f 100644
--- a/app/create/editplan/[id]/types/SpecificCourseType.ts
+++ b/app/create/editplan/[id]/types/index.ts
@@ -4,3 +4,8 @@ export type SpecificCourseType = {
content: string;
userId: number;
};
+
+export type GetDetailCourseDataResponse = {
+ plans: SpecificCourseType[];
+ auth_id: string;
+};
diff --git a/app/create/page.tsx b/app/create/page.tsx
index 8fb77c5..ee36279 100644
--- a/app/create/page.tsx
+++ b/app/create/page.tsx
@@ -1,130 +1,14 @@
-"use client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { useRouter } from "next/navigation";
-import toast from "react-hot-toast";
-import useUser from "../hooks/useUser";
-import { useState } from "react";
-import NotAllowPage from "../components/NotAllowPage";
-
-interface PlanTittleTypes {
- title: string;
- description: string;
-}
-
-const schema = z.object({
- title: z.string().min(1, "タイトルを入力してください"),
- description: z.string().min(1, "内容を入力してください"),
-});
-
-const PlanCreate = () => {
- const router = useRouter();
- const user = useUser();
- const [isLoading, setIsLoading] = useState(false);
- const {
- register,
- handleSubmit,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(schema),
- });
-
- const onSubmit = async (data: PlanTittleTypes) => {
- setIsLoading(true);
- try {
- const res = await fetch("/api/plan", {
- method: "POST",
- body: JSON.stringify({
- title: data.title,
- content: data.description,
- userId: user?.user?.id,
- }),
- headers: {
- "Content-Type": "application/json",
- "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
- },
- });
- if (!res.ok) {
- throw new Error("エラーが発生しました");
- }
- const responseJson = await res.json();
- const planId = responseJson.planId;
- const params = new URLSearchParams();
- params.append("planId", planId.toString());
- const href = `/create/create-plan?${params}`;
- router.push(href);
- setIsLoading(false);
- } catch (error) {
- toast.error("エラーが発生しました。もう一度お試しください。");
- }
- };
- if (!user.session) {
- return ;
- }
+import useSeverUser from "app/hooks/useSeverUser";
+import CreatePlanTitle from "./components/CreatePlanTitle";
+const PlanCreate = async () => {
+ const { session } = useSeverUser();
+ const sessionId = await session();
return (
始めに履修プランのタイトルと内容を書いてください。
-
-
-
- {errors.title && (
-
- {errors.title.message?.toString()}
-
- )}
-
-
-
-
- {errors.description && (
-
- {errors.description.message?.toString()}
-
- )}
-
-
- {isLoading ? (
-
- ) : (
- "投稿する"
- )}
-
-
-
-
+
);
diff --git a/app/hooks/useSeverUser.ts b/app/hooks/useSeverUser.ts
new file mode 100644
index 0000000..d53591f
--- /dev/null
+++ b/app/hooks/useSeverUser.ts
@@ -0,0 +1,11 @@
+import { createClient } from "utils/supabase/sever";
+
+export default function useSeverUser() {
+ const session = async () => {
+ const supabase = createClient();
+ const { data } = await supabase.auth.getUser();
+ const sessionId = data.user?.id;
+ return sessionId;
+ };
+ return { session };
+}
diff --git a/app/login/actions.ts b/app/login/actions.ts
new file mode 100644
index 0000000..26281e4
--- /dev/null
+++ b/app/login/actions.ts
@@ -0,0 +1,106 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+import { createClient } from "utils/supabase/sever";
+import { loginSchema, signupSchema } from "./validate";
+
+type AuthState = {
+ errors?: {
+ email?: string[];
+ password?: string[];
+ "confirm-password"?: string[];
+ };
+ message?: string | null;
+ values?: {
+ email?: string;
+ };
+};
+
+// ログイン用サーバーアクション
+export async function login(
+ prevState: AuthState,
+ formData: FormData
+): Promise {
+ const supabase = createClient();
+
+ // フォームデータを取得
+ const data = {
+ email: formData.get("email") as string,
+ password: formData.get("password") as string,
+ };
+
+ // バリデーション
+ const validateResult = loginSchema.safeParse(data);
+ if (!validateResult.success) {
+ return {
+ errors: validateResult.error.flatten().fieldErrors,
+ values: { email: data.email },
+ };
+ }
+
+ // Supabaseでログイン処理
+ const { error } = await supabase.auth.signInWithPassword(data);
+
+ if (error) {
+ return {
+ message:
+ "ログインに失敗しました。メールアドレスとパスワードを確認してください。",
+ values: { email: data.email },
+ };
+ }
+
+ revalidatePath("/", "layout");
+ redirect("/");
+}
+
+// 新規登録用サーバーアクション
+export async function signup(
+ prevState: AuthState,
+ formData: FormData
+): Promise {
+ const supabase = createClient();
+
+ // フォームデータを取得
+ const data = {
+ email: formData.get("email") as string,
+ password: formData.get("password") as string,
+ "confirm-password": formData.get("confirm-password") as string,
+ };
+
+ // バリデーション
+ const validateResult = signupSchema.safeParse(data);
+ if (!validateResult.success) {
+ return {
+ errors: validateResult.error.flatten().fieldErrors,
+ values: { email: data.email },
+ };
+ }
+
+ // Supabaseで新規登録処理
+ const { error } = await supabase.auth.signUp({
+ email: data.email,
+ password: data.password,
+ });
+
+ if (error) {
+ return {
+ message:
+ "新規登録に失敗しました。別のメールアドレスを試すか、後でもう一度お試しください。",
+ values: { email: data.email },
+ };
+ }
+
+ return {
+ message: "登録確認メールを送信しました。メールをご確認ください。",
+ values: { email: data.email },
+ };
+}
+
+// ログアウト用サーバーアクション
+export async function singOut() {
+ await createClient().auth.signOut();
+
+ revalidatePath("/", "layout");
+ redirect("/");
+}
diff --git a/app/login/components/AuthForm.tsx b/app/login/components/AuthForm.tsx
new file mode 100644
index 0000000..7540b88
--- /dev/null
+++ b/app/login/components/AuthForm.tsx
@@ -0,0 +1,121 @@
+"use client";
+import { login, signup } from "../actions";
+import { useFormState } from "react-dom";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { CardContent, CardFooter } from "@/components/ui/card";
+import { useState } from "react";
+
+const initialState = {
+ errors: {},
+ message: null,
+ values: {},
+};
+
+const AuthForm = () => {
+ const [isLogin, setIsLogin] = useState(true);
+ const [state, dispatch] = useFormState(
+ isLogin ? login : signup,
+ initialState
+ );
+
+ return (
+
+ );
+};
+
+export default AuthForm;
diff --git a/app/login/page.tsx b/app/login/page.tsx
new file mode 100644
index 0000000..67fe48b
--- /dev/null
+++ b/app/login/page.tsx
@@ -0,0 +1,21 @@
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+} from "@/components/ui/card";
+import AuthForm from "./components/AuthForm";
+
+export default function LoginPage() {
+ return (
+
+
+
+ アカウント関連
+ ログインまたは新規登録してください
+
+
+
+
+ );
+}
diff --git a/app/login/validate.ts b/app/login/validate.ts
new file mode 100644
index 0000000..d0defbf
--- /dev/null
+++ b/app/login/validate.ts
@@ -0,0 +1,20 @@
+import { z } from "zod";
+
+// zodスキーマの定義
+export const loginSchema = z.object({
+ email: z
+ .string()
+ .email({ message: "有効なメールアドレスを入力してください。" }),
+ password: z
+ .string()
+ .min(6, { message: "パスワードは6文字以上で入力してください。" }),
+});
+
+export const signupSchema = loginSchema
+ .extend({
+ "confirm-password": z.string(),
+ })
+ .refine((data) => data.password === data["confirm-password"], {
+ message: "パスワードが一致しません",
+ path: ["confirm-password"],
+ });
diff --git a/app/post/components/UserPostList.tsx b/app/post/components/UserPostList.tsx
new file mode 100644
index 0000000..da3a644
--- /dev/null
+++ b/app/post/components/UserPostList.tsx
@@ -0,0 +1,112 @@
+"use client";
+import toast from "react-hot-toast";
+import { Posts } from "../types/PostType";
+import { useReducer, useState } from "react";
+
+interface UserPostListProps {
+ data: Posts[];
+}
+
+type State = {
+ [key: string]: boolean;
+};
+type Action = {
+ type: "LOADING_START" | "LOADING_END";
+ payload: string;
+};
+const reducer = (state: State, action: Action) => {
+ switch (action.type) {
+ case "LOADING_START":
+ return { ...state, [action.payload]: true };
+ case "LOADING_END":
+ return { ...state, [action.payload]: false };
+ default:
+ return state;
+ }
+};
+
+const UserPostList = ({ data }: UserPostListProps) => {
+ const [deletedPostIds, setDeletedPostIds] = useState([]);
+ const [loadingStates, dispatch] = useReducer(reducer, {});
+
+ async function deletePost(postId: number) {
+ dispatch({ type: "LOADING_START", payload: postId.toString() });
+ try {
+ const response = await fetch(`/api/post`, {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
+ },
+ body: JSON.stringify({ postId }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Something went wrong");
+ }
+
+ const data = await response.json();
+ if (!data.post) {
+ toast.error("エラーが発生しました");
+ }
+ toast.success("投稿を削除しました");
+ dispatch({ type: "LOADING_END", payload: postId.toString() });
+ setDeletedPostIds((prevDeletedPostIds) => [
+ ...prevDeletedPostIds,
+ postId,
+ ]);
+ } catch (error) {
+ toast.error("投稿の削除に失敗しました");
+ }
+ }
+
+ return (
+
+ {data.length !== 0 ? (
+ data.map((post: Posts) =>
+ !deletedPostIds.includes(post.id) ? (
+
+
+
投稿内容:{post.title}
+
+ 投稿日付:
+ {new Date(post.createdAt).toLocaleDateString()}日・ 投稿時間:{" "}
+ {new Date(post.createdAt).toLocaleTimeString()}
+
+
+
+
deletePost(post.id)}
+ disabled={loadingStates[post.id]}
+ >
+ {loadingStates[post.id] ? (
+
+ ) : (
+ "削除する"
+ )}
+
+
+
+ ) : null
+ )
+ ) : (
+
+ )}
+
+ );
+};
+
+export default UserPostList;
diff --git a/app/post/page.tsx b/app/post/page.tsx
index 57f6c99..979acb4 100644
--- a/app/post/page.tsx
+++ b/app/post/page.tsx
@@ -1,64 +1,27 @@
-"use client";
-import useUser from "../hooks/useUser";
import Link from "next/link";
-import toast from "react-hot-toast";
-import { PostType } from "./types/PostType";
-import { useReducer, useState } from "react";
+import UserPostList from "./components/UserPostList";
+import { config } from "lib/config";
+import { headers } from "next/headers";
+import { Posts } from "./types/PostType";
+import useSeverUser from "app/hooks/useSeverUser";
-type State = {
- [key: string]: boolean;
-};
-type Action = {
- type: "LOADING_START" | "LOADING_END";
- payload: string;
-};
-const reducer = (state: State, action: Action) => {
- switch (action.type) {
- case "LOADING_START":
- return { ...state, [action.payload]: true };
- case "LOADING_END":
- return { ...state, [action.payload]: false };
- default:
- return state;
- }
-};
-
-const AddPostPage = () => {
- const { user } = useUser();
- const [deletedPostIds, setDeletedPostIds] = useState([]);
- const [loadingStates, dispatch] = useReducer(reducer, {});
-
- async function deletePost(postId: number) {
- dispatch({ type: "LOADING_START", payload: postId.toString() });
- try {
- const response = await fetch(`/api/post`, {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
- },
- body: JSON.stringify({ postId }),
- });
-
- if (!response.ok) {
- throw new Error("Something went wrong");
- }
-
- const data = await response.json();
- if (!data.post) {
- toast.error("エラーが発生しました");
- }
- toast.success("投稿を削除しました");
- dispatch({ type: "LOADING_END", payload: postId.toString() });
- setDeletedPostIds((prevDeletedPostIds) => [
- ...prevDeletedPostIds,
- postId,
- ]);
- } catch (error) {
- toast.error("投稿の削除に失敗しました");
- }
- }
+async function getUserPost(host: string, id: string): Promise {
+ const res = await fetch(`${config.apiPrefix}${host}/api/post/${id}`, {
+ cache: "no-store",
+ method: "GET",
+ headers: {
+ "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
+ },
+ });
+ const data = await res.json();
+ return data.posts;
+}
+const AddPostPage = async () => {
+ const host = headers().get("host");
+ const { session } = useSeverUser();
+ const id = await session();
+ const data = await getUserPost(host!, id!);
return (
@@ -66,53 +29,7 @@ const AddPostPage = () => {
これまで投稿したレビュー
-
- {user?.posts.length !== 0 ? (
- user?.posts.map((post: PostType) =>
- !deletedPostIds.includes(post.id) ? (
-
-
-
- 投稿内容:{post.title}
-
-
- 投稿日付:
- {new Date(post.createdAt).toLocaleDateString()}日・
- 投稿時間: {new Date(post.createdAt).toLocaleTimeString()}
-
-
-
-
deletePost(post.id)}
- disabled={loadingStates[post.id]}
- >
- {loadingStates[post.id] ? (
-
- ) : (
- "削除する"
- )}
-
-
-
- ) : null
- )
- ) : (
-
- )}
-
+
{
+ const { name, email, university, faculty, department, grade } = data;
+ return (
+
+
+ プロフィール
+
+
+
+
+
+
+ 所属学校
+
+
{university}
+
+
+
+
+
+
+
+ );
+};
+
+export default ProfileCard;
diff --git a/app/profile/components/ProfileContent.tsx b/app/profile/components/ProfileContent.tsx
index aa47ea7..34675f8 100644
--- a/app/profile/components/ProfileContent.tsx
+++ b/app/profile/components/ProfileContent.tsx
@@ -1,57 +1,32 @@
-"use client";
-import { Button } from "@/components/ui/button";
-import useUser from "app/hooks/useUser";
-import Link from "next/link";
-import { ClipLoader } from "react-spinners";
-import LogoutButton from "./LogoutButton";
-import NotAllowPage from "app/components/NotAllowPage";
+import useSeverUser from "app/hooks/useSeverUser";
+import { config } from "lib/config";
+import { headers } from "next/headers";
+import ProfileCard from "./ProfileCard";
+import ProfileOptionsCard from "./ProfileOptionsCard";
+import { Profile } from "../components/index";
-const ProfileContent = () => {
- const { user, isLoading, session, signOut } = useUser();
- const id = user?.id;
+export async function getUserDate(host: string, id: string): Promise {
+ const res = await fetch(`${config.apiPrefix}${host}/api/auth/${id}`, {
+ cache: "no-store",
+ method: "GET",
+ headers: { "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "" },
+ });
+ const data = await res.json();
+ return data.user;
+}
- if (!session) {
- return ;
- }
+const ProfileContent = async () => {
+ const { session } = useSeverUser();
+ const userId = await session();
+ const host = headers().get("host");
+ const data = await getUserDate(host!, userId!);
+ const id = data.auth_id;
return (
- <>
- {isLoading ? (
- <>
- 読み込み中・・・
-
- >
- ) : (
- <>
-
-
ユーザー名:{user?.name}
-
登録メールアドレス:{user?.email}
-
所属学校名:{user?.university}
-
学部名:{user?.faculty}
-
学科名:{user?.department}
-
学年:{user?.grade}年
-
- >
- )}
-
-
-
- プロフィールを編集する
-
-
-
-
-
- 自分の過去のレビューを見る
-
-
-
-
- 過去のプランを編集・削除
-
-
-
- >
+
);
};
diff --git a/app/profile/components/ProfileOptionsCard.tsx b/app/profile/components/ProfileOptionsCard.tsx
new file mode 100644
index 0000000..fe6ce72
--- /dev/null
+++ b/app/profile/components/ProfileOptionsCard.tsx
@@ -0,0 +1,62 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import { Edit3, LogOut, Star, Trash2 } from "lucide-react";
+import { singOut } from "app/login/actions";
+
+const ProfileOptionsCard = ({ id }: { id: string }) => {
+ return (
+
+
+ 機能
+
+
+
+
+
+ プロフィールを編集する
+
+
+
+
+
+ 自分の過去のレビューを見る
+
+
+
+
+
+ 過去のプランを編集・削除
+
+
+
+
+
+ );
+};
+
+export default ProfileOptionsCard;
diff --git a/app/profile/components/index.ts b/app/profile/components/index.ts
new file mode 100644
index 0000000..4675a8e
--- /dev/null
+++ b/app/profile/components/index.ts
@@ -0,0 +1,10 @@
+export type Profile = {
+ id: number;
+ auth_id: string;
+ name: string;
+ email: string;
+ university: string;
+ faculty: string;
+ department: string;
+ grade: number;
+};
diff --git a/app/profile/edit/actions.ts b/app/profile/edit/actions.ts
new file mode 100644
index 0000000..f818cbe
--- /dev/null
+++ b/app/profile/edit/actions.ts
@@ -0,0 +1,59 @@
+"use server";
+
+import { config } from "lib/config";
+import { userInfoEditSchema } from "./validate";
+
+type UserInfoState = {
+ errors?: {
+ name?: string[];
+ university?: string[];
+ faculty?: string[];
+ department?: string[];
+ grade?: string[];
+ };
+ message?: string | null;
+};
+
+export async function UpdateUserInfo(
+ prevState: UserInfoState,
+ formData: FormData
+): Promise {
+ const data = {
+ name: formData.get("name") as string,
+ university: formData.get("university") as string,
+ faculty: formData.get("faculty") as string,
+ department: formData.get("department") as string,
+ grade: Number(formData.get("grade")),
+ auth_id: formData.get("auth_id") as string,
+ };
+ const host = formData.get("host") as string;
+
+ const validateResult = userInfoEditSchema.safeParse(data);
+ if (!validateResult.success) {
+ return {
+ errors: validateResult.error.flatten().fieldErrors,
+ };
+ }
+
+ try {
+ const res = await fetch(`${config.apiPrefix}${host}/api/user/update`, {
+ method: "PUT",
+ cache: "no-cache",
+ headers: {
+ "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
+ "Content-Type": "application/json",
+ },
+
+ body: JSON.stringify(data),
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to update user info");
+ }
+ return { message: "ユーザー情報が正常に更新されました。" };
+ } catch (error) {
+ return {
+ message: "ユーザー情報の更新に失敗しました。もう一度お試しください。",
+ };
+ }
+}
diff --git a/app/profile/edit/components/EditProfileContent.tsx b/app/profile/edit/components/EditProfileContent.tsx
new file mode 100644
index 0000000..343f46f
--- /dev/null
+++ b/app/profile/edit/components/EditProfileContent.tsx
@@ -0,0 +1,148 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import Link from "next/link";
+import { MdChevronLeft } from "react-icons/md";
+import { useFormState, useFormStatus } from "react-dom";
+import { UpdateUserInfo } from "../actions";
+
+interface EditProfileContentProps {
+ data: {
+ id: number;
+ name: string;
+ university: string;
+ faculty: string;
+ department: string;
+ grade: number;
+ auth_id: string;
+ };
+ host: string | null;
+}
+
+const initialState = {
+ errors: {},
+ message: null,
+};
+
+const EditProfileContent = ({ data, host }: EditProfileContentProps) => {
+ const { name, university, faculty, department, grade, auth_id } = data;
+
+ const [state, formAction] = useFormState(UpdateUserInfo, initialState);
+ const { pending } = useFormStatus();
+
+ return (
+
+ );
+};
+
+export default EditProfileContent;
diff --git a/app/profile/edit/page.tsx b/app/profile/edit/page.tsx
index 2ebe065..a5f960f 100644
--- a/app/profile/edit/page.tsx
+++ b/app/profile/edit/page.tsx
@@ -1,207 +1,15 @@
-"use client";
-import useUser from "app/hooks/useUser";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useRouter } from "next/navigation";
-import { useForm } from "react-hook-form";
-import toast from "react-hot-toast";
-import { z } from "zod";
-import { FormData } from "./types/EditType";
-import { useEffect, useState } from "react";
-import Link from "next/link";
-import { MdChevronLeft } from "react-icons/md";
-import NotAllowPage from "app/components/NotAllowPage";
-
-const EditProfile = async (
- name: string,
- auth_id: string,
- university: string,
- faculty: string,
- department: string,
- grade: number
-) => {
- const res = await fetch("/api/user/update", {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
- },
- body: JSON.stringify({
- name,
- auth_id,
- university,
- faculty,
- department,
- grade,
- }),
- });
- const data = await res.json();
- return data;
-};
-
-const formSchema = z.object({
- name: z.string().min(1, "ユーザー名は1文字以上で入力してください。"),
- university: z.string().min(1, "大学名は1文字以上で入力してください。"),
- faculty: z.string().min(1, "学部名は1文字以上で入力してください。"),
- department: z.string().min(1, "学科名は1文字以上で入力してください。"),
- grade: z.string().min(1, "学年は1文字以上で入力してください。"),
-});
-
-const EditPage = () => {
- const { user, session } = useUser();
- const router = useRouter();
- const [isLoading, setIsLoading] = useState(false);
-
- const onSubmit = async (data: FormData) => {
- setIsLoading(true);
- if (session?.user?.id) {
- const res = await EditProfile(
- data.name,
- session.user.id,
- data.university,
- data.faculty,
- data.department,
- typeof data.grade === "string" ? parseInt(data.grade) : data.grade // gradeをnumberに変換
- );
- if (res.message === "Updated successfully") {
- toast.success("プロフィールを更新しました");
- router.push("/profile");
- setIsLoading(false);
- } else {
- toast.error("プロフィールの更新に失敗しました");
- }
- }
- };
-
- const {
- register,
- handleSubmit,
- setValue,
- formState: { errors },
- } = useForm({
- resolver: zodResolver(formSchema),
- defaultValues: {
- name: user?.name || "",
- university: user?.university || "",
- faculty: user?.faculty || "",
- department: user?.department || "",
- grade: user?.grade || "",
- },
- });
- // useEffectで初期値を設定
- useEffect(() => {
- setValue("name", user?.name || "");
- setValue("university", user?.university || "");
- setValue("faculty", user?.faculty || "");
- setValue("department", user?.department || "");
- setValue("grade", user?.grade?.toString() || ""); // gradeはstringに変換
- }, [user, setValue]);
-
- if (!session) return ;
- return (
-
- );
+import useSeverUser from "app/hooks/useSeverUser";
+import EditProfileContent from "./components/EditProfileContent";
+import { headers } from "next/headers";
+import { getUserDate } from "../components/ProfileContent";
+
+const EditProfilePage = async () => {
+ const { session } = useSeverUser();
+ const userId = await session();
+ const host = headers().get("host");
+ const data = await getUserDate(host!, userId!);
+
+ return ;
};
-export default EditPage;
+export default EditProfilePage;
diff --git a/app/profile/edit/validate.ts b/app/profile/edit/validate.ts
new file mode 100644
index 0000000..04c508a
--- /dev/null
+++ b/app/profile/edit/validate.ts
@@ -0,0 +1,21 @@
+import { z } from "zod";
+
+export const userInfoEditSchema = z.object({
+ name: z
+ .string()
+ .min(1, { message: "ユーザー名は一文字以上で入力してください" }),
+ university: z
+ .string()
+ .min(1, { message: "大学名は一文字以上で入力してください" }),
+ faculty: z
+ .string()
+ .min(1, { message: "学部名は一文字以上で入力してください" }),
+ department: z
+ .string()
+ .min(1, { message: "学科名は一文字以上で入力してください" }),
+ grade: z
+ .number()
+ .int()
+ .min(1, { message: "学年は1以上で入力してください" })
+ .max(6, { message: "学年は6以下で入力してください" }),
+});
diff --git a/app/profile/page.tsx b/app/profile/page.tsx
index 8fb6314..4497e12 100644
--- a/app/profile/page.tsx
+++ b/app/profile/page.tsx
@@ -2,7 +2,7 @@ import ProfileContent from "./components/ProfileContent";
const ProfilePage = () => {
return (
-
+
プロフィール画面
diff --git a/app/updatePlan/[id]/components/AddCourse.tsx b/app/updatePlan/[id]/components/AddCourse.tsx
index 6c3419d..667a54f 100644
--- a/app/updatePlan/[id]/components/AddCourse.tsx
+++ b/app/updatePlan/[id]/components/AddCourse.tsx
@@ -1,3 +1,4 @@
+"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useRouter } from "next/navigation";
diff --git a/app/updatePlan/[id]/EditCorseList.tsx b/app/updatePlan/[id]/components/EditCorseList.tsx
similarity index 99%
rename from app/updatePlan/[id]/EditCorseList.tsx
rename to app/updatePlan/[id]/components/EditCorseList.tsx
index ec523b3..cfe4e4e 100644
--- a/app/updatePlan/[id]/EditCorseList.tsx
+++ b/app/updatePlan/[id]/components/EditCorseList.tsx
@@ -1,3 +1,4 @@
+"use client";
import Modal from "app/components/layout/Modal/Modal";
import { Input } from "@/components/ui/input";
import { useRef, useState } from "react";
diff --git a/app/updatePlan/[id]/components/PageBackButton.tsx b/app/updatePlan/[id]/components/PageBackButton.tsx
new file mode 100644
index 0000000..48e719a
--- /dev/null
+++ b/app/updatePlan/[id]/components/PageBackButton.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { IoArrowBackSharp } from "react-icons/io5";
+
+const PageBackButton = () => {
+ const router = useRouter();
+
+ return (
+
{
+ router.back();
+ router.refresh();
+ }}
+ />
+ );
+};
+
+export default PageBackButton;
diff --git a/app/updatePlan/[id]/components/UpdatePageCore.tsx b/app/updatePlan/[id]/components/UpdatePageCore.tsx
index 5e2ebc2..c3aeea5 100644
--- a/app/updatePlan/[id]/components/UpdatePageCore.tsx
+++ b/app/updatePlan/[id]/components/UpdatePageCore.tsx
@@ -1,140 +1,47 @@
-"use client";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { useRef, useState } from "react";
import { CourseType } from "app/allPost/[id]/types/Course";
-import { IoArrowBackSharp } from "react-icons/io5";
-import { useRouter } from "next/navigation";
-import toast from "react-hot-toast";
-import EditCorseList from "../EditCorseList";
+import EditCorseList from "./EditCorseList";
import AddCourse from "./AddCourse";
-import useUser from "app/hooks/useUser";
-import { UserType } from "app/hooks/types/UserType";
-import NotAllowPage from "app/components/NotAllowPage";
+import { UserType } from "../components/index";
+import UpdatePlanName from "./UpdatePlanName";
+import useSeverUser from "app/hooks/useSeverUser";
+import { redirect } from "next/navigation";
+import PageBackButton from "./PageBackButton";
export interface UpdatePageCoreProps {
- paramsId: number;
+ paramsId: string;
title: string;
content: string;
courses: CourseType[];
userData: UserType;
}
-const UpdatePageCore = ({
+const UpdatePageCore = async ({
paramsId,
title,
content,
courses,
userData,
}: UpdatePageCoreProps) => {
- const [isUpdated, setIsUpdated] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
- const router = useRouter();
- const tittleRef = useRef(null);
- const contentRef = useRef(null);
- const { user } = useUser();
-
- async function tittleUpdate(id: number, title: string, content: string) {
- if (!title || !content) {
- toast.error("全ての項目を入力してください");
- return;
- }
- setIsLoading(true);
- try {
- const res = await fetch(`/api/plan`, {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
- },
- body: JSON.stringify({ id, title, content }),
- });
-
- if (!res.ok) {
- throw new Error("Something went wrong");
- }
-
- const data = await res.json();
- if (data.error) {
- throw new Error(data.error);
- }
- toast.success(`プラン名:${data.post.title}を更新しました`);
- setIsLoading(false);
- setIsUpdated(true);
- } catch (error) {
- toast.error("投稿の削除に失敗しました");
- setIsLoading(false);
- }
- }
-
- if (user?.id !== userData.id) {
- return ; //アクセス制限
+ const { session } = useSeverUser();
+ const sessionId = await session();
+ if (sessionId !== userData.auth_id) {
+ redirect("/profile");
}
return (
-
{
- router.back();
- router.refresh();
- }}
- />
+
プラン編集画面
-
-
-
- ・履修プラン名
-
-
-
- ・履修プラン名
-
-
-
-
{
- tittleUpdate(
- Number(paramsId),
- tittleRef.current!.value,
- contentRef.current!.value
- );
- }}
- disabled={isUpdated}
- >
- {isLoading ? (
-
- ) : (
- "更新する"
- )}
-
-
+
+
-
- ・現在登録中の教科一覧
+
+ 現在登録中の教科一覧
-
+
{courses.map((course: CourseType) => (
-
+
+ 教科を追加する
+
diff --git a/app/updatePlan/[id]/components/UpdatePlanName.tsx b/app/updatePlan/[id]/components/UpdatePlanName.tsx
new file mode 100644
index 0000000..ed9afc1
--- /dev/null
+++ b/app/updatePlan/[id]/components/UpdatePlanName.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { useRef, useState } from "react";
+import toast from "react-hot-toast";
+
+interface UpdatePlanNameProps {
+ title: string;
+ content: string;
+ paramsId: string;
+}
+
+const UpdatePlanName = ({ title, content, paramsId }: UpdatePlanNameProps) => {
+ const [isUpdated, setIsUpdated] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const tittleRef = useRef
(null);
+ const contentRef = useRef(null);
+
+ async function tittleUpdate(id: number, title: string, content: string) {
+ if (!title || !content) {
+ toast.error("全ての項目を入力してください");
+ return;
+ }
+ setIsLoading(true);
+ try {
+ const res = await fetch(`/api/plan`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ "x-api-key": process.env.NEXT_PUBLIC_API_KEY || "",
+ },
+ body: JSON.stringify({ id, title, content }),
+ });
+
+ if (!res.ok) {
+ throw new Error("Something went wrong");
+ }
+
+ const data = await res.json();
+ if (data.error) {
+ throw new Error(data.error);
+ }
+ toast.success(`プラン名:${data.post.title}を更新しました`);
+ setIsLoading(false);
+ setIsUpdated(true);
+ } catch (error) {
+ toast.error("投稿の削除に失敗しました");
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+
+ 履修プランタイトル
+
+
+
+ 履修プラン説明
+
+
+
+
{
+ tittleUpdate(
+ Number(paramsId),
+ tittleRef.current!.value,
+ contentRef.current!.value
+ );
+ }}
+ disabled={isUpdated}
+ >
+ {isLoading ? (
+
+ ) : (
+ "更新する"
+ )}
+
+
+ );
+};
+
+export default UpdatePlanName;
diff --git a/app/updatePlan/[id]/components/index.ts b/app/updatePlan/[id]/components/index.ts
new file mode 100644
index 0000000..ffe8d9f
--- /dev/null
+++ b/app/updatePlan/[id]/components/index.ts
@@ -0,0 +1,26 @@
+export type CourseType = {
+ id: number;
+ name: string;
+ content: string;
+ planId: number;
+};
+
+export type UserType = {
+ id: number;
+ auth_id: string;
+ email: string;
+ name: string;
+ university: string;
+ faculty: string;
+ department: string;
+ grade: number;
+};
+
+export type SpecificPlanType = {
+ id: number;
+ title: string;
+ content: string;
+ userId: number;
+ courses: CourseType[];
+ user: UserType;
+};
diff --git a/app/updatePlan/[id]/page.tsx b/app/updatePlan/[id]/page.tsx
index c1473f2..dfdc8fa 100644
--- a/app/updatePlan/[id]/page.tsx
+++ b/app/updatePlan/[id]/page.tsx
@@ -1,8 +1,12 @@
import { headers } from "next/headers";
import UpdatePageCore from "./components/UpdatePageCore";
import { config } from "lib/config";
+import { SpecificPlanType } from "./components";
-async function getDetailData(id: number, host: string) {
+async function getDetailData(
+ id: string,
+ host: string
+): Promise {
const res = await fetch(`${config.apiPrefix}${host}/api/plan/update/${id}`, {
cache: "no-store", //ssr
method: "GET",
@@ -14,7 +18,7 @@ async function getDetailData(id: number, host: string) {
return data;
}
-const UpdatePage = async ({ params }: { params: { id: number } }) => {
+const UpdatePage = async ({ params }: { params: { id: string } }) => {
const host = headers().get("host");
const uniqueDate = await getDetailData(params.id, host!);
const { title, content, courses, user } = uniqueDate;
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..c947d53
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "lib/utils";
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
index 4ebb337..c1ef7bb 100644
--- a/components/ui/button.tsx
+++ b/components/ui/button.tsx
@@ -1,6 +1,8 @@
+"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
+import { useFormStatus } from "react-dom";
import { cn } from "lib/utils";
@@ -42,11 +44,15 @@ export interface ButtonProps
const Button = React.forwardRef(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
+ const { pending } = useFormStatus();
+
return (
+ //pending 状態の時はローディングアニメーションを表示する
);
}
diff --git a/middleware.ts b/middleware.ts
index d44a7e0..366bf2c 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,20 +1,39 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
+import { updateSession } from "utils/supabase/middleware";
-export function middleware(req: NextRequest) {
- // APIへのアクセスに対してのみミドルウェアを適用
- if (req.nextUrl.pathname.startsWith("/api")) {
- // x-api-keyの検証
+export async function middleware(req: NextRequest) {
+ const { pathname } = req.nextUrl;
+
+ // updateSession を除外するパスのリスト
+ const excludeSessionUpdatePaths = ["/"];
+
+ // `/api` パスに対してのみ API 保護の処理を適用
+ if (pathname.startsWith("/api")) {
const apiKey = req.headers.get("x-api-key");
if (apiKey !== process.env.NEXT_PUBLIC_API_KEY) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
+ return NextResponse.next();
+ }
+
+ // excludeSessionUpdatePaths に含まれないパス、かつ /allPost で始まらないパスに対してのみ updateSession を適用
+ if (
+ !excludeSessionUpdatePaths.includes(pathname) &&
+ !pathname.startsWith("/allPost")
+ ) {
+ const response = await updateSession(req);
+ if (response) {
+ return response;
+ }
}
return NextResponse.next();
}
-// ミドルウェアの適用範囲を指定
export const config = {
- matcher: "/api/:path*", // すべてのAPIエンドポイントに適用
+ matcher: [
+ "/api/:path*",
+ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
+ ],
};
diff --git a/package-lock.json b/package-lock.json
index f0ca222..a34bef5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,12 +13,14 @@
"@hookform/resolvers": "^3.3.4",
"@prisma/client": "^5.10.2",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@supabase/auth-helpers-nextjs": "^0.9.0",
+ "@supabase/ssr": "^0.5.1",
"@supabase/supabase-js": "^2.39.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
@@ -1814,6 +1816,133 @@
}
}
},
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz",
+ "integrity": "sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collapsible": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz",
@@ -2477,18 +2606,20 @@
"@supabase/supabase-js": "^2.19.0"
}
},
- "node_modules/@supabase/functions-js": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.1.5.tgz",
- "integrity": "sha512-BNzC5XhCzzCaggJ8s53DP+WeHHGT/NfTsx2wUSSGKR2/ikLFQTBCDzMvGz/PxYMqRko/LwncQtKXGOYp1PkPaw==",
+ "node_modules/@supabase/auth-js": {
+ "version": "2.65.0",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.65.0.tgz",
+ "integrity": "sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==",
+ "license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
- "node_modules/@supabase/gotrue-js": {
- "version": "2.62.2",
- "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.62.2.tgz",
- "integrity": "sha512-AP6e6W9rQXFTEJ7sTTNYQrNf0LCcnt1hUW+RIgUK+Uh3jbWvcIST7wAlYyNZiMlS9+PYyymWQ+Ykz/rOYSO0+A==",
+ "node_modules/@supabase/functions-js": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.1.tgz",
+ "integrity": "sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==",
+ "license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
@@ -2497,6 +2628,7 @@
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
+ "license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -2505,17 +2637,19 @@
}
},
"node_modules/@supabase/postgrest-js": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.9.2.tgz",
- "integrity": "sha512-I6yHo8CC9cxhOo6DouDMy9uOfW7hjdsnCxZiaJuIVZm1dBGTFiQPgfMa9zXCamEWzNyWRjZvupAUuX+tqcl5Sw==",
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.16.1.tgz",
+ "integrity": "sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==",
+ "license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/realtime-js": {
- "version": "2.9.3",
- "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.3.tgz",
- "integrity": "sha512-lAp50s2n3FhGJFq+wTSXLNIDPw5Y0Wxrgt44eM5nLSA3jZNUUP3Oq2Ccd1CbZdVntPCWLZvJaU//pAd2NE+QnQ==",
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.10.2.tgz",
+ "integrity": "sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==",
+ "license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4",
@@ -2523,25 +2657,39 @@
"ws": "^8.14.2"
}
},
+ "node_modules/@supabase/ssr": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.5.1.tgz",
+ "integrity": "sha512-+G94H/GZG0nErZ3FQV9yJmsC5Rj7dmcfCAwOt37hxeR1La+QTl8cE9whzYwPUrTJjMLGNXoO+1BMvVxwBAbz4g==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^0.6.0"
+ },
+ "peerDependencies": {
+ "@supabase/supabase-js": "^2.43.4"
+ }
+ },
"node_modules/@supabase/storage-js": {
- "version": "2.5.5",
- "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.5.5.tgz",
- "integrity": "sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.0.tgz",
+ "integrity": "sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==",
+ "license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
}
},
"node_modules/@supabase/supabase-js": {
- "version": "2.39.7",
- "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.39.7.tgz",
- "integrity": "sha512-1vxsX10Uhc2b+Dv9pRjBjHfqmw2N2h1PyTg9LEfICR3x2xwE24By1MGCjDZuzDKH5OeHCsf4it6K8KRluAAEXA==",
+ "version": "2.45.4",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.45.4.tgz",
+ "integrity": "sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==",
+ "license": "MIT",
"dependencies": {
- "@supabase/functions-js": "2.1.5",
- "@supabase/gotrue-js": "2.62.2",
+ "@supabase/auth-js": "2.65.0",
+ "@supabase/functions-js": "2.4.1",
"@supabase/node-fetch": "2.6.15",
- "@supabase/postgrest-js": "1.9.2",
- "@supabase/realtime-js": "2.9.3",
- "@supabase/storage-js": "2.5.5"
+ "@supabase/postgrest-js": "1.16.1",
+ "@supabase/realtime-js": "2.10.2",
+ "@supabase/storage-js": "2.7.0"
}
},
"node_modules/@swc/counter": {
@@ -2894,9 +3042,10 @@
}
},
"node_modules/@types/phoenix": {
- "version": "1.6.4",
- "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.4.tgz",
- "integrity": "sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA=="
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.5.tgz",
+ "integrity": "sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==",
+ "license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.11",
@@ -2952,9 +3101,10 @@
"license": "MIT"
},
"node_modules/@types/ws": {
- "version": "8.5.10",
- "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
- "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz",
+ "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==",
+ "license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -4167,6 +4317,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@@ -10105,7 +10264,8 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "1.2.1",
@@ -10496,7 +10656,8 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
@@ -10525,6 +10686,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
diff --git a/package.json b/package.json
index 67636cb..10bc26b 100644
--- a/package.json
+++ b/package.json
@@ -16,12 +16,14 @@
"@hookform/resolvers": "^3.3.4",
"@prisma/client": "^5.10.2",
"@radix-ui/react-accordion": "^1.1.2",
+ "@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@supabase/auth-helpers-nextjs": "^0.9.0",
+ "@supabase/ssr": "^0.5.1",
"@supabase/supabase-js": "^2.39.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
diff --git a/utils/supabase/client.ts b/utils/supabase/client.ts
new file mode 100644
index 0000000..9f2891b
--- /dev/null
+++ b/utils/supabase/client.ts
@@ -0,0 +1,8 @@
+import { createBrowserClient } from "@supabase/ssr";
+
+export function createClient() {
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ );
+}
diff --git a/utils/supabase/middleware.ts b/utils/supabase/middleware.ts
new file mode 100644
index 0000000..8586cc2
--- /dev/null
+++ b/utils/supabase/middleware.ts
@@ -0,0 +1,48 @@
+import { createServerClient } from "@supabase/ssr";
+import { NextResponse, type NextRequest } from "next/server";
+
+export async function updateSession(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({
+ request,
+ });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) =>
+ request.cookies.set(name, value)
+ );
+ supabaseResponse = NextResponse.next({
+ request,
+ });
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ );
+ },
+ },
+ }
+ );
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (
+ !user &&
+ !request.nextUrl.pathname.startsWith("/login") &&
+ !request.nextUrl.pathname.startsWith("/auth")
+ ) {
+ // no user, potentially respond by redirecting the user to the login page
+ const url = request.nextUrl.clone();
+ url.pathname = "/login";
+ return NextResponse.redirect(url);
+ }
+
+ return supabaseResponse;
+}
diff --git a/utils/supabase/sever.ts b/utils/supabase/sever.ts
new file mode 100644
index 0000000..f466807
--- /dev/null
+++ b/utils/supabase/sever.ts
@@ -0,0 +1,26 @@
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import { createServerClient, type CookieOptions } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+export function createClient() {
+ const cookieStore = cookies();
+
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ cookieStore.set(name, value, options)
+ );
+ } catch {}
+ },
+ },
+ }
+ );
+}