Skip to content

Commit e4d8993

Browse files
GuY8528nakaterm
andauthored
カードの裏面削除・デザイン修正 (#564)
# PRの概要 * カードの裏面を削除し、詳細表示 (#565) が出るようにした (close #493) * カード表面のデザインを修正した。 ## 具体的な変更内容 https://github.com/user-attachments/assets/ce3e3752-bc73-46d8-8b93-103be2b9ccf4 ## 影響範囲 ## 動作要件 ## 補足 ## レビューリクエストを出す前にチェック! - [ ] 改めてセルフレビューしたか - [ ] 手動での動作検証を行ったか - [ ] server の機能追加ならば、テストを書いたか - 理由: 書いた | server の機能追加ではない - [ ] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 書いた | 間違った使い方は存在しない - [ ] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 --> --------- Co-authored-by: naka-12 <[email protected]>
1 parent 565fe58 commit e4d8993

File tree

5 files changed

+289
-195
lines changed

5 files changed

+289
-195
lines changed

server/src/functions/engines/recommendation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { UserID, UserWithCoursesAndSubjects } from "common/types";
44
import { prisma } from "../../database/client";
55
import { getCoursesByUserId } from "../../database/courses";
66
import * as interest from "../../database/interest";
7+
import { getUserByID } from "../../database/users";
78

89
export async function recommendedTo(
910
user: UserID,
@@ -24,12 +25,15 @@ export async function recommendedTo(
2425
const { overlap: count, ...u } = res;
2526
if (count === null)
2627
throw new Error("count is null: something is wrong");
28+
// TODO: user の情報はここで再度 DB に問い合わせるのではなく、 recommend の sql で取得
29+
const user = await getUserByID(u.id);
30+
if (!user.ok) throw new Error("user not found");
2731
const courses = getCoursesByUserId(u.id);
2832
const subjects = interest.of(u.id);
2933
return {
3034
count: Number(count),
3135
u: {
32-
...u,
36+
...user.value,
3337
courses: await courses,
3438
interestSubjects: await subjects,
3539
},

web/api/user.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ export function useAll(): Hook<UserWithCoursesAndSubjects[]> {
2424
}
2525
export function useRecommended(): UseHook<UserWithCoursesAndSubjects[]> {
2626
const url = endpoints.recommendedUsers;
27-
return useAuthorizedData<UserWithCoursesAndSubjects[]>(
28-
url,
29-
z.array(UserWithCoursesAndSubjectsSchema),
30-
);
27+
return useAuthorizedData<UserWithCoursesAndSubjects[]>(url, UserListSchema);
3128
}
3229
export function useMatched(): Hook<UserWithCoursesAndSubjects[]> {
3330
return useCustomizedSWR("users::matched", matched, UserListSchema);

web/app/home/page.tsx

Lines changed: 100 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,66 @@ import PersonDetailedMenu from "./components/PersonDetailedMenu";
1515
export default function Home() {
1616
const { data, error } = useRecommended();
1717
const controls = useAnimation();
18+
const backCardControls = useAnimation();
1819
const [clickedButton, setClickedButton] = useState<string>("");
20+
1921
const [openDetailedMenu, setOpenDetailedMenu] = useState(false);
2022
const {
2123
state: { data: currentUser },
2224
} = useAboutMe();
2325

2426
const [_, rerender] = useState({});
25-
const [recommended, setRecommended] =
26-
useState<Queue<UserWithCoursesAndSubjects> | null>(null);
27+
const [recommended, setRecommended] = useState<
28+
Queue<UserWithCoursesAndSubjects>
29+
>(() => new Queue([]));
30+
2731
useEffect(() => {
2832
if (data) setRecommended(new Queue(data));
2933
}, [data]);
3034

31-
const displayedUser = recommended?.peek(0);
32-
const nextUser = recommended?.peek(1);
33-
const reject = useCallback(() => {
34-
const current = recommended?.pop();
35-
if (!current) return;
36-
recommended?.push(current);
37-
rerender({});
38-
}, [recommended]);
39-
const accept = useCallback(async () => {
40-
const current = recommended?.pop();
41-
if (!current) return;
42-
request.send(current.id);
43-
rerender({});
44-
}, [recommended]);
45-
46-
const onClickClose = useCallback(() => {
47-
setClickedButton("cross");
48-
controls
49-
.start({
50-
x: [0, -1000],
51-
transition: { duration: 0.5, times: [0, 1], delay: 0.2 },
52-
})
53-
.then(() => {
54-
reject();
55-
setClickedButton("");
56-
controls.set({ x: 0 });
57-
});
58-
}, [controls, reject]);
59-
60-
const onClickHeart = useCallback(() => {
61-
setClickedButton("heart");
62-
controls
63-
.start({
64-
x: [0, 1000],
65-
transition: { duration: 0.5, times: [0, 1], delay: 0.2 },
66-
})
67-
.then(() => {
68-
accept();
69-
setClickedButton("");
70-
controls.set({ x: 0 });
71-
});
72-
}, [controls, accept]);
35+
const displayedUser = recommended.peek(0);
36+
const nextUser = recommended.peek(1);
37+
38+
const handleAction = useCallback(
39+
async (action: "accept" | "reject") => {
40+
const current = recommended.peek(0);
41+
if (!current) return;
42+
43+
setClickedButton(action === "accept" ? "heart" : "cross");
44+
45+
// アニメーション開始前に BackCard の位置をリセット
46+
backCardControls.set({ x: 0, y: 0 });
47+
48+
// 移動アニメーションを実行
49+
await Promise.all([
50+
controls.start({
51+
x: action === "accept" ? 1000 : -1000,
52+
transition: { duration: 0.5, delay: 0.2 },
53+
}),
54+
backCardControls.start({
55+
x: 10,
56+
y: 10,
57+
transition: { duration: 0.5, delay: 0.2 },
58+
}),
59+
]);
60+
61+
// 状態更新
62+
recommended.pop();
63+
if (action === "accept") {
64+
await request.send(current.id);
65+
} else if (action === "reject") {
66+
recommended.push(current);
67+
}
68+
rerender({});
69+
70+
// 位置をリセット
71+
controls.set({ x: 0 });
72+
backCardControls.set({ x: 0, y: 0 });
73+
74+
setClickedButton("");
75+
},
76+
[recommended, controls, backCardControls],
77+
);
7378

7479
if (recommended == null) {
7580
return <FullScreenCircularProgress />;
@@ -85,40 +90,57 @@ export default function Home() {
8590
return (
8691
<div className="flex h-full flex-col items-center justify-center p-4">
8792
{displayedUser && (
88-
<>
89-
<div className="flex h-full flex-col items-center justify-center">
90-
{nextUser && (
91-
<div className="relative h-full w-full">
92-
<div className="-translate-x-4 -translate-y-4 inset-0 z-0 mt-4 transform">
93-
<Card displayedUser={nextUser} currentUser={currentUser} />
94-
</div>
95-
<motion.div
96-
animate={controls}
97-
className="absolute inset-0 z-10 mt-4 flex items-center justify-center"
98-
>
99-
<DraggableCard
100-
displayedUser={displayedUser}
101-
currentUser={currentUser}
102-
onSwipeLeft={reject}
103-
onSwipeRight={accept}
104-
clickedButton={clickedButton}
105-
/>
106-
</motion.div>
107-
</div>
108-
)}
109-
<button
110-
type="button"
111-
onClick={() => setOpenDetailedMenu(!openDetailedMenu)}
112-
>
113-
てすと
114-
</button>
115-
<div className="button-container mt-4 mb-4 flex w-full justify-center space-x-8">
116-
<CloseButton onclick={onClickClose} icon={<CloseIconStyled />} />
117-
<GoodButton
118-
onclick={onClickHeart}
119-
icon={<FavoriteIconStyled />}
120-
/>
93+
<div className="flex h-full flex-col items-center justify-center">
94+
{nextUser && (
95+
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
96+
<motion.div
97+
className="z-0 col-start-1 row-start-1 mt-4"
98+
initial={{ x: 0, y: 0 }} // 初期位置を (0, 0) に設定
99+
animate={backCardControls}
100+
>
101+
<Card displayedUser={nextUser} currentUser={currentUser} />
102+
</motion.div>
103+
<motion.div
104+
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
105+
animate={controls}
106+
>
107+
<DraggableCard
108+
displayedUser={displayedUser}
109+
currentUser={currentUser}
110+
onSwipeLeft={() => handleAction("reject")}
111+
onSwipeRight={() => handleAction("accept")}
112+
clickedButton={clickedButton}
113+
setOpenDetailedMenu={setOpenDetailedMenu}
114+
/>
115+
</motion.div>
116+
</div>
117+
)}
118+
{nextUser == null && (
119+
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
120+
<motion.div
121+
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
122+
animate={controls}
123+
>
124+
<DraggableCard
125+
displayedUser={displayedUser}
126+
currentUser={currentUser}
127+
onSwipeLeft={() => handleAction("reject")}
128+
onSwipeRight={() => handleAction("accept")}
129+
clickedButton={clickedButton}
130+
setOpenDetailedMenu={setOpenDetailedMenu}
131+
/>
132+
</motion.div>
121133
</div>
134+
)}
135+
<div className="button-container mt-4 mb-4 flex w-full justify-center space-x-8">
136+
<CloseButton
137+
onclick={() => handleAction("reject")}
138+
icon={<CloseIconStyled />}
139+
/>
140+
<GoodButton
141+
onclick={() => handleAction("accept")}
142+
icon={<FavoriteIconStyled />}
143+
/>
122144
</div>
123145
{openDetailedMenu && (
124146
<PersonDetailedMenu
@@ -129,7 +151,7 @@ export default function Home() {
129151
currentUser={currentUser}
130152
/>
131153
)}
132-
</>
154+
</div>
133155
)}
134156
</div>
135157
);
@@ -179,8 +201,5 @@ class Queue<T> {
179201
}
180202
pop(): T | undefined {
181203
return this.store.shift();
182-
// yes, I know what you want to say, it has O(n) time complexity.
183-
// it doesn't really matter if there is only like 100 people in home queue at most.
184-
// if you really care about performance, why don't you go and limit the amount of people to fetch? that probably has significantly more impact to the performance.
185204
}
186205
}

0 commit comments

Comments
 (0)