Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="room-selector" ref="selectorRef"> | |
| <div class="dropdown-container" @click="toggleDropdown" :class="{ disabled }"> | |
| <div class="selected-room"> | |
| <span v-if="currentRoom" class="room-display"> | |
| {{ currentRoom }} | |
| </span> | |
| <span v-else class="placeholder">Select room...</span> | |
| </div> | |
| <span class="dropdown-arrow">{{ isOpen ? '▲' : '▼' }}</span> | |
| </div> | |
| <div v-if="isOpen" class="dropdown-menu"> | |
| <!-- Create New Room Option --> | |
| <div class="dropdown-item create-room" @click="handleCreate"> | |
| <span class="item-icon">➕</span> | |
| <span class="item-text">Create New Room</span> | |
| </div> | |
| <div class="dropdown-divider" v-if="rooms.length > 0"></div> | |
| <!-- Room List --> | |
| <div class="room-list"> | |
| <div v-if="loading" class="loading-state"> | |
| Loading rooms... | |
| </div> | |
| <div v-else-if="rooms.length === 0" class="empty-state"> | |
| No active rooms | |
| </div> | |
| <div | |
| v-else | |
| v-for="room in sortedRooms" | |
| :key="room.id" | |
| class="dropdown-item room-item" | |
| :class="{ | |
| 'is-current': room.id === currentRoom, | |
| 'is-full': room.isFull | |
| }" | |
| @click="handleSelect(room)" | |
| > | |
| <div class="room-info"> | |
| <span class="room-name">{{ room.name || room.id }}</span> | |
| <span class="room-id">{{ room.id }}</span> | |
| </div> | |
| <span class="player-count" :class="playerCountClass(room)"> | |
| {{ room.playerCount }}/2 | |
| </span> | |
| <span class="room-status-badge" :class="room.status"> | |
| {{ formatStatus(room) }} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, computed, onMounted, onUnmounted } from "vue"; | |
| export interface RoomSummary { | |
| id: string; | |
| name: string; | |
| playerCount: number; | |
| maxPlayers: number; | |
| status: "waiting" | "playing" | "finished"; | |
| isFull: boolean; | |
| createdAt: string; | |
| } | |
| const props = defineProps<{ | |
| currentRoom: string | null; | |
| rooms: RoomSummary[]; | |
| loading?: boolean; | |
| disabled?: boolean; | |
| }>(); | |
| const emit = defineEmits<{ | |
| create: []; | |
| select: [roomId: string]; | |
| }>(); | |
| const isOpen = ref(false); | |
| const selectorRef = ref<HTMLElement | null>(null); | |
| const sortedRooms = computed(() => { | |
| // Sort: current room first, then by player count (waiting rooms first), then by creation time | |
| return [...props.rooms].sort((a, b) => { | |
| // Current room always first | |
| if (a.id === props.currentRoom) return -1; | |
| if (b.id === props.currentRoom) return 1; | |
| // Waiting rooms before playing | |
| if (a.status === "waiting" && b.status !== "waiting") return -1; | |
| if (a.status !== "waiting" && b.status === "waiting") return 1; | |
| // By player count (less players first - more likely to join) | |
| if (a.playerCount !== b.playerCount) return a.playerCount - b.playerCount; | |
| // By creation time (newer first) | |
| return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); | |
| }); | |
| }); | |
| const toggleDropdown = () => { | |
| if (props.disabled) return; | |
| isOpen.value = !isOpen.value; | |
| }; | |
| const handleCreate = () => { | |
| isOpen.value = false; | |
| emit("create"); | |
| }; | |
| const handleSelect = (room: RoomSummary) => { | |
| if (room.isFull && room.id !== props.currentRoom) return; | |
| isOpen.value = false; | |
| emit("select", room.id); | |
| }; | |
| const playerCountClass = (room: RoomSummary) => { | |
| if (room.playerCount === 0) return "empty"; | |
| if (room.playerCount === 1) return "partial"; | |
| return "full"; | |
| }; | |
| const formatStatus = (room: RoomSummary) => { | |
| if (room.isFull) return "Full"; | |
| if (room.status === "waiting") return "Waiting"; | |
| if (room.status === "playing") return "Playing"; | |
| return room.status; | |
| }; | |
| // Close dropdown when clicking outside | |
| const handleClickOutside = (event: MouseEvent) => { | |
| if (selectorRef.value && !selectorRef.value.contains(event.target as Node)) { | |
| isOpen.value = false; | |
| } | |
| }; | |
| onMounted(() => { | |
| document.addEventListener("click", handleClickOutside); | |
| }); | |
| onUnmounted(() => { | |
| document.removeEventListener("click", handleClickOutside); | |
| }); | |
| </script> | |
| <style scoped lang="scss"> | |
| .room-selector { | |
| position: relative; | |
| display: inline-block; | |
| } | |
| .dropdown-container { | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid rgba(255, 255, 255, 0.3); | |
| border-radius: 4px; | |
| padding: 0.4rem 0.6rem; | |
| min-width: 140px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 0.5rem; | |
| transition: background 0.2s; | |
| &:hover:not(.disabled) { | |
| background: rgba(0, 0, 0, 0.4); | |
| } | |
| &.disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| } | |
| .selected-room { | |
| font-size: 0.9rem; | |
| color: #fff; | |
| } | |
| .placeholder { | |
| color: rgba(255, 255, 255, 0.5); | |
| } | |
| .dropdown-arrow { | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| .dropdown-menu { | |
| position: absolute; | |
| top: calc(100% + 4px); | |
| left: 0; | |
| min-width: 220px; | |
| background: #3a3a3a; | |
| border: 1px solid #505050; | |
| border-radius: 4px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
| z-index: 100; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .dropdown-item { | |
| padding: 0.6rem 0.75rem; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: background 0.15s; | |
| &:hover:not(.is-full) { | |
| background: #4a4a4a; | |
| } | |
| &.is-full:not(.is-current) { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| &.is-current { | |
| background: #505050; | |
| border-left: 3px solid #e94560; | |
| padding-left: calc(0.75rem - 3px); | |
| } | |
| } | |
| .create-room { | |
| color: #4ade80; | |
| border-bottom: 1px solid #505050; | |
| .item-icon { | |
| font-size: 0.9rem; | |
| } | |
| .item-text { | |
| font-size: 0.9rem; | |
| } | |
| } | |
| .dropdown-divider { | |
| height: 1px; | |
| background: #505050; | |
| margin: 0; | |
| } | |
| .room-list { | |
| max-height: 240px; | |
| overflow-y: auto; | |
| } | |
| .loading-state, | |
| .empty-state { | |
| padding: 1rem; | |
| text-align: center; | |
| color: rgba(255, 255, 255, 0.5); | |
| font-size: 0.85rem; | |
| } | |
| .room-item { | |
| .room-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.15rem; | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .room-name { | |
| font-size: 0.85rem; | |
| color: #fff; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .room-id { | |
| font-family: monospace; | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.5); | |
| } | |
| .player-count { | |
| font-family: monospace; | |
| font-size: 0.8rem; | |
| padding: 0.1rem 0.3rem; | |
| border-radius: 3px; | |
| &.empty { | |
| color: #4ade80; | |
| } | |
| &.partial { | |
| color: #fbbf24; | |
| } | |
| &.full { | |
| color: #ef4444; | |
| } | |
| } | |
| .room-status-badge { | |
| font-size: 0.75rem; | |
| padding: 0.15rem 0.4rem; | |
| border-radius: 3px; | |
| min-width: 50px; | |
| text-align: center; | |
| &.waiting { | |
| background: rgba(74, 222, 128, 0.15); | |
| color: #4ade80; | |
| } | |
| &.playing { | |
| background: rgba(251, 191, 36, 0.15); | |
| color: #fbbf24; | |
| } | |
| } | |
| } | |
| </style> | |