Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified frontend/src/assets/home_page_as_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/src/assets/home_page_as_image_no_text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 16 additions & 9 deletions frontend/src/components/ScreenBackground.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
<!--
Usage:
<ScreenBackground blur />

Import this component in your Vue file:
import ScreenBackground from '@/components/ScreenBackground.vue';
-->
<template>
<div :class="['bg-container', { blur }]">
<div v-if="blur" class="overlay"></div>
<img :src="blur ? imgBlur : imgNormal" />
<div :class="['bg-container', { type }]">
<div v-if="type" class="overlay"></div>

<!-- if text prop is true, use imgNormal, if noTree prop is true, use imgNoTree, otherwise use imgBlur -->
<img v-else-if="text" :src="imgNormal" />
<img v-else-if="noTree" :src="imgNoTree" />
<img v-else :src="imgBlur" />
</div>
</template>

<script setup lang="ts">
import { defineProps } from 'vue';
import imgNormal from '@/assets/home_page_as_image.png';
import imgBlur from '@/assets/home_page_as_image_no_text.png';
defineProps<{ blur?: boolean }>();
import imgNoTree from '@/assets/home_page_as_image_no_tree.png';

defineProps<{
type?: string;
blur?: boolean;
noTree?: boolean;
text?: boolean;
}>();

</script>

<style scoped>
Expand All @@ -33,9 +42,7 @@ defineProps<{ blur?: boolean }>();
.bg-container .overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 8, 26, 0.8);
z-index: -1;
backdrop-filter: blur(10px);
}

