Initial commit
This commit is contained in:
141
src-backup/assets/css/style.css
Normal file
141
src-backup/assets/css/style.css
Normal file
@@ -0,0 +1,141 @@
|
||||
/* Main Stylesheet */
|
||||
:root {
|
||||
--primary-color: #3498db;
|
||||
--secondary-color: #2c3e50;
|
||||
--success-color: #27ae60;
|
||||
--danger-color: #e74c3c;
|
||||
--warning-color: #f39c12;
|
||||
--info-color: #17a2b8;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #336494;
|
||||
margin: 10;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
background: linear-gradient(135deg, #8bea66 0%, #a0a24b 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
border-radius: 15px 15px 0 0 !important;
|
||||
}
|
||||
|
||||
/* Table Styles */
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
padding: 8px 20px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Badge Styles */
|
||||
.badge {
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-control {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(52, 152, 219, 0.25);
|
||||
}
|
||||
|
||||
/* Alert Styles */
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
/* Animation for attendance success */
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.attendance-success {
|
||||
animation: successPulse 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Scanner container */
|
||||
#qr-reader {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Calendar icon */
|
||||
.calendar-icon {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
width: 50px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.sidebar, .navbar, .btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
BIN
src-backup/assets/image/aldersgate.png
Normal file
BIN
src-backup/assets/image/aldersgate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 511 KiB |
273
src-backup/assets/js/scanner.js
Normal file
273
src-backup/assets/js/scanner.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* QR Code Scanner Module
|
||||
*/
|
||||
class QRScanner {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
scannerId: 'qr-reader',
|
||||
resultId: 'scan-result',
|
||||
facingMode: 'environment',
|
||||
fps: 10,
|
||||
qrbox: { width: 250, height: 250 },
|
||||
...options
|
||||
};
|
||||
|
||||
this.scanner = null;
|
||||
this.isScanning = false;
|
||||
this.lastScanTime = 0;
|
||||
this.scanCooldown = 2000; // 2 seconds cooldown
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.scanner = new Html5Qrcode(this.options.scannerId);
|
||||
|
||||
// Create result container if not exists
|
||||
if (!document.getElementById(this.options.resultId)) {
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.id = this.options.resultId;
|
||||
document.querySelector(`#${this.options.scannerId}`).parentNode.appendChild(resultDiv);
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.isScanning) return;
|
||||
|
||||
try {
|
||||
await this.scanner.start(
|
||||
{ facingMode: this.options.facingMode },
|
||||
this.options,
|
||||
this.onScanSuccess.bind(this),
|
||||
this.onScanError.bind(this)
|
||||
);
|
||||
|
||||
this.isScanning = true;
|
||||
this.updateUI('started');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start scanner:', error);
|
||||
this.showError('Failed to start camera. Please check permissions.');
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.isScanning) return;
|
||||
|
||||
this.scanner.stop().then(() => {
|
||||
this.isScanning = false;
|
||||
this.updateUI('stopped');
|
||||
}).catch(error => {
|
||||
console.error('Failed to stop scanner:', error);
|
||||
});
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Prevent multiple scans in short time
|
||||
if (currentTime - this.lastScanTime < this.scanCooldown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastScanTime = currentTime;
|
||||
|
||||
// Show scanning indicator
|
||||
this.showResult('scanning', 'Scanning QR code...');
|
||||
|
||||
// Process the scan
|
||||
this.processQRCode(decodedText);
|
||||
}
|
||||
|
||||
onScanError(error) {
|
||||
console.warn('QR Scan Error:', error);
|
||||
// Don't show errors to user unless critical
|
||||
}
|
||||
|
||||
async processQRCode(qrData) {
|
||||
try {
|
||||
const response = await fetch('../api/scan_qr.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ qr_code: qrData })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showResult('success', data);
|
||||
this.playSound('success');
|
||||
|
||||
// Auto-stop after successful scan (optional)
|
||||
if (this.options.autoStop) {
|
||||
setTimeout(() => this.stop(), 3000);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.showResult('error', data.message);
|
||||
this.playSound('error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Processing error:', error);
|
||||
this.showResult('error', 'Network error. Please try again.');
|
||||
this.playSound('error');
|
||||
}
|
||||
}
|
||||
|
||||
showResult(type, data) {
|
||||
const resultDiv = document.getElementById(this.options.resultId);
|
||||
|
||||
let html = '';
|
||||
|
||||
switch(type) {
|
||||
case 'scanning':
|
||||
html = `
|
||||
<div class="alert alert-info">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
${data}
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'success':
|
||||
html = `
|
||||
<div class="alert alert-success attendance-success">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="bi bi-check-circle-fill me-3" style="font-size: 2rem;"></i>
|
||||
<div>
|
||||
<h5 class="mb-1">Attendance Recorded!</h5>
|
||||
<p class="mb-1"><strong>Student:</strong> ${data.data.student_name}</p>
|
||||
<p class="mb-1"><strong>Activity:</strong> ${data.data.activity_name}</p>
|
||||
<p class="mb-0"><strong>Time:</strong> ${data.data.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
html = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
${data}
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
resultDiv.innerHTML = html;
|
||||
|
||||
// Auto-clear result after some time
|
||||
if (type !== 'scanning') {
|
||||
setTimeout(() => {
|
||||
if (resultDiv.innerHTML.includes(html)) {
|
||||
resultDiv.innerHTML = '';
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const resultDiv = document.getElementById(this.options.resultId);
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-camera-video-off me-2"></i>
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updateUI(state) {
|
||||
const scannerDiv = document.getElementById(this.options.scannerId);
|
||||
|
||||
if (state === 'started') {
|
||||
scannerDiv.classList.add('scanning');
|
||||
} else {
|
||||
scannerDiv.classList.remove('scanning');
|
||||
}
|
||||
}
|
||||
|
||||
playSound(type) {
|
||||
// Create audio context for sounds
|
||||
try {
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
if (type === 'success') {
|
||||
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
|
||||
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
|
||||
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
|
||||
} else {
|
||||
oscillator.frequency.setValueAtTime(200, audioContext.currentTime); // Low tone
|
||||
}
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + 0.3);
|
||||
|
||||
} catch (error) {
|
||||
console.log('Audio not supported');
|
||||
}
|
||||
}
|
||||
|
||||
toggleCamera() {
|
||||
if (this.options.facingMode === 'environment') {
|
||||
this.options.facingMode = 'user';
|
||||
} else {
|
||||
this.options.facingMode = 'environment';
|
||||
}
|
||||
|
||||
this.stop();
|
||||
setTimeout(() => this.start(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize scanner when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check for scanner elements on page
|
||||
const scannerElements = document.querySelectorAll('[data-scanner]');
|
||||
|
||||
scannerElements.forEach(element => {
|
||||
const scannerId = element.getAttribute('data-scanner') || 'qr-reader';
|
||||
const scanner = new QRScanner({
|
||||
scannerId: scannerId,
|
||||
autoStop: element.getAttribute('data-auto-stop') === 'true'
|
||||
});
|
||||
|
||||
// Add start/stop buttons if not auto-start
|
||||
if (element.getAttribute('data-auto-start') !== 'true') {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'scanner-controls mt-3';
|
||||
controls.innerHTML = `
|
||||
<button class="btn btn-success me-2 start-scan">
|
||||
<i class="bi bi-play-circle"></i> Start Scanner
|
||||
</button>
|
||||
<button class="btn btn-danger me-2 stop-scan">
|
||||
<i class="bi bi-stop-circle"></i> Stop Scanner
|
||||
</button>
|
||||
<button class="btn btn-secondary toggle-camera">
|
||||
<i class="bi bi-camera-video"></i> Switch Camera
|
||||
</button>
|
||||
`;
|
||||
|
||||
element.parentNode.appendChild(controls);
|
||||
|
||||
// Add event listeners
|
||||
controls.querySelector('.start-scan').addEventListener('click', () => scanner.start());
|
||||
controls.querySelector('.stop-scan').addEventListener('click', () => scanner.stop());
|
||||
controls.querySelector('.toggle-camera').addEventListener('click', () => scanner.toggleCamera());
|
||||
} else {
|
||||
// Auto-start scanner
|
||||
setTimeout(() => scanner.start(), 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user