Kinda working, dragging and dropping is working good
This commit is contained in:
@@ -1,290 +1,455 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { createWebSocketClient } from '$lib/websocketClient';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { createWebSocketClient } from '$lib/websocketClient';
|
||||
import type { Player } from '$lib/types';
|
||||
|
||||
interface Card {
|
||||
id: number;
|
||||
artist: string;
|
||||
songTitle: string;
|
||||
year: number;
|
||||
isFlipped: boolean;
|
||||
position: number;
|
||||
}
|
||||
// Game types
|
||||
interface SongCard {
|
||||
id: number;
|
||||
artist: string;
|
||||
title: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
// WebSocket client
|
||||
let wsClient = $state(createWebSocketClient());
|
||||
// WebSocket connection
|
||||
let ws = $state(createWebSocketClient());
|
||||
|
||||
// Game state
|
||||
let isCurrentPlayer = $state(true);
|
||||
let currentPlayerId = $state(1); // ID of the current player
|
||||
// let audioPlayer = $state<HTMLAudioElement | null>(null);
|
||||
let isPlaying = $state(false);
|
||||
let audioProgress = $state(0);
|
||||
// let audioDuration = $state(0);
|
||||
// Game $state
|
||||
let currentPlayerId = $state<number>(1);
|
||||
let cards = $state<SongCard[]>([]);
|
||||
let placedCards = $state<SongCard[]>([]);
|
||||
let currentCard = $state<SongCard | null>(null);
|
||||
let dragPosition = $state<{ x: number; y: number } | null>(null);
|
||||
let isDragging = $state<boolean>(false);
|
||||
let dragTargetIndex = $state<number>(-1);
|
||||
|
||||
// Card state
|
||||
const cards = $state<Card[]>([
|
||||
{ id: 1, artist: "Queen", songTitle: "Bohemian Rhapsody", year: 1975, isFlipped: false, position: 0 },
|
||||
{ id: 2, artist: "Michael Jackson", songTitle: "Thriller", year: 1982, isFlipped: false, position: 1 },
|
||||
{ id: 3, artist: "The Beatles", songTitle: "Hey Jude", year: 1968, isFlipped: false, position: 2 },
|
||||
{ id: 4, artist: "Nirvana", songTitle: "Smells Like Teen Spirit", year: 1991, isFlipped: false, position: 3 },
|
||||
{ id: 5, artist: "Led Zeppelin", songTitle: "Stairway to Heaven", year: 1971, isFlipped: false, position: 4 }
|
||||
]);
|
||||
// Music player $state
|
||||
let audioProgress = $state(0);
|
||||
let audioDuration = $state(240); // 4 minutes in seconds
|
||||
let isPlaying = $state(false);
|
||||
|
||||
const middleCard = $state<Card>({
|
||||
id: 0,
|
||||
artist: "Unknown Artist",
|
||||
songTitle: "Mystery Song",
|
||||
year: 2000,
|
||||
isFlipped: true,
|
||||
position: -1
|
||||
});
|
||||
// Mock player data
|
||||
const players = $state<Player[]>([
|
||||
{ id: 1, name: 'YourName', isHost: true },
|
||||
{ id: 2, name: 'Player2', isHost: false },
|
||||
{ id: 3, name: 'GamerX', isHost: false }
|
||||
]);
|
||||
|
||||
// Current player order
|
||||
let players = $derived(wsClient.players);
|
||||
// Mock cards data
|
||||
const mockCards = [
|
||||
{ id: 1, artist: 'Queen', title: 'Bohemian Rhapsody', year: 1975 },
|
||||
{ id: 2, artist: 'Michael Jackson', title: 'Thriller', year: 1982 },
|
||||
{ id: 3, artist: 'The Beatles', title: 'Hey Jude', year: 1968 },
|
||||
{ id: 4, artist: 'Madonna', title: 'Like a Prayer', year: 1989 }
|
||||
];
|
||||
|
||||
let currentPlayerIndex = $derived(players.findIndex(p => p.id === currentPlayerId));
|
||||
let currentPlayer = $derived(currentPlayerIndex >= 0 ? players[currentPlayerIndex] : null);
|
||||
let nextPlayer = $derived(currentPlayerIndex >= 0 && currentPlayerIndex < players.length - 1
|
||||
? players[currentPlayerIndex + 1]
|
||||
: players[0]);
|
||||
let previousPlayer = $derived(currentPlayerIndex > 0
|
||||
? players[currentPlayerIndex - 1]
|
||||
: players[players.length - 1]);
|
||||
// Set up player information
|
||||
function getPlayerInfo() {
|
||||
const playerIndex = players.findIndex((p) => p.id === currentPlayerId);
|
||||
const info = {
|
||||
previous: playerIndex > 0 ? players[playerIndex - 1] : null,
|
||||
current: players[playerIndex],
|
||||
next: playerIndex < players.length - 1 ? players[playerIndex + 1] : null
|
||||
};
|
||||
return info;
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (/* !audioPlayer || */ !isCurrentPlayer) return;
|
||||
// Audio player functions
|
||||
function togglePlayPause() {
|
||||
isPlaying = !isPlaying;
|
||||
|
||||
// if (isPlaying) {
|
||||
// audioPlayer.pause();
|
||||
// } else {
|
||||
// audioPlayer.play();
|
||||
// }
|
||||
}
|
||||
// In a real app, this would control actual audio playback
|
||||
if (isPlaying) {
|
||||
startProgressTimer();
|
||||
} else {
|
||||
stopProgressTimer();
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(e: Event) {
|
||||
if (/* !audioPlayer || */ !isCurrentPlayer) return;
|
||||
function updateProgress(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
audioProgress = parseInt(target.value);
|
||||
}
|
||||
|
||||
// const target = e.target as HTMLInputElement;
|
||||
// const newTime = (parseInt(target.value) / 100) * audioDuration;
|
||||
// audioPlayer.currentTime = newTime;
|
||||
}
|
||||
let progressTimer: number | null = null;
|
||||
|
||||
function onAudioTimeUpdate() {
|
||||
// if (!audioPlayer) return;
|
||||
// audioProgress = (audioPlayer.currentTime / audioDuration) * 100;
|
||||
}
|
||||
function startProgressTimer() {
|
||||
if (progressTimer) return;
|
||||
|
||||
function onAudioLoaded() {
|
||||
// if (!audioPlayer) return;
|
||||
// audioDuration = audioPlayer.duration;
|
||||
}
|
||||
progressTimer = window.setInterval(() => {
|
||||
if (audioProgress < audioDuration) {
|
||||
audioProgress += 1;
|
||||
} else {
|
||||
audioProgress = 0;
|
||||
isPlaying = false;
|
||||
stopProgressTimer();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function onAudioPlayStateChange() {
|
||||
// if (!audioPlayer) return;
|
||||
// isPlaying = !audioPlayer.paused;
|
||||
}
|
||||
function stopProgressTimer() {
|
||||
if (progressTimer) {
|
||||
clearInterval(progressTimer);
|
||||
progressTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startDrag(e: MouseEvent) {
|
||||
if (!isCurrentPlayer) return;
|
||||
// Format time as MM:SS
|
||||
function formatTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// In a real implementation, this would handle the card dragging logic
|
||||
// and synchronize positions with other players via WebSockets
|
||||
console.log("Started dragging card");
|
||||
}
|
||||
// Card dragging functionality
|
||||
function startDrag(event: MouseEvent) {
|
||||
// Only allow dragging for the current player
|
||||
if (currentPlayerId !== players[0].id) return;
|
||||
|
||||
function placeCard(position: number) {
|
||||
if (!isCurrentPlayer) return;
|
||||
const card = document.getElementById('current-card');
|
||||
if (!card) return;
|
||||
|
||||
// In a real implementation, this would place the card at the specified position
|
||||
// and synchronize with other players via WebSockets
|
||||
console.log(`Placing card at position: ${position}`);
|
||||
// Prevent text selection while dragging
|
||||
event.preventDefault();
|
||||
|
||||
wsClient.sendMessage({
|
||||
type: 'cardPlaced',
|
||||
position: position
|
||||
});
|
||||
}
|
||||
isDragging = true;
|
||||
|
||||
onMount(() => {
|
||||
// Setup audio player
|
||||
// audioPlayer = new Audio('/music/sample-song.mp3');
|
||||
//
|
||||
// if (audioPlayer) {
|
||||
// audioPlayer.addEventListener('timeupdate', onAudioTimeUpdate);
|
||||
// audioPlayer.addEventListener('loadedmetadata', onAudioLoaded);
|
||||
// audioPlayer.addEventListener('play', onAudioPlayStateChange);
|
||||
// audioPlayer.addEventListener('pause', onAudioPlayStateChange);
|
||||
// }
|
||||
const rect = card.getBoundingClientRect();
|
||||
dragPosition = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
|
||||
// Connect to WebSocket
|
||||
// wsClient.connect('ws://example.com/game');
|
||||
// Add a class to prevent text selection while dragging
|
||||
document.body.classList.add('no-select');
|
||||
}
|
||||
|
||||
// For demo purposes, simulate a connection with players
|
||||
setTimeout(() => {
|
||||
wsClient.connected = true;
|
||||
wsClient.players = [
|
||||
{ id: 1, name: 'YourName', isHost: true },
|
||||
{ id: 2, name: 'Player2', isHost: false },
|
||||
{ id: 3, name: 'GamerX', isHost: false },
|
||||
{ id: 4, name: 'MusicFan', isHost: false }
|
||||
];
|
||||
function onDrag(event: MouseEvent) {
|
||||
if (!isDragging || !dragPosition) return;
|
||||
|
||||
// Set current player
|
||||
isCurrentPlayer = players[0].id === 1;
|
||||
}, 500);
|
||||
const card = document.getElementById('current-card');
|
||||
if (!card) return;
|
||||
|
||||
return () => {
|
||||
// Cleanup
|
||||
// if (audioPlayer) {
|
||||
// audioPlayer.removeEventListener('timeupdate', onAudioTimeUpdate);
|
||||
// audioPlayer.removeEventListener('loadedmetadata', onAudioLoaded);
|
||||
// audioPlayer.removeEventListener('play', onAudioPlayStateChange);
|
||||
// audioPlayer.removeEventListener('pause', onAudioPlayStateChange);
|
||||
// audioPlayer.pause();
|
||||
// }
|
||||
};
|
||||
});
|
||||
const x = event.clientX - dragPosition.x;
|
||||
const y = event.clientY - dragPosition.y;
|
||||
|
||||
card.style.position = 'fixed';
|
||||
card.style.left = `${x}px`;
|
||||
card.style.top = `${y}px`;
|
||||
card.style.zIndex = '1000';
|
||||
card.style.transform = 'none'; // Clear any transform
|
||||
|
||||
// Find the closest card slot
|
||||
const cardSlots = document.querySelectorAll('.card-slot');
|
||||
let closestDistance = Infinity;
|
||||
let closestIndex = -1;
|
||||
|
||||
cardSlots.forEach((slot, index) => {
|
||||
const rect = slot.getBoundingClientRect();
|
||||
const slotCenterX = rect.left + rect.width / 2;
|
||||
const slotCenterY = rect.top + rect.height / 2;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(event.clientX - slotCenterX, 2) + Math.pow(event.clientY - slotCenterY, 2)
|
||||
);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = index;
|
||||
}
|
||||
});
|
||||
|
||||
// Only highlight if we're close enough
|
||||
if (closestDistance < 100) {
|
||||
dragTargetIndex = closestIndex;
|
||||
} else {
|
||||
dragTargetIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (!isDragging) return;
|
||||
|
||||
isDragging = false;
|
||||
document.body.classList.remove('no-select');
|
||||
|
||||
const card = document.getElementById('current-card');
|
||||
if (!card) return;
|
||||
|
||||
// If we have a valid drop target and a current card
|
||||
if (dragTargetIndex >= 0 && currentCard) {
|
||||
// Place the card in the timeline
|
||||
const newCards = [...placedCards];
|
||||
newCards.splice(dragTargetIndex, 0, currentCard);
|
||||
placedCards = newCards;
|
||||
|
||||
// Remove the current card
|
||||
currentCard = null;
|
||||
|
||||
// Send update to other players via WebSocket
|
||||
ws.sendMessage({
|
||||
type: 'cardPlaced',
|
||||
cardId: currentCard!.id,
|
||||
position: dragTargetIndex
|
||||
});
|
||||
|
||||
// Reset the card position and style
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
} else {
|
||||
// Snap back to original position with animation
|
||||
card.style.transition = 'all 0.3s ease';
|
||||
card.style.position = '';
|
||||
card.style.left = '';
|
||||
card.style.top = '';
|
||||
card.style.zIndex = '';
|
||||
|
||||
setTimeout(() => {
|
||||
if (card) card.style.transition = '';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Reset drag $state
|
||||
dragPosition = null;
|
||||
dragTargetIndex = -1;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Set up initial game $state
|
||||
placedCards = [mockCards[0]];
|
||||
cards = mockCards.slice(1);
|
||||
currentCard = mockCards[1];
|
||||
|
||||
// Set up event listeners for drag and drop
|
||||
window.addEventListener('mousemove', onDrag);
|
||||
window.addEventListener('mouseup', stopDrag);
|
||||
|
||||
// Connect to WebSocket in a real app
|
||||
// ws.connect('ws://example.com/game');
|
||||
|
||||
// Mock WebSocket connection
|
||||
// setTimeout(() => {
|
||||
// ws.connected = true;
|
||||
// ws.players = players;
|
||||
// }, 500);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Clean up event listeners
|
||||
// window.removeEventListener('mousemove', onDrag);
|
||||
// window.removeEventListener('mouseup', stopDrag);
|
||||
|
||||
// Stop the progress timer
|
||||
// stopProgressTimer();
|
||||
|
||||
// Clean up WebSocket connection
|
||||
ws.disconnect();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-white py-6 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- Header with player info -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-4">Music Guessing Game</h1>
|
||||
<div class="min-h-screen bg-white flex flex-col">
|
||||
<!-- Header with player info -->
|
||||
<div class="bg-indigo-700 text-white px-6 py-3">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center">
|
||||
<div class="flex items-center space-x-3">
|
||||
{#if getPlayerInfo().previous}
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-xs text-indigo-200">Previous</span>
|
||||
<span class="text-sm font-medium">PREVIOUS PLACE {getPlayerInfo().previous.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-6">
|
||||
<!-- Previous Player -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="bg-gray-100 h-10 w-10 rounded-full flex items-center justify-center text-gray-600 font-medium text-sm">
|
||||
{previousPlayer?.name.substring(0, 2).toUpperCase() || '??'}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 mt-1">Previous</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center bg-indigo-600 px-4 py-1 rounded-lg">
|
||||
<span class="text-xs text-indigo-200">Current</span>
|
||||
<span class="text-lg font-semibold">{getPlayerInfo().current?.name}</span>
|
||||
</div>
|
||||
|
||||
<!-- Current Player -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="bg-gradient-to-br from-indigo-500 to-purple-600 h-12 w-12 rounded-full flex items-center justify-center text-white font-medium text-sm ring-2 ring-offset-2 ring-indigo-500">
|
||||
{currentPlayer?.name.substring(0, 2).toUpperCase() || '??'}
|
||||
</div>
|
||||
<span class="text-sm font-medium text-indigo-700 mt-1">{currentPlayer?.name || 'Current'}</span>
|
||||
</div>
|
||||
{#if getPlayerInfo().next}
|
||||
<div class="flex flex-col items-center">
|
||||
<span class="text-xs text-indigo-200">Next</span>
|
||||
<span class="text-sm font-medium">{getPlayerInfo().next.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Next Player -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="bg-gray-100 h-10 w-10 rounded-full flex items-center justify-center text-gray-600 font-medium text-sm">
|
||||
{nextPlayer?.name.substring(0, 2).toUpperCase() || '??'}
|
||||
</div>
|
||||
<span class="text-xs text-gray-500 mt-1">Next</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm">{players.length} Players</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isCurrentPlayer}
|
||||
<div class="bg-indigo-100 text-indigo-800 px-4 py-2 rounded-full font-medium">
|
||||
Your turn!
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-gray-500">
|
||||
Waiting for {currentPlayer?.name || 'current player'} to make a move...
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 max-w-7xl mx-auto w-full p-6 flex flex-col">
|
||||
<!-- Music player -->
|
||||
<div class="bg-gray-50 rounded-lg shadow-sm p-4 mb-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
class="p-2 bg-indigo-600 text-white rounded-full hover:bg-indigo-700"
|
||||
on:click={togglePlayPause}
|
||||
>
|
||||
{#if isPlaying}
|
||||
<!-- Pause icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 9v6m4-6v6m-9-9h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Play icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Audio Player -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border border-gray-100 mb-8">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="w-10 h-10 rounded-full flex items-center justify-center {isCurrentPlayer ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-gray-200 text-gray-500 cursor-not-allowed'}"
|
||||
on:click={togglePlay}
|
||||
disabled={!isCurrentPlayer}
|
||||
>
|
||||
{#if isPlaying}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={audioDuration}
|
||||
value={audioProgress}
|
||||
on:input={updateProgress}
|
||||
class="w-full h-2 rounded-lg appearance-none bg-gray-200 cursor-pointer"
|
||||
/>
|
||||
<div class="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>{formatTime(audioProgress)}</span>
|
||||
<span>{formatTime(audioDuration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 ml-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={audioProgress}
|
||||
on:input={updateProgress}
|
||||
class="{isCurrentPlayer ? 'cursor-pointer' : 'cursor-not-allowed'} w-full h-2 bg-gray-200 rounded-lg appearance-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-indigo-600 [&::-webkit-slider-thumb]:rounded-full"
|
||||
disabled={!isCurrentPlayer}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Game area -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<!-- Card placement area - using horizontal scrolling for many cards -->
|
||||
<div class="relative py-10 mb-12">
|
||||
<div class="overflow-x-auto pb-4">
|
||||
<div class="flex space-x-6 min-w-full px-6">
|
||||
{#each placedCards as card, index}
|
||||
<!-- Adding card-slot class for drop target detection -->
|
||||
<div
|
||||
class="card-slot relative flex-shrink-0 w-48 h-64 rounded-lg overflow-hidden shadow-md border border-gray-200 transition-all {dragTargetIndex ===
|
||||
index
|
||||
? 'highlight'
|
||||
: ''}"
|
||||
>
|
||||
<div class="card absolute inset-0 flex flex-col p-4 bg-white">
|
||||
<div class="text-center text-gray-600 text-sm mb-1">Artist</div>
|
||||
<div class="text-center font-semibold mb-2">{card.artist}</div>
|
||||
|
||||
<!-- Main Game Area -->
|
||||
<div class="flex flex-col items-center mb-8">
|
||||
<!-- Card placement area -->
|
||||
<div class="relative w-full overflow-x-auto py-10">
|
||||
<div class="flex justify-center items-center space-x-6 min-w-max px-4">
|
||||
<!-- Cards to place -->
|
||||
{#each cards as card (card.id)}
|
||||
<div class="bg-white shadow-md rounded-lg w-32 h-44 border border-gray-200 p-3 flex flex-col items-center justify-between">
|
||||
<div class="text-sm text-gray-800 font-medium">{card.artist}</div>
|
||||
<div class="text-xl font-bold text-center text-indigo-700">{card.year}</div>
|
||||
<div class="text-sm text-gray-800 text-center">{card.songTitle}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="text-3xl font-bold text-indigo-600">{card.year}</div>
|
||||
</div>
|
||||
|
||||
<!-- Draggable card in the middle -->
|
||||
<div
|
||||
class="relative cursor-move bg-white shadow-lg rounded-lg w-40 h-52 border-2 border-indigo-300 {isCurrentPlayer ? '' : 'cursor-not-allowed'}"
|
||||
on:mousedown={isCurrentPlayer ? startDrag : undefined}
|
||||
>
|
||||
{#if middleCard.isFlipped}
|
||||
<!-- Flipped card shows a placeholder -->
|
||||
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-indigo-50 to-purple-50 rounded-lg">
|
||||
<img
|
||||
src="/images/card-placeholder.jpg"
|
||||
alt="Music card"
|
||||
class="w-24 h-24 opacity-30"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Unflipped card shows song info -->
|
||||
<div class="w-full h-full p-4 flex flex-col items-center justify-between">
|
||||
<div class="text-sm text-gray-800 font-medium">{middleCard.artist}</div>
|
||||
<div class="text-2xl font-bold text-center text-indigo-700">{middleCard.year}</div>
|
||||
<div class="text-sm text-gray-800 text-center">{middleCard.songTitle}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-center text-gray-600 text-sm mb-1">Title</div>
|
||||
<div class="text-center font-semibold">{card.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- Positioning indicators -->
|
||||
<div class="flex justify-center mt-8 space-x-4">
|
||||
{#each Array(cards.length + 1) as _, i}
|
||||
<button
|
||||
class="w-8 h-8 rounded-full flex items-center justify-center {isCurrentPlayer ? 'bg-indigo-100 hover:bg-indigo-200 text-indigo-800' : 'bg-gray-100 text-gray-400 cursor-not-allowed'}"
|
||||
on:click={() => placeCard(i)}
|
||||
disabled={!isCurrentPlayer}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add an extra slot at the end -->
|
||||
<div
|
||||
class="card-slot relative flex-shrink-0 w-48 h-64 border-2 border-dashed border-gray-200 rounded-lg {dragTargetIndex ===
|
||||
placedCards.length
|
||||
? 'highlight'
|
||||
: ''}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-gray-500">
|
||||
{#if isCurrentPlayer}
|
||||
Drag the mystery card and place it where you think it belongs in the timeline!
|
||||
{:else}
|
||||
Watch as {currentPlayer?.name || 'the current player'} makes their guess!
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Current card to place - will be draggable -->
|
||||
{#if currentCard}
|
||||
<div class="flex justify-center mb-8">
|
||||
<div
|
||||
id="current-card"
|
||||
class="card relative w-48 h-64 rounded-lg overflow-hidden shadow-lg border border-indigo-400 cursor-move {currentPlayerId !==
|
||||
players[0].id
|
||||
? 'pointer-events-none opacity-70'
|
||||
: ''}"
|
||||
on:mousedown={startDrag}
|
||||
>
|
||||
<div class="absolute inset-0 flex flex-col p-4 bg-white">
|
||||
<div class="text-center text-gray-600 text-sm mb-1">Artist</div>
|
||||
<div class="text-center font-semibold mb-2">{currentCard.artist}</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center">
|
||||
<div class="text-3xl font-bold text-indigo-600">{currentCard.year}</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-gray-600 text-sm mb-1">Title</div>
|
||||
<div class="text-center font-semibold">{currentCard.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Placeholder when no card is available -->
|
||||
<div class="flex justify-center mb-8">
|
||||
<div
|
||||
class="relative w-48 h-64 rounded-lg overflow-hidden shadow border border-gray-200 bg-gray-50 flex items-center justify-center"
|
||||
>
|
||||
<span class="text-gray-400">Waiting for next card...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Prevent text selection during drag */
|
||||
:global(.no-select) {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* Card styles */
|
||||
.card {
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Card placement slot highlight */
|
||||
.card-slot.highlight {
|
||||
border-color: indigo-500;
|
||||
background-color: indigo-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user