Add specific team chat channels, and update README
This commit is contained in:
parent
e2a95252bb
commit
2ab11f7a4b
8 changed files with 351 additions and 97 deletions
|
|
@ -25,6 +25,7 @@ let map: L.Map | null = null;
|
|||
let teamMarkers: { [key: string]: L.Marker } = {};
|
||||
|
||||
const chatMessage = ref('');
|
||||
const chatTarget = ref<'all' | string>('all');
|
||||
const selectedTeam = ref<Team | null>(null);
|
||||
|
||||
async function loadGame() {
|
||||
|
|
@ -134,7 +135,11 @@ function connectSocket() {
|
|||
});
|
||||
|
||||
socket.on('chat-message', (data: ChatMessage) => {
|
||||
chatMessages.value.push(data);
|
||||
if (data.isDirect && data.teamId) {
|
||||
chatMessages.value.push(data);
|
||||
} else if (!data.isDirect) {
|
||||
chatMessages.value.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('team-advanced', () => {
|
||||
|
|
@ -145,8 +150,12 @@ function connectSocket() {
|
|||
async function sendChat() {
|
||||
if (!chatMessage.value.trim() || !socket) return;
|
||||
|
||||
const isDirect = chatTarget.value !== 'all';
|
||||
|
||||
socket.emit('chat-message', {
|
||||
gameId: gameId.value,
|
||||
teamId: isDirect ? chatTarget.value : undefined,
|
||||
isDirect,
|
||||
message: chatMessage.value,
|
||||
userId: authStore.user?.id,
|
||||
userName: authStore.user?.name
|
||||
|
|
@ -271,15 +280,39 @@ onUnmounted(() => {
|
|||
|
||||
<section>
|
||||
<h2>Chat</h2>
|
||||
<div class="chat-target">
|
||||
<label>
|
||||
Send to:
|
||||
<select v-model="chatTarget">
|
||||
<option value="all">All Teams</option>
|
||||
<option v-for="team in teams" :key="team.id" :value="team.id">
|
||||
{{ team.name }} only
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="chat-messages">
|
||||
<article v-for="msg in chatMessages" :key="msg.id" style="margin: 0.5rem 0; padding: 0.5rem;">
|
||||
<strong>{{ msg.userName }}:</strong> {{ msg.message }}
|
||||
<article
|
||||
v-for="msg in chatMessages"
|
||||
:key="msg.id"
|
||||
class="chat-message"
|
||||
:class="{ 'direct-message': msg.isDirect }"
|
||||
style="margin: 0.5rem 0; padding: 0.5rem;"
|
||||
>
|
||||
<div class="message-header">
|
||||
<strong>{{ msg.userName }}</strong>
|
||||
<span v-if="msg.isDirect" class="direct-badge">
|
||||
→ {{ teams.find(t => t.id === msg.teamId)?.name || 'Unknown' }}
|
||||
</span>
|
||||
<span v-else class="broadcast-badge">→ All</span>
|
||||
</div>
|
||||
<div class="message-text">{{ msg.message }}</div>
|
||||
</article>
|
||||
<article v-if="!chatMessages.length" style="text-align: center; color: var(--pico-muted-color);">
|
||||
No messages yet
|
||||
</article>
|
||||
</div>
|
||||
<form @submit.prevent="sendChat" class="grid">
|
||||
<form @submit.prevent="sendChat" class="chat-form">
|
||||
<input v-model="chatMessage" placeholder="Type a message..." />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
|
|
@ -300,8 +333,63 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.chat-messages {
|
||||
height: calc(100% - 60px);
|
||||
height: calc(100% - 120px);
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-target {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-target select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-form input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
border-left: 3px solid var(--pico-primary-background-color);
|
||||
}
|
||||
|
||||
.chat-message.direct-message {
|
||||
border-left-color: var(--pico-warning-background-color);
|
||||
background: var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.broadcast-badge, .direct-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.broadcast-badge {
|
||||
background: var(--pico-primary-background-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.direct-badge {
|
||||
background: var(--pico-warning-background-color);
|
||||
color: var(--pico-warning-color);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.selected {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute, RouterLink } from 'vue-router';
|
||||
import { useRoute, RouterLink, useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import type { Game } from '../types';
|
||||
import { gameService } from '../services/api';
|
||||
|
|
@ -8,6 +8,7 @@ import { alert, confirm } from '../composables/useModal';
|
|||
import { formatRadius } from '../utils/units';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const game = ref<Game | null>(null);
|
||||
|
|
@ -86,6 +87,17 @@ async function copyInviteLink() {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteGame() {
|
||||
if (!await confirm('Are you sure you want to delete this game? This action cannot be undone.', 'Delete Game', true)) return;
|
||||
|
||||
try {
|
||||
await gameService.delete(gameId.value);
|
||||
router.push('/dashboard');
|
||||
} catch (err: any) {
|
||||
await alert(err.response?.data?.error || 'Failed to delete game');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadGame();
|
||||
});
|
||||
|
|
@ -115,6 +127,7 @@ onMounted(() => {
|
|||
<div v-if="isGameMaster" class="grid">
|
||||
<RouterLink v-if="game.status === 'DRAFT'" :to="`/games/${game.id}/edit`" role="button" class="secondary">Edit Game</RouterLink>
|
||||
<button v-if="game.status === 'DRAFT'" @click="publishGame">Publish Game</button>
|
||||
<button v-if="game.status === 'DRAFT'" @click="deleteGame" class="contrast">Delete Game</button>
|
||||
<RouterLink v-if="game.status === 'LIVE'" :to="`/games/${game.id}/live`" role="button">Live Dashboard</RouterLink>
|
||||
<button v-if="game.status === 'LIVE'" @click="endGame" class="contrast">End Game</button>
|
||||
<button v-if="game.status === 'ENDED'" @click="archiveGame" class="secondary">Archive Game</button>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,11 @@ function connectSocket() {
|
|||
});
|
||||
|
||||
socket.on('chat-message', (data: ChatMessage) => {
|
||||
chatMessages.value.push(data);
|
||||
if (!data.isDirect) {
|
||||
chatMessages.value.push(data);
|
||||
} else if (data.teamId === team.value?.id) {
|
||||
chatMessages.value.push(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -274,8 +278,22 @@ onUnmounted(() => {
|
|||
<section>
|
||||
<h3>Chat</h3>
|
||||
<div class="chat-messages">
|
||||
<article v-for="msg in chatMessages" :key="msg.id" style="margin: 0.5rem 0; padding: 0.5rem;">
|
||||
<strong>{{ msg.userName }}:</strong> {{ msg.message }}
|
||||
<article
|
||||
v-for="msg in chatMessages"
|
||||
:key="msg.id"
|
||||
class="chat-message"
|
||||
:class="{ 'direct-message': msg.isDirect }"
|
||||
style="margin: 0.5rem 0; padding: 0.5rem;"
|
||||
>
|
||||
<div class="message-header">
|
||||
<strong>{{ msg.userName }}</strong>
|
||||
<span v-if="msg.isDirect" class="direct-badge">→ To your team</span>
|
||||
<span v-else class="broadcast-badge">→ All teams</span>
|
||||
</div>
|
||||
<div class="message-text">{{ msg.message }}</div>
|
||||
</article>
|
||||
<article v-if="!chatMessages.length" style="text-align: center; color: var(--pico-muted-color);">
|
||||
No messages yet
|
||||
</article>
|
||||
</div>
|
||||
<form @submit.prevent="sendChat" class="grid">
|
||||
|
|
@ -319,6 +337,42 @@ onUnmounted(() => {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
border-left: 3px solid var(--pico-primary-background-color);
|
||||
}
|
||||
|
||||
.chat-message.direct-message {
|
||||
border-left-color: var(--pico-ins-color);
|
||||
background: var(--pico-ins-background-color);
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.broadcast-badge, .direct-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.broadcast-badge {
|
||||
background: var(--pico-primary-background-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.direct-badge {
|
||||
background: var(--pico-ins-background-color);
|
||||
color: var(--pico-ins-color);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--pico-del-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,11 +137,12 @@ export interface PhotoSubmission {
|
|||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
gameId: string;
|
||||
gameId?: string;
|
||||
teamId?: string;
|
||||
userId: string;
|
||||
userName: string;
|
||||
message: string;
|
||||
isDirect?: boolean;
|
||||
sentAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue