Skip to content

Commit 0b39bd1

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

File tree

2 files changed

+69
-37
lines changed

2 files changed

+69
-37
lines changed

web/app/home/page.tsx

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
import type { UserWithCoursesAndSubjects } from "common/types";
44
import { motion, useAnimation } from "framer-motion";
5-
import { useCallback, useEffect, useState } from "react";
5+
import {
6+
useCallback,
7+
useEffect,
8+
useLayoutEffect,
9+
useRef,
10+
useState,
11+
} from "react";
612
import { MdClose, MdThumbUp } from "react-icons/md";
713
import request from "~/api/request";
814
import { useAboutMe, useRecommended } from "~/api/user";
@@ -18,8 +24,8 @@ export default function Home() {
1824
const controls = useAnimation();
1925
const backCardControls = useAnimation();
2026
const [clickedButton, setClickedButton] = useState<string>("");
21-
2227
const [openDetailedMenu, setOpenDetailedMenu] = useState(false);
28+
2329
const {
2430
state: { data: currentUser },
2531
} = useAboutMe();
@@ -30,6 +36,33 @@ export default function Home() {
3036
>(() => new Queue([]));
3137
const [loading, setLoading] = useState<boolean>(true);
3238

39+
// コンテナと topCard の DOM 参照を用意
40+
const containerRef = useRef<HTMLDivElement>(null);
41+
const topCardRef = useRef<HTMLDivElement>(null);
42+
// topCard のコンテナ内での相対位置(backCard の最終的な配置位置)を保存
43+
const [targetPos, setTargetPos] = useState({ x: 0, y: 0 });
44+
45+
// 初期オフセット:右方向へのずれを防ぐため x は 0、縦方向は必要に応じて設定(例: 20)
46+
const initialOffset = { x: 0, y: 0 };
47+
48+
// レイアウト完了後に topCard の位置を計算する
49+
useLayoutEffect(() => {
50+
if (topCardRef.current && containerRef.current) {
51+
const containerRect = containerRef.current.getBoundingClientRect();
52+
const topCardRect = topCardRef.current.getBoundingClientRect();
53+
setTargetPos({
54+
x: topCardRect.left - containerRect.left,
55+
y: topCardRect.top - containerRect.top,
56+
});
57+
// backCard のコントロールに初期オフセットを設定(レンダリング位置と合わせる)
58+
backCardControls.set(initialOffset);
59+
}
60+
}, [backCardControls]);
61+
62+
useLayoutEffect(() => {
63+
if (data) setRecommended(new Queue(data));
64+
}, [data]);
65+
3366
useEffect(() => {
3467
if (data) {
3568
setRecommended(new Queue(data));
@@ -47,23 +80,24 @@ export default function Home() {
4780

4881
setClickedButton(action === "accept" ? "heart" : "cross");
4982

50-
// アニメーション開始前に BackCard の位置をリセット
51-
backCardControls.set({ x: 0, y: 0 });
83+
// アニメーション開始前に backCard を初期レンダリング位置(initialOffset)に設定
84+
backCardControls.set(initialOffset);
5285

53-
// 移動アニメーションを実行
5486
await Promise.all([
87+
// トップカードは画面外へ移動(画面サイズに合わせる)
5588
controls.start({
56-
x: action === "accept" ? 1000 : -1000,
89+
x: action === "accept" ? window.innerWidth : -window.innerWidth,
5790
transition: { duration: 0.5, delay: 0.2 },
5891
}),
92+
// backCard は computed した topCard の位置 (targetPos) に移動
5993
backCardControls.start({
60-
x: 10,
61-
y: 10,
94+
x: targetPos.x,
95+
y: targetPos.y,
6296
transition: { duration: 0.5, delay: 0.2 },
6397
}),
6498
]);
6599

66-
// 状態更新
100+
// キューの更新などの処理
67101
recommended.pop();
68102
if (action === "accept") {
69103
await request.send(current.id);
@@ -72,13 +106,12 @@ export default function Home() {
72106
}
73107
rerender({});
74108

75-
// 位置をリセット
109+
// アニメーション後に位置をリセット(backCard は再び初期レンダリング位置に戻す)
76110
controls.set({ x: 0 });
77-
backCardControls.set({ x: 0, y: 0 });
78-
111+
backCardControls.set(initialOffset);
79112
setClickedButton("");
80113
},
81-
[recommended, controls, backCardControls],
114+
[recommended, controls, backCardControls, targetPos],
82115
);
83116

84117
if (loading) {
@@ -93,20 +126,26 @@ export default function Home() {
93126
if (error) throw error;
94127

95128
return (
96-
<div className="flex h-full flex-col items-center justify-center p-4">
129+
<div
130+
ref={containerRef}
131+
className="flex h-full flex-col items-center justify-center p-4"
132+
>
97133
{displayedUser && (
98134
<div className="flex h-full flex-col items-center justify-center">
99135
{nextUser && (
100136
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
137+
{/* backCard: 初期レンダリング位置とアニメーション開始位置を両方とも initialOffset に合わせる */}
101138
<motion.div
102139
className="z-0 col-start-1 row-start-1 mt-4"
103-
initial={{ x: 0, y: 0 }} // 初期位置を (0, 0) に設定
140+
initial={initialOffset}
104141
animate={backCardControls}
105142
>
106143
<Card displayedUser={nextUser} currentUser={currentUser} />
107144
</motion.div>
145+
{/* トップカード: この位置を基準にするために ref を設定 */}
108146
<motion.div
109-
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
147+
ref={topCardRef}
148+
className="z-10 col-start-1 row-start-1 mt-4"
110149
animate={controls}
111150
>
112151
<DraggableCard
@@ -123,6 +162,7 @@ export default function Home() {
123162
{nextUser == null && (
124163
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
125164
<motion.div
165+
ref={topCardRef}
126166
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
127167
animate={controls}
128168
>
@@ -162,6 +202,7 @@ export default function Home() {
162202
);
163203
}
164204

205+
// Queue クラス(状態管理用)
165206
class Queue<T> {
166207
private store: T[];
167208
constructor(initial: T[]) {
@@ -170,7 +211,7 @@ class Queue<T> {
170211
push(top: T): void {
171212
this.store.push(top);
172213
}
173-
// peek(0) to peek the next elem to be popped, peek(1) peeks the second next element to be popped.
214+
// peek(0): 次にポップされる要素、peek(1): その次の要素
174215
peek(nth: number): T | undefined {
175216
return this.store[nth];
176217
}

web/components/DraggableCard.tsx

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useCallback, useState } from "react";
44
import { MdClose, MdThumbUp } from "react-icons/md";
55
import { Card } from "./Card";
66

7-
const SWIPE_THRESHOLD = 30;
7+
const SWIPE_THRESHOLD = 50;
88

99
interface DraggableCardProps {
1010
displayedUser: UserWithCoursesAndSubjects;
@@ -30,22 +30,13 @@ export const DraggableCard = ({
3030

3131
useMotionValueEvent(dragX, "change", (latest: number) => {
3232
if (dragging) {
33-
dragX.set(latest);
3433
setDragProgress(latest);
3534
} else {
36-
dragX.set(0);
3735
setDragProgress(0);
3836
}
3937
});
4038

41-
useMotionValueEvent(dragY, "change", (latest: number) => {
42-
if (dragging) {
43-
dragY.set(latest);
44-
} else {
45-
dragY.set(0);
46-
}
47-
});
48-
39+
// ドラッグ処理の他の部分はそのまま
4940
const CardOverlay = () => {
5041
return (
5142
<div>
@@ -68,14 +59,15 @@ export const DraggableCard = ({
6859
);
6960
};
7061

71-
const handleDragEnd = useCallback(() => {
72-
const x = dragX.get();
73-
if (x > SWIPE_THRESHOLD) {
74-
onSwipeRight();
75-
}
76-
if (x < -SWIPE_THRESHOLD) {
77-
onSwipeLeft();
62+
const handleDragEnd = useCallback(async () => {
63+
const xValue = dragX.get();
64+
if (xValue > SWIPE_THRESHOLD) {
65+
await Promise.resolve(onSwipeRight());
66+
} else if (xValue < -SWIPE_THRESHOLD) {
67+
await Promise.resolve(onSwipeLeft());
7868
}
69+
dragX.stop();
70+
dragY.stop();
7971
dragX.set(0);
8072
dragY.set(0);
8173
}, [dragX, dragY, onSwipeRight, onSwipeLeft]);
@@ -89,11 +81,10 @@ export const DraggableCard = ({
8981
drag
9082
dragElastic={0.9}
9183
dragListener={true}
92-
dragConstraints={{ left: 0, right: 0 }}
9384
onDragStart={() => setDragging(true)}
9485
onDragEnd={() => {
95-
setDragging(false);
9686
handleDragEnd();
87+
setDragging(false);
9788
}}
9889
style={{ x: dragX, y: dragY, padding: "10px" }}
9990
whileTap={{ scale: 0.95 }}

0 commit comments

Comments
 (0)