Initial commit: InviteMe event registration app
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.checkin-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.qr-reader {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.guest-photo {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
border: 3px solid #dee2e6;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.guest-photo-placeholder {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 12px;
|
||||
color: #6c757d;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#search-results .list-group-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#search-results .list-group-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 15px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
let html5QrCode = null;
|
||||
let currentToken = null;
|
||||
|
||||
const qrReader = document.getElementById("qr-reader");
|
||||
const startScanBtn = document.getElementById("start-scan");
|
||||
const stopScanBtn = document.getElementById("stop-scan");
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const searchResults = document.getElementById("search-results");
|
||||
const guestCard = document.getElementById("guest-card");
|
||||
const guestPhoto = document.getElementById("guest-photo");
|
||||
const guestName = document.getElementById("guest-name");
|
||||
const guestDepartment = document.getElementById("guest-department");
|
||||
const guestPosition = document.getElementById("guest-position");
|
||||
const guestEmail = document.getElementById("guest-email");
|
||||
const checkInStatus = document.getElementById("check-in-status");
|
||||
const checkInBtn = document.getElementById("check-in-btn");
|
||||
const resetBtn = document.getElementById("reset-btn");
|
||||
|
||||
function resetGuestCard() {
|
||||
guestCard.classList.add("d-none");
|
||||
currentToken = null;
|
||||
guestPhoto.src = "";
|
||||
guestName.textContent = "";
|
||||
guestDepartment.textContent = "";
|
||||
guestPosition.textContent = "";
|
||||
guestEmail.textContent = "";
|
||||
checkInStatus.classList.add("d-none");
|
||||
checkInStatus.className = "alert d-none";
|
||||
checkInBtn.disabled = false;
|
||||
checkInBtn.textContent = "Отметить вход";
|
||||
}
|
||||
|
||||
function showGuest(guest) {
|
||||
currentToken = guest.token;
|
||||
guestCard.classList.remove("d-none");
|
||||
|
||||
if (guest.photo_url) {
|
||||
guestPhoto.src = guest.photo_url;
|
||||
guestPhoto.classList.remove("d-none");
|
||||
} else {
|
||||
guestPhoto.src = "";
|
||||
guestPhoto.classList.add("d-none");
|
||||
// Show placeholder
|
||||
if (!guestPhoto.nextElementSibling || !guestPhoto.nextElementSibling.classList.contains("guest-photo-placeholder")) {
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.className = "guest-photo-placeholder mx-auto mb-3";
|
||||
placeholder.textContent = "Нет фото";
|
||||
guestPhoto.parentNode.insertBefore(placeholder, guestPhoto.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
guestName.textContent = guest.full_name;
|
||||
guestDepartment.textContent = guest.department;
|
||||
guestPosition.textContent = guest.position || "";
|
||||
guestEmail.textContent = guest.email;
|
||||
|
||||
if (guest.checked_in) {
|
||||
checkInStatus.classList.remove("d-none", "alert-success", "alert-warning");
|
||||
checkInStatus.classList.add("alert-warning");
|
||||
checkInStatus.textContent = "Гость уже отмечен как вошедший";
|
||||
checkInBtn.disabled = true;
|
||||
} else {
|
||||
checkInStatus.classList.add("d-none");
|
||||
checkInBtn.disabled = false;
|
||||
}
|
||||
|
||||
window.scrollTo({ top: guestCard.offsetTop - 20, behavior: "smooth" });
|
||||
}
|
||||
|
||||
function loadGuestByToken(token) {
|
||||
fetch(`/api/guest/${token}`)
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error("Гость не найден");
|
||||
return response.json();
|
||||
})
|
||||
.then(guest => showGuest(guest))
|
||||
.catch(error => {
|
||||
alert(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// QR Scanner
|
||||
startScanBtn.addEventListener("click", () => {
|
||||
html5QrCode = new Html5Qrcode("qr-reader");
|
||||
Html5Qrcode.getCameras()
|
||||
.then(cameras => {
|
||||
if (cameras && cameras.length) {
|
||||
const cameraId = cameras[0].id;
|
||||
html5QrCode.start(
|
||||
cameraId,
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
(decodedText) => {
|
||||
html5QrCode.stop();
|
||||
stopScanBtn.classList.add("d-none");
|
||||
startScanBtn.classList.remove("d-none");
|
||||
|
||||
// Extract token from URL
|
||||
const url = new URL(decodedText);
|
||||
const token = url.searchParams.get("token");
|
||||
if (token) {
|
||||
loadGuestByToken(token);
|
||||
}
|
||||
},
|
||||
(errorMessage) => {}
|
||||
);
|
||||
startScanBtn.classList.add("d-none");
|
||||
stopScanBtn.classList.remove("d-none");
|
||||
} else {
|
||||
alert("Камеры не найдены");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert("Ошибка доступа к камере: " + err.message);
|
||||
});
|
||||
});
|
||||
|
||||
stopScanBtn.addEventListener("click", () => {
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop();
|
||||
}
|
||||
stopScanBtn.classList.add("d-none");
|
||||
startScanBtn.classList.remove("d-none");
|
||||
});
|
||||
|
||||
// Search by name
|
||||
let searchTimeout = null;
|
||||
searchInput.addEventListener("input", (e) => {
|
||||
const query = e.target.value.trim();
|
||||
searchResults.innerHTML = "";
|
||||
|
||||
if (query.length < 2) return;
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
fetch(`/api/guests/search?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(guests => {
|
||||
searchResults.innerHTML = "";
|
||||
guests.forEach(guest => {
|
||||
const item = document.createElement("button");
|
||||
item.className = "list-group-item list-group-item-action";
|
||||
item.innerHTML = `<strong>${guest.full_name}</strong><br><small>${guest.department}</small>`;
|
||||
item.addEventListener("click", () => {
|
||||
searchResults.innerHTML = "";
|
||||
searchInput.value = "";
|
||||
showGuest(guest);
|
||||
});
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Check-in button
|
||||
checkInBtn.addEventListener("click", () => {
|
||||
if (!currentToken) return;
|
||||
|
||||
checkInBtn.disabled = true;
|
||||
checkInBtn.textContent = "Отметка...";
|
||||
|
||||
fetch(`/api/check-in/${currentToken}`, { method: "POST" })
|
||||
.then(response => response.json().then(data => ({ status: response.status, data })))
|
||||
.then(({ status, data }) => {
|
||||
checkInStatus.classList.remove("d-none", "alert-success", "alert-warning", "alert-danger");
|
||||
if (data.success) {
|
||||
checkInStatus.classList.add("alert-success");
|
||||
checkInStatus.textContent = "Вход успешно отмечен!";
|
||||
checkInBtn.textContent = "Вход отмечен";
|
||||
} else if (status === 409) {
|
||||
checkInStatus.classList.add("alert-warning");
|
||||
checkInStatus.textContent = "Гость уже отмечен как вошедший";
|
||||
checkInBtn.textContent = "Уже отмечен";
|
||||
} else {
|
||||
checkInStatus.classList.add("alert-danger");
|
||||
checkInStatus.textContent = data.message || "Ошибка";
|
||||
checkInBtn.disabled = false;
|
||||
checkInBtn.textContent = "Отметить вход";
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
checkInStatus.classList.remove("d-none", "alert-success", "alert-warning", "alert-danger");
|
||||
checkInStatus.classList.add("alert-danger");
|
||||
checkInStatus.textContent = "Ошибка сети: " + error.message;
|
||||
checkInBtn.disabled = false;
|
||||
checkInBtn.textContent = "Отметить вход";
|
||||
});
|
||||
});
|
||||
|
||||
resetBtn.addEventListener("click", resetGuestCard);
|
||||
|
||||
// If token is in URL (scanned via external scanner)
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("token");
|
||||
if (token) {
|
||||
loadGuestByToken(token);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user