Add specific team chat channels, and update README

This commit is contained in:
Brian McGonagill 2026-03-25 08:32:15 -05:00
parent e2a95252bb
commit 2ab11f7a4b
8 changed files with 351 additions and 97 deletions

View file

@ -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 {

View file

@ -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>

View file

@ -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);
}

View file

@ -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;
}