Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions web/app/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -18,8 +24,8 @@ export default function Home() {
const controls = useAnimation();
const backCardControls = useAnimation();
const [clickedButton, setClickedButton] = useState<string>("");

const [openDetailedMenu, setOpenDetailedMenu] = useState(false);

const {
state: { data: currentUser },
} = useAboutMe();
Expand All @@ -30,6 +36,33 @@ export default function Home() {
>(() => new Queue([]));
const [loading, setLoading] = useState<boolean>(true);

// コンテナと topCard の DOM 参照を用意
const containerRef = useRef<HTMLDivElement>(null);
const topCardRef = useRef<HTMLDivElement>(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));
Expand All @@ -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);
Expand All @@ -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) {
Expand All @@ -93,20 +126,26 @@ export default function Home() {
if (error) throw error;

return (
<div className="flex h-full flex-col items-center justify-center p-4">
<div
ref={containerRef}
className="flex h-full flex-col items-center justify-center p-4"
>
{displayedUser && (
<div className="flex h-full flex-col items-center justify-center">
{nextUser && (
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
{/* backCard: 初期レンダリング位置とアニメーション開始位置を両方とも initialOffset に合わせる */}
<motion.div
className="z-0 col-start-1 row-start-1 mt-4"
initial={{ x: 0, y: 0 }} // 初期位置を (0, 0) に設定
initial={initialOffset}
animate={backCardControls}
>
<Card displayedUser={nextUser} currentUser={currentUser} />
</motion.div>
{/* トップカード: この位置を基準にするために ref を設定 */}
<motion.div
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
ref={topCardRef}
className="z-10 col-start-1 row-start-1 mt-4"
animate={controls}
>
<DraggableCard
Expand All @@ -123,6 +162,7 @@ export default function Home() {
{nextUser == null && (
<div className="relative grid h-full w-full grid-cols-1 grid-rows-1">
<motion.div
ref={topCardRef}
className="z-10 col-start-1 row-start-1 mt-4 flex items-center justify-center"
animate={controls}
>
Expand Down Expand Up @@ -162,6 +202,7 @@ export default function Home() {
);
}

// Queue クラス(状態管理用)
class Queue<T> {
private store: T[];
constructor(initial: T[]) {
Expand All @@ -170,7 +211,7 @@ class Queue<T> {
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];
}
Expand Down
31 changes: 11 additions & 20 deletions web/components/DraggableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
<div>
Expand All @@ -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]);
Expand All @@ -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 }}
Expand Down