From e80f7e6a5877ce58ae5366c6d2a892de0ccc5613 Mon Sep 17 00:00:00 2001 From: tonkaew131 Date: Sun, 27 Jul 2025 18:07:27 +0700 Subject: [PATCH] Add swipping mechanic --- .../[memberId]/_components/polls/index.tsx | 254 ++++++++++++++++++ app/polls/[id]/members/[memberId]/page.tsx | 14 + lib/supabase/database.types.ts | 35 +++ 3 files changed, 303 insertions(+) create mode 100644 app/polls/[id]/members/[memberId]/_components/polls/index.tsx diff --git a/app/polls/[id]/members/[memberId]/_components/polls/index.tsx b/app/polls/[id]/members/[memberId]/_components/polls/index.tsx new file mode 100644 index 0000000..62419a8 --- /dev/null +++ b/app/polls/[id]/members/[memberId]/_components/polls/index.tsx @@ -0,0 +1,254 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { useState, useRef, useEffect } from "react"; + +interface PollsProps { + choices: { + id: string; + name: string; + picture: string; + distance?: number; + }[]; +} + +interface SwipeResult { + choiceId: string; + name: string; + action: "like" | "pass"; + timestamp: Date; +} + +export default function Polls(props: PollsProps) { + const [currentIndex, setCurrentIndex] = useState(0); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); + const [isDragging, setIsDragging] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + const [results, setResults] = useState([]); + const cardRef = useRef(null); + + const currentChoice = props.choices[currentIndex]; + + // Global mouse event listeners + useEffect(() => { + const handleGlobalMouseMove = (e: MouseEvent) => { + if (isDragging) { + handleMove(e.clientX, e.clientY); + } + }; + + const handleGlobalMouseUp = () => { + if (isDragging) { + handleEnd(); + } + }; + + if (isDragging) { + document.addEventListener("mousemove", handleGlobalMouseMove); + document.addEventListener("mouseup", handleGlobalMouseUp); + } + + return () => { + document.removeEventListener("mousemove", handleGlobalMouseMove); + document.removeEventListener("mouseup", handleGlobalMouseUp); + }; + }, [isDragging, dragOffset.x]); + + // Handle swipe completion + const handleSwipe = (direction: "like" | "pass") => { + if (!currentChoice || isAnimating) return; + + const result: SwipeResult = { + choiceId: currentChoice.id, + name: currentChoice.name, + action: direction, + timestamp: new Date(), + }; + + setResults((prev) => [...prev, result]); + setIsAnimating(true); + + // Animate out + setDragOffset({ + x: direction === "like" ? 300 : -300, + y: 0, + }); + + setTimeout(() => { + if (currentIndex >= props.choices.length - 1) { + // All choices completed + const finalResults = [...results, result]; + console.log("🎉 Polling Complete! Final Results:", finalResults); + console.log("📊 Summary:", { + totalChoices: finalResults.length, + likes: finalResults.filter((r) => r.action === "like").length, + passes: finalResults.filter((r) => r.action === "pass").length, + choices: finalResults.map((r) => ({ + name: r.name, + action: r.action, + })), + }); + // Move beyond the last index to trigger completion state + setCurrentIndex(props.choices.length); + } else { + setCurrentIndex((prev) => prev + 1); + } + setDragOffset({ x: 0, y: 0 }); + setIsAnimating(false); + }, 300); + }; + + // Mouse/Touch event handlers + const handleStart = () => { + if (isAnimating) return; + setIsDragging(true); + }; + + const handleMove = (clientX: number, clientY: number) => { + if (!isDragging || isAnimating) return; + + const rect = cardRef.current?.getBoundingClientRect(); + if (!rect) return; + + const x = clientX - rect.left - rect.width / 2; + const y = clientY - rect.top - rect.height / 2; + + setDragOffset({ x: x * 0.5, y: y * 0.1 }); + }; + + const handleEnd = () => { + if (!isDragging || isAnimating) return; + setIsDragging(false); + + const threshold = 80; + if (Math.abs(dragOffset.x) > threshold) { + handleSwipe(dragOffset.x > 0 ? "like" : "pass"); + } else { + // Snap back + setDragOffset({ x: 0, y: 0 }); + } + }; + + // Mouse events + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + handleStart(); + }; + + // Touch events + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); + handleStart(); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + handleMove(touch.clientX, touch.clientY); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + e.preventDefault(); + handleEnd(); + }; + + // Button handlers + const handleLike = () => handleSwipe("like"); + const handlePass = () => handleSwipe("pass"); + + if (!currentChoice) { + return ( +
+
+

ตอบครบหมดแล้ว! 🎉

+

รอดูผลลัพธ์ในเร็วๆ นี้!

+
+
+ ); + } + return ( +
+
150 ? 0.5 : 1, + }} + onMouseDown={handleMouseDown} + onTouchStart={handleTouchStart} + onTouchMove={handleTouchMove} + onTouchEnd={handleTouchEnd} + > + + {currentChoice.name} + +
+ + {/* Swipe indicators */} + {Math.abs(dragOffset.x) > 50 && ( +
+
0 + ? "text-green-500 border-green-500 bg-green-500/20" + : "text-red-500 border-red-500 bg-red-500/20" + }`} + style={{ + opacity: Math.min(Math.abs(dragOffset.x) / 150, 1), + transform: `rotate(${dragOffset.x > 0 ? -15 : 15}deg)`, + }} + > + {dragOffset.x > 0 ? "LIKE" : "PASS"} +
+
+ )} + +
+ +
+
+

+ {currentChoice.name} +

+

+ {currentIndex + 1} of {props.choices.length} +

+
+
+ + +
+
+
+ ); +} diff --git a/app/polls/[id]/members/[memberId]/page.tsx b/app/polls/[id]/members/[memberId]/page.tsx index 41e9db7..cd406fa 100644 --- a/app/polls/[id]/members/[memberId]/page.tsx +++ b/app/polls/[id]/members/[memberId]/page.tsx @@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button"; import { createClient } from "@/lib/supabase/server"; import { ChevronLeftIcon } from "lucide-react"; import Link from "next/link"; +import Polls from "./_components/polls"; interface PageProps { params: Promise<{ id: string; memberId: string }>; @@ -16,6 +17,10 @@ export default async function Page(props: PageProps) { .select() .eq("poll_id", id) .eq("id", parseInt(memberId)); + const { data: choices } = await supabase + .from("poll_choices") + .select() + .eq("poll_id", id); if (!polls || polls.length === 0) { return
Poll not found
; @@ -43,6 +48,15 @@ export default async function Page(props: PageProps) { + + ({ + id: c.id, + name: c.name, + picture: c.picture, + distance: c.distance || undefined, + }))} + /> ); } diff --git a/lib/supabase/database.types.ts b/lib/supabase/database.types.ts index 62b9246..521c5e4 100644 --- a/lib/supabase/database.types.ts +++ b/lib/supabase/database.types.ts @@ -14,6 +14,41 @@ export type Database = { } public: { Tables: { + poll_choices: { + Row: { + created_at: string + distance: number | null + id: string + name: string + picture: string + poll_id: string + } + Insert: { + created_at?: string + distance?: number | null + id?: string + name: string + picture: string + poll_id: string + } + Update: { + created_at?: string + distance?: number | null + id?: string + name?: string + picture?: string + poll_id?: string + } + Relationships: [ + { + foreignKeyName: "poll_choices_poll_id_fkey" + columns: ["poll_id"] + isOneToOne: false + referencedRelation: "polls" + referencedColumns: ["id"] + }, + ] + } poll_members: { Row: { created_at: string