Skip to content

Commit 7561741

Browse files
authored
Feat/card with interests (#543)
# PRの概要 v2のカード実装 close #532 ## 具体的な変更内容 カードののビュー カードの表示情報を変更 ## 影響範囲 とくになし ## 動作要件 とくになし ## 補足 とくになし ## レビューリクエストを出す前にチェック! - [ ] 改めてセルフレビューしたか - [ ] 手動での動作検証を行ったか - [ ] server の機能追加ならば、テストを書いたか - 理由: 書いた | server の機能追加ではない - [ ] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 書いた | 間違った使い方は存在しない - [ ] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 -->
1 parent cf3e6f2 commit 7561741

File tree

5 files changed

+212
-100
lines changed

5 files changed

+212
-100
lines changed

web/app/home/page.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { motion, useAnimation } from "framer-motion";
66
import { useCallback, useEffect, useState } from "react";
77
import { MdThumbUp } from "react-icons/md";
88
import request from "~/api/request";
9-
import { useMyID, useRecommended } from "~/api/user";
9+
import { useAboutMe, useRecommended } from "~/api/user";
1010
import { Card } from "~/components/Card";
1111
import { DraggableCard } from "~/components/DraggableCard";
1212
import FullScreenCircularProgress from "~/components/common/FullScreenCircularProgress";
@@ -17,8 +17,8 @@ export default function Home() {
1717
const controls = useAnimation();
1818
const [clickedButton, setClickedButton] = useState<string>("");
1919
const {
20-
state: { data: myId },
21-
} = useMyID();
20+
state: { data: currentUser },
21+
} = useAboutMe();
2222

2323
const [_, rerender] = useState({});
2424
const [recommended, setRecommended] = useState<
@@ -71,6 +71,9 @@ export default function Home() {
7171
});
7272
}, [controls, accept]);
7373

74+
if (currentUser == null) {
75+
return <FullScreenCircularProgress />;
76+
}
7477
if (recommended == null) {
7578
return <FullScreenCircularProgress />;
7679
}
@@ -89,15 +92,15 @@ export default function Home() {
8992
{nextUser && (
9093
<div className="relative h-full w-full">
9194
<div className="-translate-x-4 -translate-y-4 inset-0 z-0 mt-4 transform">
92-
<Card displayedUser={nextUser} />
95+
<Card displayedUser={nextUser} currentUser={currentUser} />
9396
</div>
9497
<motion.div
9598
animate={controls}
9699
className="absolute inset-0 z-10 mt-4 flex items-center justify-center"
97100
>
98101
<DraggableCard
99102
displayedUser={displayedUser}
100-
comparisonUserId={myId || undefined}
103+
currentUser={currentUser}
101104
onSwipeLeft={reject}
102105
onSwipeRight={accept}
103106
clickedButton={clickedButton}

web/app/settings/profile/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ export default function SettingsProfile() {
4242
編集する
4343
</Link>
4444
</div>
45-
<Card displayedUser={data} onFlip={(back) => setBack(back)} />
45+
<Card
46+
displayedUser={data}
47+
currentUser={data}
48+
onFlip={(back) => setBack(back)}
49+
/>
4650
</div>
4751
</div>
4852
)}

web/components/Card.tsx

Lines changed: 190 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,200 @@
11
import 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";
54
import NonEditableCoursesTable from "./course/NonEditableCoursesTable";
65
import UserAvatar from "./human/avatar";
76

87
interface 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-
};

web/components/DraggableCard.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import CloseIcon from "@mui/icons-material/Close";
22
import { Box, Typography } from "@mui/material";
3-
import type { UserID, UserWithCoursesAndSubjects } from "common/types";
3+
import type { UserWithCoursesAndSubjects } from "common/types";
44
import { motion, useMotionValue, useMotionValueEvent } from "framer-motion";
55
import { useCallback, useState } from "react";
66
import { MdThumbUp } from "react-icons/md";
@@ -10,15 +10,15 @@ const SWIPE_THRESHOLD = 30;
1010

1111
interface DraggableCardProps {
1212
displayedUser: UserWithCoursesAndSubjects;
13-
comparisonUserId?: UserID;
13+
currentUser: UserWithCoursesAndSubjects;
1414
onSwipeRight: () => void;
1515
onSwipeLeft: () => void;
1616
clickedButton: string;
1717
}
1818

1919
export const DraggableCard = ({
2020
displayedUser,
21-
comparisonUserId,
21+
currentUser,
2222
onSwipeRight,
2323
onSwipeLeft,
2424
clickedButton,
@@ -136,10 +136,7 @@ export const DraggableCard = ({
136136
whileTap={{ scale: 0.95 }}
137137
>
138138
<CardOverlay />
139-
<Card
140-
displayedUser={displayedUser}
141-
comparisonUserId={comparisonUserId}
142-
/>
139+
<Card displayedUser={displayedUser} currentUser={currentUser} />
143140
</motion.div>
144141
</section>
145142
</div>

0 commit comments

Comments
 (0)