.bg-container img {
Expand Down
27 changes: 25 additions & 2 deletions frontend/src/types/APIManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ class APIManager {

/**
* Attempt to reconnect using saved session info.
* Returns true if reconnection is successful.
* Returns true if reconnection is successful (or if offline session created for ended game).
*/
public reconnectSession(role: "host" | "player"): Promise<boolean> {
this.startLoading();
Expand All @@ -222,7 +222,30 @@ class APIManager {
return resolve(false);
}

// Check if game is over - don't attempt reconnection
// For hosts, check if game has ended - create offline session instead of reconnecting
if (role === "host") {
const hostSessionData = localStorage.getItem('host-session-data');
if (hostSessionData) {
try {
const data = JSON.parse(hostSessionData);
if (data.gameEnded === true) {
// Game ended - create offline session (no WebSocket connection needed)
const offlineWs = {
send: () => { /* offline mode - no messages to send */ },
close: () => { /* offline mode - no connection to close */ },
readyState: WebSocket.CLOSED
} as unknown as WebSocket;
this.session = new HostSession(offlineWs, info.lobbyCode, info.hostToken);
this.stopLoading();
return resolve(true);
}
} catch (e) {
// If parsing fails, continue with normal reconnection
}
}
}

// For players, check if game is over - don't attempt reconnection
if (info.gameOver) {
this.stopLoading();
console.log("Game is over, skipping reconnection");
Expand Down
103 changes: 70 additions & 33 deletions frontend/src/types/useHostSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export function useHostSession() {
* Sets up event listeners, watchers, and restores any persisted session state.
*/
onMounted(async () => {

// Load persisted data first to restore previous state
loadData();

// Get the current session from the API manager
const apiManager = APIManager.getInstance();
const session = await apiManager.getSession();
Expand All @@ -121,46 +125,79 @@ export function useHostSession() {
return;
}

// Load persisted data first to restore previous state
loadData();

// Set the lobby code from the active session
lobbyCode.value = session.lobbyCode;

// If we restored players from storage, prime the session cache so future updates merge correctly
if (playersData.value.length) {
const restoredPlayers = playersData.value.map((player) =>
new PlayerAnswers(
player.getId(),
player.getNickname(),
player.getScore(),
[...player.getAnswers()]
)
);
// If game has ended, we rely entirely on localStorage data and skip server requests
if (gameEnded.value) {
// Restore questions to the session object so components can access them
if (questions.value.length > 0) {
session.AllQuestionsData.value = questions.value.map((question) =>
new QuestionData(
question.getId(),
question.getTitle(),
question.getAverageAnswerTime(),
question.getCorrectAnswerCount(),
question.getIncorrectAnswerCount(),
question.getUnansweredCount()
)
);
}

// Restore players to the session object
if (playersData.value.length > 0) {
session.playerQuestions.value = playersData.value.map((player) =>
new PlayerAnswers(
player.getId(),
player.getNickname(),
player.getScore(),
[...player.getAnswers()]
)
);
}

// Still listen for any final updates, but don't request fresh data
session.addEventListener("ANALYTICS_UPDATED", (data) => {
session.updateHostSessionData(data);
saveData();
});
} else {
// Game is still active - set up normal synchronization with server

// If we restored players from storage, prime the session cache so future updates merge correctly
if (playersData.value.length) {
const restoredPlayers = playersData.value.map((player) =>
new PlayerAnswers(
player.getId(),
player.getNickname(),
player.getScore(),
[...player.getAnswers()]
)
);

session.playerQuestions.value = restoredPlayers;
}
session.playerQuestions.value = restoredPlayers;
}

// Listen for analytics updates from the server and persist them
session.addEventListener("ANALYTICS_UPDATED", (data) => {
session.updateHostSessionData(data);
saveData();
});

// Listen for analytics updates from the server and persist them
session.addEventListener("ANALYTICS_UPDATED", (data) => {
session.updateHostSessionData(data);
saveData();
});
// Initialize player data and request current player list
playersData.value = [];
session.getPlayers();

// Initialize player data and request current player list
playersData.value = [];
session.getPlayers();

// Convert session player data to PlayerAnswers objects for local state
playersData.value = session.playerQuestions.value.map((player: any) => {
return new PlayerAnswers(
player.id || player.getId?.() || '',
player.nickname || player.getNickname?.() || '',
player.score || player.getScore?.() || 0,
player.answers || player.getAnswers?.() || []
);
});
// Convert session player data to PlayerAnswers objects for local state
playersData.value = session.playerQuestions.value.map((player: any) => {
return new PlayerAnswers(
player.id || player.getId?.() || '',
player.nickname || player.getNickname?.() || '',
player.score || player.getScore?.() || 0,
player.answers || player.getAnswers?.() || []
);
});
}

// Watch for question data updates from the session and sync local state
watch(
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/HomeView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div class="home">
<ScreenBackground />
<ScreenBackground text />
<div class="button-container">
<CustomButton :action="() => $router.push('/practice')">Solo</CustomButton>
<CustomButton :action="() => joinIsOpen = true">Join</CustomButton>
Expand Down
12 changes: 9 additions & 3 deletions frontend/src/views/HostView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ const totalPlayers = computed(() => playersData.value.length);
color: var(--text-color);
}

@media (max-width: 1055px) {
@media (max-width: 1300px) {

.lobby-right {
max-height:none
Expand Down Expand Up @@ -594,7 +594,7 @@ const totalPlayers = computed(() => playersData.value.length);
.player-name {
overflow: hidden;
text-wrap: nowrap;
width: 250px;
max-width: 250px;
text-align: left;
}

Expand Down Expand Up @@ -659,7 +659,7 @@ const totalPlayers = computed(() => playersData.value.length);

.question-analytics {
display: flex;
min-width: 500px;
width: 700px;
flex-direction: column;
gap: 2.5rem;
overflow-x: hidden;
Expand All @@ -671,6 +671,12 @@ const totalPlayers = computed(() => playersData.value.length);
/* Allow flex item to shrink below content size */
}

@media (max-width: 900px) {
.question-analytics {
width: 100%;
}
}

.question-header {
display: flex;
justify-content: space-between;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/PracticeView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<ScreenBackground blur />
<ScreenBackground noTree />
<ReturnHomeComponent skipConfirm />
<div class="question-view">
<h2 v-if="question">{{ question.title }}</h2>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/QuestionView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<ScreenBackground blur />
<ScreenBackground noTree />
<ReturnHomeComponent
:onConfirm="handleReturnHome"/>
<div class="question-view">
Expand Down