291 lines
12 KiB
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' }} — {{ $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
|
|
|
|
|