diff --git a/web/app/home/page.tsx b/web/app/home/page.tsx index b6c0a7b1..da45ff4b 100644 --- a/web/app/home/page.tsx +++ b/web/app/home/page.tsx @@ -2,7 +2,13 @@ import type { UserWithCoursesAndSubjects } from "common/types"; import { motion, useAnimation } from "framer-motion"; -import { useCallback, useEffect, useState } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { MdClose, MdThumbUp } from "react-icons/md"; import request from "~/api/request"; import { useAboutMe, useRecommended } from "~/api/user"; @@ -18,8 +24,8 @@ export default function Home() { const controls = useAnimation(); const backCardControls = useAnimation(); const [clickedButton, setClickedButton] = useState(""); - const [openDetailedMenu, setOpenDetailedMenu] = useState(false); + const { state: { data: currentUser }, } = useAboutMe(); @@ -30,6 +36,33 @@ export default function Home() { >(() => new Queue([])); const [loading, setLoading] = useState(true); + // コンテナと topCard の DOM 参照を用意 + const containerRef = useRef(null); + const topCardRef = useRef(null); + // topCard のコンテナ内での相対位置(backCard の最終的な配置位置)を保存 + const [targetPos, setTargetPos] = useState({ x: 0, y: 0 }); + + // 初期オフセット:右方向へのずれを防ぐため x は 0、縦方向は必要に応じて設定(例: 20) + const initialOffset = { x: 0, y: 0 }; + + // レイアウト完了後に topCard の位置を計算する + useLayoutEffect(() => { + if (topCardRef.current && containerRef.current) { + const containerRect = containerRef.current.getBoundingClientRect(); + const topCardRect = topCardRef.current.getBoundingClientRect(); + setTargetPos({ + x: topCardRect.left - containerRect.left, + y: topCardRect.top - containerRect.top, + }); + // backCard のコントロールに初期オフセットを設定(レンダリング位置と合わせる) + backCardControls.set(initialOffset); + } + }, [backCardControls]); + + useLayoutEffect(() => { + if (data) setRecommended(new Queue(data)); + }, [data]); + useEffect(() => { if (data) { setRecommended(new Queue(data)); @@ -47,23 +80,24 @@ export default function Home() { setClickedButton(action === "accept" ? "heart" : "cross"); - // アニメーション開始前に BackCard の位置をリセット - backCardControls.set({ x: 0, y: 0 }); + // アニメーション開始前に backCard を初期レンダリング位置(initialOffset)に設定 + backCardControls.set(initialOffset); - // 移動アニメーションを実行 await Promise.all([ + // トップカードは画面外へ移動(画面サイズに合わせる) controls.start({ - x: action === "accept" ? 1000 : -1000, + x: action === "accept" ? window.innerWidth : -window.innerWidth, transition: { duration: 0.5, delay: 0.2 }, }), + // backCard は computed した topCard の位置 (targetPos) に移動 backCardControls.start({ - x: 10, - y: 10, + x: targetPos.x, + y: targetPos.y, transition: { duration: 0.5, delay: 0.2 }, }), ]); - // 状態更新 + // キューの更新などの処理 recommended.pop(); if (action === "accept") { await request.send(current.id); @@ -72,13 +106,12 @@ export default function Home() { } rerender({}); - // 位置をリセット + // アニメーション後に位置をリセット(backCard は再び初期レンダリング位置に戻す) controls.set({ x: 0 }); - backCardControls.set({ x: 0, y: 0 }); - + backCardControls.set(initialOffset); setClickedButton(""); }, - [recommended, controls, backCardControls], + [recommended, controls, backCardControls, targetPos], ); if (loading) { @@ -93,20 +126,26 @@ export default function Home() { if (error) throw error; return ( -
+
{displayedUser && (
{nextUser && (
+ {/* backCard: 初期レンダリング位置とアニメーション開始位置を両方とも initialOffset に合わせる */} + {/* トップカード: この位置を基準にするために ref を設定 */} @@ -162,6 +202,7 @@ export default function Home() { ); } +// Queue クラス(状態管理用) class Queue { private store: T[]; constructor(initial: T[]) { @@ -170,7 +211,7 @@ class Queue { push(top: T): void { this.store.push(top); } - // peek(0) to peek the next elem to be popped, peek(1) peeks the second next element to be popped. + // peek(0): 次にポップされる要素、peek(1): その次の要素 peek(nth: number): T | undefined { return this.store[nth]; } diff --git a/web/components/DraggableCard.tsx b/web/components/DraggableCard.tsx index 4856f86e..59d6429e 100644 --- a/web/components/DraggableCard.tsx +++ b/web/components/DraggableCard.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { MdClose, MdThumbUp } from "react-icons/md"; import { Card } from "./Card"; -const SWIPE_THRESHOLD = 30; +const SWIPE_THRESHOLD = 50; interface DraggableCardProps { displayedUser: UserWithCoursesAndSubjects; @@ -30,22 +30,13 @@ export const DraggableCard = ({ useMotionValueEvent(dragX, "change", (latest: number) => { if (dragging) { - dragX.set(latest); setDragProgress(latest); } else { - dragX.set(0); setDragProgress(0); } }); - useMotionValueEvent(dragY, "change", (latest: number) => { - if (dragging) { - dragY.set(latest); - } else { - dragY.set(0); - } - }); - + // ドラッグ処理の他の部分はそのまま const CardOverlay = () => { return (
@@ -68,14 +59,15 @@ export const DraggableCard = ({ ); }; - const handleDragEnd = useCallback(() => { - const x = dragX.get(); - if (x > SWIPE_THRESHOLD) { - onSwipeRight(); - } - if (x < -SWIPE_THRESHOLD) { - onSwipeLeft(); + const handleDragEnd = useCallback(async () => { + const xValue = dragX.get(); + if (xValue > SWIPE_THRESHOLD) { + await Promise.resolve(onSwipeRight()); + } else if (xValue < -SWIPE_THRESHOLD) { + await Promise.resolve(onSwipeLeft()); } + dragX.stop(); + dragY.stop(); dragX.set(0); dragY.set(0); }, [dragX, dragY, onSwipeRight, onSwipeLeft]); @@ -89,11 +81,10 @@ export const DraggableCard = ({ drag dragElastic={0.9} dragListener={true} - dragConstraints={{ left: 0, right: 0 }} onDragStart={() => setDragging(true)} onDragEnd={() => { - setDragging(false); handleDragEnd(); + setDragging(false); }} style={{ x: dragX, y: dragY, padding: "10px" }} whileTap={{ scale: 0.95 }}