Rapport-Laravel/resources/views/microsite/session-questions.blade.php

291 lines
12 KiB
PHP

@extends('layouts.base', ['page_title' => 'Questions - ' . ($session->title ?? 'Session')])
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">{{ $session->title ?? 'Session Questions' }}</h2>
<p class="text-muted mb-0">{{ $event->name ?? 'Event' }} — {{ $track->name ?? 'Track' }}</p>
</div>
<a href="{{ route('microsite.session', [$event->id, $track->id, $session->id]) }}" class="btn btn-outline-secondary btn-sm">
Back to session
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Live Questions</h5>
<small class="text-muted">This list updates in realtime as the audience participates.</small>
</div>
<div class="card-body">
<div id="questions-loading" class="text-muted small">Loading questions…</div>
<div id="questions-error" class="alert alert-danger d-none"></div>
<div id="questions-empty" class="text-muted d-none">No questions have been submitted yet.</div>
<div id="questions-list" class="list-group list-group-flush"></div>
</div>
</div>
</div>
@endsection
@section('scripts')
@parent
@if(!empty($firebaseWebConfig))
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-database-compat.js"></script>
@endif
<script>
(function () {
const loading = document.getElementById('questions-loading');
const errorBox = document.getElementById('questions-error');
const emptyState = document.getElementById('questions-empty');
const list = document.getElementById('questions-list');
const firebaseConfig = @json($firebaseWebConfig);
const eventId = @json($event->id);
const sessionId = @json($session->id);
const partition = @json($sessionPartition);
const addClass = (el, cls) => el && el.classList.add(cls);
const removeClass = (el, cls) => el && el.classList.remove(cls);
const setText = (el, value) => el && (el.textContent = value);
const showError = (message) => {
addClass(loading, 'd-none');
removeClass(errorBox, 'd-none');
setText(errorBox, message);
addClass(emptyState, 'd-none');
if (list) {
list.innerHTML = '';
}
};
if (!firebaseConfig) {
showError('Missing Firebase configuration.');
return;
}
if (!partition) {
showError('Missing session partition.');
return;
}
if (typeof window.firebase === 'undefined') {
showError('Firebase SDK failed to load.');
return;
}
const getLikeCount = (item) => {
const fields = ['likes', 'upvotes', 'votes', 'score'];
for (const field of fields) {
if (item[field] !== undefined && item[field] !== null && !isNaN(item[field])) {
return Number(item[field]);
}
}
return 0;
};
const getDislikeCount = (item) => {
const fields = ['dislikes', 'downvotes'];
for (const field of fields) {
if (item[field] !== undefined && item[field] !== null && !isNaN(item[field])) {
return Number(item[field]);
}
}
return 0;
};
const getTimestamp = (item) => {
const candidates = [item.created_at, item.createdAt];
for (const value of candidates) {
if (!value) continue;
if (!isNaN(value)) {
return Number(value);
}
const parsed = Date.parse(value);
if (!isNaN(parsed)) {
return parsed;
}
}
return 0;
};
const sortQuestions = (questions) => {
return questions.sort((a, b) => {
const scoreDiff = getLikeCount(b) - getLikeCount(a);
if (scoreDiff !== 0) {
return scoreDiff;
}
return getTimestamp(b) - getTimestamp(a);
});
};
const firebaseAppName = `microsite-session-questions-${partition}-${eventId}-${sessionId}`;
const getExistingApp = () => {
if (!Array.isArray(window.firebase.apps)) {
return null;
}
return window.firebase.apps.find((app) => app.name === firebaseAppName) ?? null;
};
let firebaseApp;
try {
firebaseApp = getExistingApp() || window.firebase.initializeApp(firebaseConfig, firebaseAppName);
} catch (initError) {
showError('Unable to initialize Firebase: ' + initError.message);
return;
}
const database = window.firebase.database(firebaseApp);
const refPath = `questions/${partition}/${sessionId}`;
window.__questionsDebug = { refPath };
const handleVote = (questionKey, field) => {
const targetRef = database.ref(`${refPath}/${questionKey}/${field}`);
targetRef.transaction((current) => {
if (typeof current !== 'number') {
return 1;
}
return current + 1;
}).catch((voteError) => {
console.error('Vote failed', voteError);
});
};
const renderList = (questions) => {
addClass(loading, 'd-none');
addClass(errorBox, 'd-none');
if (!questions.length) {
removeClass(emptyState, 'd-none');
if (list) {
list.innerHTML = '';
}
return;
}
addClass(emptyState, 'd-none');
if (!list) {
return;
}
list.innerHTML = '';
questions.forEach((item) => {
const row = document.createElement('div');
row.className = 'list-group-item';
const wrapper = document.createElement('div');
wrapper.className = 'd-flex justify-content-between align-items-start gap-3 flex-wrap';
const content = document.createElement('div');
const title = document.createElement('h6');
title.className = 'mb-2';
title.textContent = item.question || 'Question';
content.appendChild(title);
if (item.answer) {
const answer = document.createElement('p');
answer.className = 'text-muted small mb-0';
answer.textContent = item.answer;
content.appendChild(answer);
}
const meta = document.createElement('div');
meta.className = 'd-flex align-items-center gap-2 flex-wrap';
const upvoteBtn = document.createElement('button');
upvoteBtn.className = 'btn btn-sm btn-outline-success';
upvoteBtn.textContent = `▲ ${getLikeCount(item)}`;
upvoteBtn.addEventListener('click', () => handleVote(item.firebase_key, 'upvotes'));
const downvoteBtn = document.createElement('button');
downvoteBtn.className = 'btn btn-sm btn-outline-danger';
downvoteBtn.textContent = `▼ ${getDislikeCount(item)}`;
downvoteBtn.addEventListener('click', () => handleVote(item.firebase_key, 'downvotes'));
meta.appendChild(upvoteBtn);
meta.appendChild(downvoteBtn);
wrapper.appendChild(content);
wrapper.appendChild(meta);
row.appendChild(wrapper);
list.appendChild(row);
});
};
database.ref(refPath).on('value', (snapshot) => {
const value = snapshot.val();
window.__questionsDebug = { refPath, snapshot: value };
if (!value) {
renderList([]);
return;
}
const questions = Object.entries(value).map(([key, data]) => {
if (data && typeof data === 'object') {
return { firebase_key: key, ...data };
}
return { firebase_key: key, question: data };
});
renderList(sortQuestions(questions));
}, (listenerError) => {
showError('Realtime feed error: ' + listenerError.message);
});
})();
</script>
@endsection
@extends('layouts.base', ['page_title' => 'Questions - ' . ($session->title ?? 'Session')])
@section('content')
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-1">{{ $session->title ?? 'Session Questions' }}</h2>
<p class="text-muted mb-0">{{ $event->name ?? 'Event' }} &mdash; {{ $track->name ?? 'Track' }}</p>
</div>
<a href="{{ route('microsite.session', [$event->id, $track->id, $session->id]) }}" class="btn btn-outline-secondary btn-sm">
Back to session
</a>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">All Questions</h5>
<small class="text-muted">Stay updated with every audience submission.</small>
</div>
<div class="card-body">
@if(!empty($questionsError))
<div class="alert alert-danger mb-0">{{ $questionsError }}</div>
@elseif(isset($questions) && $questions->count() > 0)
<div class="list-group list-group-flush">
@foreach($questions as $question)
@php
$likeCount = $question['likes'] ?? $question['upvotes'] ?? $question['votes'] ?? null;
@endphp
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<h6 class="mb-2">{{ $question['question'] ?? 'Question' }}</h6>
@if(!empty($question['answer']))
<p class="text-muted small mb-0">{{ $question['answer'] }}</p>
@endif
</div>
@if(is_numeric($likeCount))
<span class="badge bg-light text-muted">{{ $likeCount }} likes</span>
@endif
</div>
</div>
@endforeach
</div>
@else
<p class="text-muted mb-0">No questions have been submitted yet.</p>
@endif
</div>
</div>
</div>
@endsection