11import ThreeSixtyIcon from "@mui/icons-material/ThreeSixty" ;
2- import { Chip } from "@mui/material" ;
3- import type { UserID , UserWithCoursesAndSubjects } from "common/types" ;
4- import { useState } from "react" ;
2+ import type { UserWithCoursesAndSubjects } from "common/types" ;
3+ import React , { useState , useRef , useEffect , useCallback } from "react" ;
54import NonEditableCoursesTable from "./course/NonEditableCoursesTable" ;
65import UserAvatar from "./human/avatar" ;
76
87interface CardProps {
98 displayedUser : UserWithCoursesAndSubjects ;
10- comparisonUserId ?: UserID ;
9+ currentUser : UserWithCoursesAndSubjects ;
1110 onFlip ?: ( isBack : boolean ) => void ;
1211}
1312
14- export function Card ( { displayedUser, comparisonUserId, onFlip } : CardProps ) {
13+ const CardFront = ( { displayedUser, currentUser } : CardProps ) => {
14+ const containerRef = useRef < HTMLDivElement > ( null ) ;
15+ const interestsContainerRef = useRef < HTMLDivElement > ( null ) ;
16+ const coursesContainerRef = useRef < HTMLDivElement > ( null ) ;
17+ const [ isHiddenInterestExist , setHiddenInterestExist ] = useState ( false ) ;
18+ const [ isHiddenCourseExist , setHiddenCourseExist ] = useState ( false ) ;
19+
20+ useEffect ( ( ) => {
21+ const container = containerRef . current ;
22+ if ( ! container ) return ;
23+
24+ const resizeObserver = new ResizeObserver ( ( ) => {
25+ calculateVisibleInterests ( ) ;
26+ calculateVisibleCourses ( ) ;
27+ } ) ;
28+
29+ resizeObserver . observe ( container ) ;
30+
31+ calculateVisibleInterests ( ) ; // 初期計算
32+ calculateVisibleCourses ( ) ; // 初期計算
33+
34+ return ( ) => resizeObserver . disconnect ( ) ;
35+ } , [ ] ) ;
36+
37+ const calculateVisibleCourses = useCallback ( ( ) => {
38+ const courses = displayedUser . courses ;
39+ const container = coursesContainerRef . current ;
40+ if ( ! container ) return ;
41+
42+ const containerHeight = container . offsetHeight ; // コンテナの高さを取得
43+
44+ // 一旦コンテナを初期化
45+ container . innerHTML = "" ;
46+ setHiddenCourseExist ( false ) ;
47+
48+ // courses を一致・非一致で分類
49+ const matchingCourses = courses . filter ( ( course ) =>
50+ currentUser . courses . some ( ( c ) => c . id === course . id ) ,
51+ ) ;
52+ const nonMatchingCourses = courses . filter (
53+ ( course ) => ! currentUser . courses . some ( ( c ) => c . id === course . id ) ,
54+ ) ;
55+
56+ // courses を表示する flex コンテナ
57+ const coursesContainer = document . createElement ( "div" ) ;
58+ coursesContainer . classList . add ( "flex" , "flex-wrap" , "gap-2" ) ;
59+ container . appendChild ( coursesContainer ) ;
60+
61+ // 一致しているコースを先に表示
62+ for ( const course of [ ...matchingCourses , ...nonMatchingCourses ] ) {
63+ const isMatching = currentUser . courses . some ( ( c ) => c . id === course . id ) ;
64+
65+ // 新しい div 要素を作成
66+ const element = document . createElement ( "div" ) ;
67+ element . textContent = course . name ;
68+
69+ // スタイル適用(赤 or 灰色)
70+ element . classList . add ( "badge" , "badge-outline" ) ;
71+ element . style . backgroundColor = isMatching ? "red" : "gray" ;
72+ element . style . color = "white" ;
73+
74+ // 表示判定
75+ if ( coursesContainer . offsetHeight + 30 <= containerHeight ) {
76+ coursesContainer . appendChild ( element ) ;
77+ } else {
78+ setHiddenCourseExist ;
79+ }
80+ }
81+ } , [ displayedUser , currentUser ] ) ;
82+
83+ const calculateVisibleInterests = useCallback ( ( ) => {
84+ const interests = displayedUser . interestSubjects ;
85+ const container = interestsContainerRef . current ;
86+ if ( ! container ) return ;
87+
88+ const containerHeight = container . offsetHeight ; // コンテナの高さを取得
89+
90+ // 一旦コンテナを初期化
91+ container . innerHTML = "" ;
92+ setHiddenInterestExist ( false ) ;
93+
94+ // interests を一致・非一致で分類
95+ const matchingInterests = interests . filter ( ( interest ) =>
96+ currentUser . interestSubjects . some ( ( i ) => i . name === interest . name ) ,
97+ ) ;
98+ const nonMatchingInterests = interests . filter (
99+ ( interest ) =>
100+ ! currentUser . interestSubjects . some ( ( i ) => i . name === interest . name ) ,
101+ ) ;
102+
103+ // interests を表示する flex コンテナ
104+ const flexContainer = document . createElement ( "div" ) ;
105+ flexContainer . classList . add ( "flex" , "flex-wrap" , "gap-2" ) ;
106+ container . appendChild ( flexContainer ) ;
107+
108+ // 一致している興味分野を先に表示
109+ for ( const interest of [ ...matchingInterests , ...nonMatchingInterests ] ) {
110+ const isMatching = currentUser . interestSubjects . some (
111+ ( i ) => i . name === interest . name ,
112+ ) ;
113+
114+ // 新しい div 要素を作成
115+ const element = document . createElement ( "div" ) ;
116+ element . textContent = interest . name ;
117+
118+ // スタイル適用(赤 or 灰色)
119+ element . classList . add ( "badge" , "badge-outline" ) ;
120+ element . style . backgroundColor = isMatching ? "red" : "gray" ;
121+ element . style . color = "white" ;
122+ element . style . overflow = "hidden" ;
123+ element . style . whiteSpace = "nowrap" ;
124+ element . style . textOverflow = "ellipsis" ;
125+
126+ // 表示判定
127+ if ( flexContainer . offsetHeight + 30 <= containerHeight ) {
128+ flexContainer . appendChild ( element ) ;
129+ } else {
130+ setHiddenInterestExist ( true ) ;
131+ }
132+ }
133+ } , [ displayedUser , currentUser ] ) ;
134+
135+ return (
136+ < div className = "flex h-full flex-col gap-5 overflow-clip border-2 border-primary bg-secondary p-5" >
137+ < div className = "grid h-[20%] grid-cols-3 items-center" >
138+ < UserAvatar
139+ pictureUrl = { displayedUser . pictureUrl }
140+ width = "9dvh"
141+ height = "9dvh"
142+ />
143+ < div className = "col-span-2 grid grid-rows-3 items-center" >
144+ < p className = "col-span-3 font-bold text-1xl" > { displayedUser . name } </ p >
145+ < p className = "col-span-1 text-1xl" > { displayedUser . grade } </ p >
146+ < p className = "col-span-2 text-1xl" > { displayedUser . faculty } </ p >
147+ < p className = "col-span-2 text-1xl" > { displayedUser . department } </ p >
148+ </ div >
149+ </ div >
150+
151+ < div className = "flex h-[70%] w-full flex-col gap-2" ref = { containerRef } >
152+ < div
153+ ref = { interestsContainerRef }
154+ className = "width-full h-[50%] overflow-hidden"
155+ >
156+ < div />
157+ { isHiddenInterestExist && (
158+ < div className = "badge badge-outline bg-gray-200 text-gray-700" >
159+ And More
160+ </ div >
161+ ) }
162+ </ div >
163+
164+ < div
165+ ref = { coursesContainerRef }
166+ className = "width-full h-[50%] overflow-hidden"
167+ >
168+ < div />
169+ { isHiddenCourseExist && (
170+ < div className = "badge badge-outline bg-gray-200 text-gray-700" >
171+ And More
172+ </ div >
173+ ) }
174+ </ div >
175+ </ div >
176+ </ div >
177+ ) ;
178+ } ;
179+
180+ const CardBack = ( { displayedUser, currentUser } : CardProps ) => {
181+ return (
182+ < div className = "flex h-full flex-col overflow-hidden border-2 border-primary bg-secondary p-4" >
183+ < div className = "flex justify-center" >
184+ < p className = "font-bold text-lg" > { displayedUser ?. name } </ p >
185+ </ div >
186+ < NonEditableCoursesTable
187+ userId = { displayedUser . id }
188+ comparisonUserId = { currentUser . id }
189+ />
190+ < div className = "mt-4 flex justify-center" >
191+ < ThreeSixtyIcon className = "text-3xl" />
192+ </ div >
193+ </ div >
194+ ) ;
195+ } ;
196+
197+ export function Card ( { displayedUser, currentUser, onFlip } : CardProps ) {
15198 const [ isDisplayingBack , setIsDisplayingBack ] = useState ( false ) ;
16199
17200 const handleRotate = ( ) => {
@@ -39,7 +222,7 @@ export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) {
39222 transform : isDisplayingBack ? "rotateY(180deg)" : "rotateY(0deg)" ,
40223 } }
41224 >
42- < CardFront displayedUser = { displayedUser } />
225+ < CardFront displayedUser = { displayedUser } currentUser = { currentUser } />
43226 </ div >
44227 < div
45228 className = "absolute h-full w-full"
@@ -48,81 +231,9 @@ export function Card({ displayedUser, comparisonUserId, onFlip }: CardProps) {
48231 transform : isDisplayingBack ? "rotateY(0deg)" : "rotateY(-180deg)" ,
49232 } }
50233 >
51- < CardBack
52- displayedUser = { displayedUser }
53- comparisonUserId = { comparisonUserId }
54- />
234+ < CardBack displayedUser = { displayedUser } currentUser = { currentUser } />
55235 </ div >
56236 </ div >
57237 </ div >
58238 ) ;
59239}
60-
61- const CardFront = ( { displayedUser } : CardProps ) => {
62- return (
63- < div className = "flex h-full flex-col justify-between gap-5 overflow-hidden border-2 border-primary bg-secondary p-5" >
64- < div className = "grid h-[30%] grid-cols-3 items-center" >
65- < UserAvatar
66- pictureUrl = { displayedUser . pictureUrl }
67- width = "10dvh"
68- height = "10dvh"
69- />
70- < div className = "col-span-2 ml-2 flex justify-center" >
71- < span className = "font-bold text-4xl" > { displayedUser . name } </ span >
72- </ div >
73- </ div >
74- < div className = "grid grid-cols-6 items-center gap-4" >
75- < Chip label = "学部" size = "small" className = "col-span-1" />
76- < p className = "col-span-5 text-xl" > { displayedUser . faculty } </ p >
77- </ div >
78- < div className = "grid grid-cols-6 items-center gap-4" >
79- < Chip label = "学科" size = "small" className = "col-span-1" />
80- < p
81- className = { `col-span-5 text-xl ${ displayedUser . department . length > 7 ? "text-xs" : "text-2xl" } ` }
82- >
83- { displayedUser . department }
84- </ p >
85- </ div >
86- < div className = "grid grid-cols-6 items-center gap-4" >
87- < Chip label = "性別" size = "small" className = "col-span-1" />
88- < p className = "col-span-5 text-xl" > { displayedUser . gender } </ p >
89- </ div >
90- < div className = "grid grid-cols-6 items-center gap-4" >
91- < Chip label = "学年" size = "small" className = "col-span-1" />
92- < p className = "col-span-5 text-xl" > { displayedUser . grade } </ p >
93- </ div >
94- < div className = "grid max-h-[32%] flex-1 grid-cols-6 gap-4" >
95- < Chip label = "自己紹介" size = "small" className = "col-span-1 text-sm" />
96- < p className = "col-span-5 line-clamp-8 overflow-hidden text-sm" >
97- { displayedUser . intro }
98- </ p >
99- </ div >
100- < p > TODO: これはサンプルです</ p >
101- < ul >
102- { displayedUser . interestSubjects . map ( ( subject ) => (
103- < li key = { subject . id } > { subject . name } </ li >
104- ) ) }
105- </ ul >
106- < div className = "flex justify-center" >
107- < ThreeSixtyIcon className = "text-3xl" />
108- </ div >
109- </ div >
110- ) ;
111- } ;
112-
113- const CardBack = ( { displayedUser, comparisonUserId } : CardProps ) => {
114- return (
115- < div className = "flex h-full flex-col overflow-hidden border-2 border-primary bg-secondary p-4" >
116- < div className = "flex justify-center" >
117- < p className = "font-bold text-lg" > { displayedUser ?. name } </ p >
118- </ div >
119- < NonEditableCoursesTable
120- userId = { displayedUser . id }
121- comparisonUserId = { comparisonUserId }
122- />
123- < div className = "mt-4 flex justify-center" >
124- < ThreeSixtyIcon className = "text-3xl" />
125- </ div >
126- </ div >
127- ) ;
128- } ;
0 commit comments