diff --git a/web/app/home/page.tsx b/web/app/home/page.tsx index f489da5f..b4824bdf 100644 --- a/web/app/home/page.tsx +++ b/web/app/home/page.tsx @@ -6,7 +6,7 @@ import { motion, useAnimation } from "framer-motion"; import { useCallback, useEffect, useState } from "react"; import { MdThumbUp } from "react-icons/md"; import request from "~/api/request"; -import { useMyID, useRecommended } from "~/api/user"; +import { useAboutMe, useRecommended } from "~/api/user"; import { Card } from "~/components/Card"; import { DraggableCard } from "~/components/DraggableCard"; import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress"; @@ -17,8 +17,8 @@ export default function Home() { const controls = useAnimation(); const [clickedButton, setClickedButton] = useState(""); const { - state: { data: myId }, - } = useMyID(); + state: { data: currentUser }, + } = useAboutMe(); const [_, rerender] = useState({}); const [recommended, setRecommended] = useState< @@ -71,6 +71,9 @@ export default function Home() { }); }, [controls, accept]); + if (currentUser == null) { + return ; + } if (recommended == null) { return ; } @@ -89,7 +92,7 @@ export default function Home() { {nextUser && (
- +
- setBack(back)} /> + setBack(back)} + /> )} diff --git a/web/components/Card.tsx b/web/components/Card.tsx index 2ba27e57..e81fc16c 100644 --- a/web/components/Card.tsx +++ b/web/components/Card.tsx @@ -1,17 +1,200 @@ import ThreeSixtyIcon from "@mui/icons-material/ThreeSixty"; -import { Chip } from "@mui/material"; -import type { UserID, UserWithCoursesAndSubjects } from "common/types"; -import { useState } from "react"; +import type { UserWithCoursesAndSubjects } from "common/types"; +import React, { useState, useRef, useEffect, useCallback } from "react"; import NonEditableCoursesTable from "./course/NonEditableCoursesTable"; import UserAvatar from "./human/avatar"; interface CardProps { displayedUser: UserWithCoursesAndSubjects; - comparisonUserId?: UserID; + currentUser: UserWithCoursesAndSubjects; onFlip?: (isBack: boolean) => void; } -export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) { +const CardFront = ({ displayedUser, currentUser }: CardProps) => { + const containerRef = useRef(null); + const interestsContainerRef = useRef(null); + const coursesContainerRef = useRef(null); + const [isHiddenInterestExist, setHiddenInterestExist] = useState(false); + const [isHiddenCourseExist, setHiddenCourseExist] = useState(false); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const resizeObserver = new ResizeObserver(() => { + calculateVisibleInterests(); + calculateVisibleCourses(); + }); + + resizeObserver.observe(container); + + calculateVisibleInterests(); // 初期計算 + calculateVisibleCourses(); // 初期計算 + + return () => resizeObserver.disconnect(); + }, []); + + const calculateVisibleCourses = useCallback(() => { + const courses = displayedUser.courses; + const container = coursesContainerRef.current; + if (!container) return; + + const containerHeight = container.offsetHeight; // コンテナの高さを取得 + + // 一旦コンテナを初期化 + container.innerHTML = ""; + setHiddenCourseExist(false); + + // courses を一致・非一致で分類 + const matchingCourses = courses.filter((course) => + currentUser.courses.some((c) => c.id === course.id), + ); + const nonMatchingCourses = courses.filter( + (course) => !currentUser.courses.some((c) => c.id === course.id), + ); + + // courses を表示する flex コンテナ + const coursesContainer = document.createElement("div"); + coursesContainer.classList.add("flex", "flex-wrap", "gap-2"); + container.appendChild(coursesContainer); + + // 一致しているコースを先に表示 + for (const course of [...matchingCourses, ...nonMatchingCourses]) { + const isMatching = currentUser.courses.some((c) => c.id === course.id); + + // 新しい div 要素を作成 + const element = document.createElement("div"); + element.textContent = course.name; + + // スタイル適用(赤 or 灰色) + element.classList.add("badge", "badge-outline"); + element.style.backgroundColor = isMatching ? "red" : "gray"; + element.style.color = "white"; + + // 表示判定 + if (coursesContainer.offsetHeight + 30 <= containerHeight) { + coursesContainer.appendChild(element); + } else { + setHiddenCourseExist; + } + } + }, [displayedUser, currentUser]); + + const calculateVisibleInterests = useCallback(() => { + const interests = displayedUser.interestSubjects; + const container = interestsContainerRef.current; + if (!container) return; + + const containerHeight = container.offsetHeight; // コンテナの高さを取得 + + // 一旦コンテナを初期化 + container.innerHTML = ""; + setHiddenInterestExist(false); + + // interests を一致・非一致で分類 + const matchingInterests = interests.filter((interest) => + currentUser.interestSubjects.some((i) => i.name === interest.name), + ); + const nonMatchingInterests = interests.filter( + (interest) => + !currentUser.interestSubjects.some((i) => i.name === interest.name), + ); + + // interests を表示する flex コンテナ + const flexContainer = document.createElement("div"); + flexContainer.classList.add("flex", "flex-wrap", "gap-2"); + container.appendChild(flexContainer); + + // 一致している興味分野を先に表示 + for (const interest of [...matchingInterests, ...nonMatchingInterests]) { + const isMatching = currentUser.interestSubjects.some( + (i) => i.name === interest.name, + ); + + // 新しい div 要素を作成 + const element = document.createElement("div"); + element.textContent = interest.name; + + // スタイル適用(赤 or 灰色) + element.classList.add("badge", "badge-outline"); + element.style.backgroundColor = isMatching ? "red" : "gray"; + element.style.color = "white"; + element.style.overflow = "hidden"; + element.style.whiteSpace = "nowrap"; + element.style.textOverflow = "ellipsis"; + + // 表示判定 + if (flexContainer.offsetHeight + 30 <= containerHeight) { + flexContainer.appendChild(element); + } else { + setHiddenInterestExist(true); + } + } + }, [displayedUser, currentUser]); + + return ( +
+
+ +
+

{displayedUser.name}

+

{displayedUser.grade}

+

{displayedUser.faculty}

+

{displayedUser.department}

+
+
+ +
+
+
+ {isHiddenInterestExist && ( +
+ And More +
+ )} +
+ +
+
+ {isHiddenCourseExist && ( +
+ And More +
+ )} +
+
+
+ ); +}; + +const CardBack = ({ displayedUser, currentUser }: CardProps) => { + return ( +
+
+

{displayedUser?.name}

+
+ +
+ +
+
+ ); +}; + +export function Card({ displayedUser, currentUser, onFlip }: CardProps) { const [isDisplayingBack, setIsDisplayingBack] = useState(false); const handleRotate = () => { @@ -39,7 +222,7 @@ export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) { transform: isDisplayingBack ? "rotateY(180deg)" : "rotateY(0deg)", }} > - +
- +
); } - -const CardFront = ({ displayedUser }: CardProps) => { - return ( -
-
- -
- {displayedUser.name} -
-
-
- -

{displayedUser.faculty}

-
-
- -

7 ? "text-xs" : "text-2xl"}`} - > - {displayedUser.department} -

-
-
- -

{displayedUser.gender}

-
-
- -

{displayedUser.grade}

-
-
- -

- {displayedUser.intro} -

-
-

TODO: これはサンプルです

-
    - {displayedUser.interestSubjects.map((subject) => ( -
  • {subject.name}
  • - ))} -
-
- -
-
- ); -}; - -const CardBack = ({ displayedUser, comparisonUserId }: CardProps) => { - return ( -
-
-

{displayedUser?.name}

-
- -
- -
-
- ); -}; diff --git a/web/components/DraggableCard.tsx b/web/components/DraggableCard.tsx index d6ae570d..4e7a8f1a 100644 --- a/web/components/DraggableCard.tsx +++ b/web/components/DraggableCard.tsx @@ -1,6 +1,6 @@ import CloseIcon from "@mui/icons-material/Close"; import { Box, Typography } from "@mui/material"; -import type { UserID, UserWithCoursesAndSubjects } from "common/types"; +import type { UserWithCoursesAndSubjects } from "common/types"; import { motion, useMotionValue, useMotionValueEvent } from "framer-motion"; import { useCallback, useState } from "react"; import { MdThumbUp } from "react-icons/md"; @@ -10,7 +10,7 @@ const SWIPE_THRESHOLD = 30; interface DraggableCardProps { displayedUser: UserWithCoursesAndSubjects; - comparisonUserId?: UserID; + currentUser: UserWithCoursesAndSubjects; onSwipeRight: () => void; onSwipeLeft: () => void; clickedButton: string; @@ -18,7 +18,7 @@ interface DraggableCardProps { export const DraggableCard = ({ displayedUser, - comparisonUserId, + currentUser, onSwipeRight, onSwipeLeft, clickedButton, @@ -136,10 +136,7 @@ export const DraggableCard = ({ whileTap={{ scale: 0.95 }} > - + diff --git a/web/components/common/modal/ModalProvider.tsx b/web/components/common/modal/ModalProvider.tsx index 37fe8836..99fe463c 100644 --- a/web/components/common/modal/ModalProvider.tsx +++ b/web/components/common/modal/ModalProvider.tsx @@ -1,6 +1,6 @@ import type { UserWithCoursesAndSubjects } from "common/types"; import { type ReactNode, createContext, useContext, useState } from "react"; -import { useMyID } from "~/api/user"; +import { useAboutMe } from "~/api/user"; import { Card } from "../../Card"; const ModalContext = createContext(undefined); @@ -19,8 +19,8 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { const [selectedUser, setSelectedUser] = useState(null); const { - state: { data: myId }, - } = useMyID(); + state: { data: currentUser }, + } = useAboutMe(); const openModal = (user: UserWithCoursesAndSubjects) => { setSelectedUser(user); @@ -35,7 +35,7 @@ export const ModalProvider = ({ children }: ModalProviderProps) => { return ( {children} - {open && selectedUser && ( + {open && selectedUser && currentUser && ( // biome-ignore lint/a11y/useKeyWithClickEvents:
{ className="rounded bg-white p-4 shadow-lg" onClick={(e) => e.stopPropagation()} > - +
)}