trigo / trigo-web /app /src /components /RoomSelector.vue
k-l-lambda's picture
feat: room rename and room switching with confirmation
6f4808d
<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>