Spaces:
Running
Running
Update index.html
Browse files- index.html +110 -87
index.html
CHANGED
|
@@ -341,40 +341,32 @@
|
|
| 341 |
</style>
|
| 342 |
</head>
|
| 343 |
<body>
|
| 344 |
-
<!-- Animated particles background -->
|
| 345 |
<div class="particles" id="particles"></div>
|
| 346 |
-
|
| 347 |
<div class="container">
|
| 348 |
<div class="header">
|
| 349 |
<h1>Exam Countdown</h1>
|
| 350 |
-
<div class="settings-btn"
|
| 351 |
<svg viewBox="0 0 24 24">
|
| 352 |
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
| 353 |
</svg>
|
| 354 |
</div>
|
| 355 |
</div>
|
| 356 |
-
|
| 357 |
-
<div class="exams-grid" id="examsGrid">
|
| 358 |
-
<!-- Exam cards will be inserted here -->
|
| 359 |
-
</div>
|
| 360 |
-
|
| 361 |
<div class="empty-state" id="emptyState" style="display: none;">
|
| 362 |
<p>No exams scheduled yet. Click the + button to add your first exam!</p>
|
| 363 |
</div>
|
| 364 |
</div>
|
| 365 |
-
|
| 366 |
-
<!-- Add Exam Modal -->
|
| 367 |
<div class="modal" id="modal">
|
| 368 |
<div class="modal-content">
|
| 369 |
<div class="modal-header">
|
| 370 |
<h2 class="modal-title">Add New Exam</h2>
|
| 371 |
-
<button class="close-btn"
|
| 372 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff">
|
| 373 |
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
| 374 |
</svg>
|
| 375 |
</button>
|
| 376 |
</div>
|
| 377 |
-
<form
|
| 378 |
<div class="form-group">
|
| 379 |
<label for="examName">Exam Name</label>
|
| 380 |
<input type="text" id="examName" required placeholder="e.g., Physics Final">
|
|
@@ -388,10 +380,39 @@
|
|
| 388 |
</div>
|
| 389 |
</div>
|
| 390 |
|
| 391 |
-
<script>
|
| 392 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
function createParticles() {
|
| 394 |
const particlesContainer = document.getElementById('particles');
|
|
|
|
| 395 |
for (let i = 0; i < 50; i++) {
|
| 396 |
const particle = document.createElement('div');
|
| 397 |
particle.className = 'particle';
|
|
@@ -402,120 +423,122 @@
|
|
| 402 |
}
|
| 403 |
}
|
| 404 |
|
| 405 |
-
//
|
| 406 |
-
|
|
|
|
| 407 |
|
| 408 |
-
//
|
| 409 |
-
function openModal() {
|
| 410 |
-
document.getElementById('modal').classList.add('active');
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
function closeModal() {
|
| 414 |
-
document.getElementById('modal').classList.remove('active');
|
| 415 |
-
document.getElementById('examName').value = '';
|
| 416 |
-
document.getElementById('examDate').value = '';
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
// Add exam
|
| 420 |
function addExam(event) {
|
| 421 |
event.preventDefault();
|
| 422 |
-
const
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
const exam = {
|
| 426 |
-
id: Date.now(),
|
| 427 |
-
name,
|
| 428 |
-
date
|
| 429 |
-
};
|
| 430 |
-
|
| 431 |
-
exams.push(exam);
|
| 432 |
-
localStorage.setItem('exams', JSON.stringify(exams));
|
| 433 |
closeModal();
|
| 434 |
-
renderExams();
|
| 435 |
}
|
| 436 |
|
| 437 |
-
// Delete exam
|
| 438 |
function deleteExam(id) {
|
| 439 |
-
|
| 440 |
-
localStorage.setItem('exams', JSON.stringify(exams));
|
| 441 |
-
renderExams();
|
| 442 |
}
|
| 443 |
|
| 444 |
-
//
|
| 445 |
function calculateDaysUntil(examDate) {
|
| 446 |
const today = new Date();
|
|
|
|
| 447 |
const exam = new Date(examDate);
|
| 448 |
-
|
| 449 |
-
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
| 450 |
-
return diffDays;
|
| 451 |
}
|
| 452 |
|
| 453 |
-
|
| 454 |
-
function renderExams() {
|
| 455 |
const grid = document.getElementById('examsGrid');
|
| 456 |
const emptyState = document.getElementById('emptyState');
|
| 457 |
-
|
| 458 |
grid.innerHTML = '';
|
| 459 |
|
| 460 |
-
if (
|
| 461 |
emptyState.style.display = 'block';
|
| 462 |
return;
|
| 463 |
}
|
| 464 |
|
| 465 |
emptyState.style.display = 'none';
|
| 466 |
-
|
| 467 |
-
// Sort exams by date
|
| 468 |
-
const sortedExams = [...exams].sort((a, b) => new Date(a.date) - new Date(b.date));
|
| 469 |
|
| 470 |
sortedExams.forEach(exam => {
|
| 471 |
const daysUntil = calculateDaysUntil(exam.date);
|
| 472 |
const card = document.createElement('div');
|
| 473 |
card.className = 'exam-card';
|
| 474 |
-
|
| 475 |
-
let countdownColor = '#00D4FF';
|
| 476 |
-
if (daysUntil <= 3) countdownColor = '#FF0080';
|
| 477 |
-
else if (daysUntil <= 7) countdownColor = '#00FF88';
|
| 478 |
|
| 479 |
card.innerHTML = `
|
| 480 |
-
<div class="delete-btn"
|
| 481 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="#FF0080">
|
| 482 |
-
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
| 483 |
-
</svg>
|
| 484 |
</div>
|
| 485 |
<h3 class="exam-name">${exam.name}</h3>
|
| 486 |
-
<div class="countdown" style="color: ${
|
| 487 |
-
${daysUntil}
|
| 488 |
-
</div>
|
| 489 |
<div class="countdown-label">days remaining</div>
|
| 490 |
-
<div class="exam-date">${new Date(exam.date).toLocaleDateString('en-US', {
|
| 491 |
-
weekday: 'long',
|
| 492 |
-
year: 'numeric',
|
| 493 |
-
month: 'long',
|
| 494 |
-
day: 'numeric'
|
| 495 |
-
})}</div>
|
| 496 |
-
`;
|
| 497 |
|
|
|
|
| 498 |
grid.appendChild(card);
|
| 499 |
});
|
| 500 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
-
|
| 503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
|
| 505 |
-
//
|
| 506 |
createParticles();
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
}
|
| 518 |
-
|
|
|
|
|
|
|
| 519 |
</script>
|
| 520 |
</body>
|
| 521 |
</html>
|
|
|
|
| 341 |
</style>
|
| 342 |
</head>
|
| 343 |
<body>
|
|
|
|
| 344 |
<div class="particles" id="particles"></div>
|
|
|
|
| 345 |
<div class="container">
|
| 346 |
<div class="header">
|
| 347 |
<h1>Exam Countdown</h1>
|
| 348 |
+
<div class="settings-btn" id="openModalBtn">
|
| 349 |
<svg viewBox="0 0 24 24">
|
| 350 |
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/>
|
| 351 |
</svg>
|
| 352 |
</div>
|
| 353 |
</div>
|
| 354 |
+
<div class="exams-grid" id="examsGrid"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
<div class="empty-state" id="emptyState" style="display: none;">
|
| 356 |
<p>No exams scheduled yet. Click the + button to add your first exam!</p>
|
| 357 |
</div>
|
| 358 |
</div>
|
|
|
|
|
|
|
| 359 |
<div class="modal" id="modal">
|
| 360 |
<div class="modal-content">
|
| 361 |
<div class="modal-header">
|
| 362 |
<h2 class="modal-title">Add New Exam</h2>
|
| 363 |
+
<button class="close-btn" id="closeModalBtn">
|
| 364 |
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff">
|
| 365 |
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
| 366 |
</svg>
|
| 367 |
</button>
|
| 368 |
</div>
|
| 369 |
+
<form id="addExamForm">
|
| 370 |
<div class="form-group">
|
| 371 |
<label for="examName">Exam Name</label>
|
| 372 |
<input type="text" id="examName" required placeholder="e.g., Physics Final">
|
|
|
|
| 380 |
</div>
|
| 381 |
</div>
|
| 382 |
|
| 383 |
+
<script type="module">
|
| 384 |
+
// Import the functions you need from the SDKs you need
|
| 385 |
+
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.0/firebase-app.js";
|
| 386 |
+
import { getDatabase, ref, onValue, push, remove, child } from "https://www.gstatic.com/firebasejs/10.7.0/firebase-database.js";
|
| 387 |
+
|
| 388 |
+
// Your web app's Firebase configuration
|
| 389 |
+
const firebaseConfig = {
|
| 390 |
+
apiKey: "AIzaSyCBGYdGdPjYJiKsTMjVYZ9mf9F82ns7g4Q",
|
| 391 |
+
authDomain: "pikachu-rxppbp.firebaseapp.com",
|
| 392 |
+
databaseURL: "https://pikachu-rxppbp.firebaseio.com",
|
| 393 |
+
projectId: "pikachu-rxppbp",
|
| 394 |
+
storageBucket: "pikachu-rxppbp.appspot.com",
|
| 395 |
+
messagingSenderId: "241970333280",
|
| 396 |
+
appId: "1:241970333280:web:704e8930bd591c138d6505"
|
| 397 |
+
};
|
| 398 |
+
|
| 399 |
+
// Initialize Firebase
|
| 400 |
+
const app = initializeApp(firebaseConfig);
|
| 401 |
+
const database = getDatabase(app);
|
| 402 |
+
const examsRef = ref(database, 'exams');
|
| 403 |
+
|
| 404 |
+
// --- DOM Elements ---
|
| 405 |
+
const openModalBtn = document.getElementById('openModalBtn');
|
| 406 |
+
const closeModalBtn = document.getElementById('closeModalBtn');
|
| 407 |
+
const modal = document.getElementById('modal');
|
| 408 |
+
const addExamForm = document.getElementById('addExamForm');
|
| 409 |
+
const examNameInput = document.getElementById('examName');
|
| 410 |
+
const examDateInput = document.getElementById('examDate');
|
| 411 |
+
|
| 412 |
+
// --- Particle Background ---
|
| 413 |
function createParticles() {
|
| 414 |
const particlesContainer = document.getElementById('particles');
|
| 415 |
+
if (particlesContainer.children.length > 0) return;
|
| 416 |
for (let i = 0; i < 50; i++) {
|
| 417 |
const particle = document.createElement('div');
|
| 418 |
particle.className = 'particle';
|
|
|
|
| 423 |
}
|
| 424 |
}
|
| 425 |
|
| 426 |
+
// --- Modal ---
|
| 427 |
+
function openModal() { modal.classList.add('active'); }
|
| 428 |
+
function closeModal() { modal.classList.remove('active'); addExamForm.reset(); }
|
| 429 |
|
| 430 |
+
// --- Firebase Data Operations ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
function addExam(event) {
|
| 432 |
event.preventDefault();
|
| 433 |
+
const newExam = { name: examNameInput.value, date: examDateInput.value };
|
| 434 |
+
push(examsRef, newExam);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
closeModal();
|
|
|
|
| 436 |
}
|
| 437 |
|
|
|
|
| 438 |
function deleteExam(id) {
|
| 439 |
+
remove(child(examsRef, id));
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
|
| 442 |
+
// --- Rendering Logic ---
|
| 443 |
function calculateDaysUntil(examDate) {
|
| 444 |
const today = new Date();
|
| 445 |
+
today.setHours(0, 0, 0, 0);
|
| 446 |
const exam = new Date(examDate);
|
| 447 |
+
return Math.ceil((exam - today) / (1000 * 60 * 60 * 24));
|
|
|
|
|
|
|
| 448 |
}
|
| 449 |
|
| 450 |
+
function renderExams(examsList) {
|
|
|
|
| 451 |
const grid = document.getElementById('examsGrid');
|
| 452 |
const emptyState = document.getElementById('emptyState');
|
|
|
|
| 453 |
grid.innerHTML = '';
|
| 454 |
|
| 455 |
+
if (!examsList || examsList.length === 0) {
|
| 456 |
emptyState.style.display = 'block';
|
| 457 |
return;
|
| 458 |
}
|
| 459 |
|
| 460 |
emptyState.style.display = 'none';
|
| 461 |
+
const sortedExams = [...examsList].sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
|
|
|
|
|
| 462 |
|
| 463 |
sortedExams.forEach(exam => {
|
| 464 |
const daysUntil = calculateDaysUntil(exam.date);
|
| 465 |
const card = document.createElement('div');
|
| 466 |
card.className = 'exam-card';
|
| 467 |
+
let color = daysUntil <= 3 ? '#FF0080' : (daysUntil <= 7 ? '#00FF88' : '#00D4FF');
|
|
|
|
|
|
|
|
|
|
| 468 |
|
| 469 |
card.innerHTML = `
|
| 470 |
+
<div class="delete-btn">
|
| 471 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="#FF0080"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
|
|
|
|
|
| 472 |
</div>
|
| 473 |
<h3 class="exam-name">${exam.name}</h3>
|
| 474 |
+
<div class="countdown" style="color: ${color}; text-shadow: 0 0 20px ${color}80;">${daysUntil}</div>
|
|
|
|
|
|
|
| 475 |
<div class="countdown-label">days remaining</div>
|
| 476 |
+
<div class="exam-date">${new Date(exam.date).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
+
card.querySelector('.delete-btn').addEventListener('click', () => deleteExam(exam.id));
|
| 479 |
grid.appendChild(card);
|
| 480 |
});
|
| 481 |
}
|
| 482 |
+
|
| 483 |
+
// ===================================================================================
|
| 484 |
+
// ONE-TIME MANUAL IMPORT FUNCTION
|
| 485 |
+
// TO USE: Open developer console (F12) and type `runManualImport()` then press Enter.
|
| 486 |
+
// AFTER RUNNING ONCE, DELETE THIS ENTIRE FUNCTION.
|
| 487 |
+
// ===================================================================================
|
| 488 |
+
window.runManualImport = function() {
|
| 489 |
+
const localExamsJSON = localStorage.getItem('exams');
|
| 490 |
+
if (!localExamsJSON) {
|
| 491 |
+
console.log("No data found in LocalStorage. Nothing to import.");
|
| 492 |
+
return;
|
| 493 |
+
}
|
| 494 |
|
| 495 |
+
try {
|
| 496 |
+
const localExams = JSON.parse(localExamsJSON);
|
| 497 |
+
if (Array.isArray(localExams) && localExams.length > 0) {
|
| 498 |
+
console.log(`Found ${localExams.length} exams in LocalStorage. Importing...`);
|
| 499 |
+
|
| 500 |
+
const importPromises = localExams.map(exam => {
|
| 501 |
+
const examToPush = { name: exam.name, date: exam.date };
|
| 502 |
+
return push(examsRef, examToPush);
|
| 503 |
+
});
|
| 504 |
+
|
| 505 |
+
Promise.all(importPromises).then(() => {
|
| 506 |
+
console.log("SUCCESS: All exams have been imported to Firebase.");
|
| 507 |
+
alert("Import complete! Your data is now in Firebase. You can now delete the runManualImport function from your code.");
|
| 508 |
+
});
|
| 509 |
+
} else {
|
| 510 |
+
console.log("LocalStorage data is empty or invalid. Nothing to import.");
|
| 511 |
+
}
|
| 512 |
+
} catch (error) {
|
| 513 |
+
console.error("FAILED to parse or import from LocalStorage:", error);
|
| 514 |
+
alert("Import failed. Check the console for errors.");
|
| 515 |
+
}
|
| 516 |
+
};
|
| 517 |
+
// ===================================================================================
|
| 518 |
+
// END OF IMPORT FUNCTION - REMOVE AFTER USE
|
| 519 |
+
// ===================================================================================
|
| 520 |
|
| 521 |
+
// --- INITIALIZATION & EVENT LISTENERS ---
|
| 522 |
createParticles();
|
| 523 |
+
examDateInput.min = new Date().toISOString().split('T')[0];
|
| 524 |
+
|
| 525 |
+
openModalBtn.addEventListener('click', openModal);
|
| 526 |
+
closeModalBtn.addEventListener('click', closeModal);
|
| 527 |
+
addExamForm.addEventListener('submit', addExam);
|
| 528 |
+
window.addEventListener('click', (event) => {
|
| 529 |
+
if (event.target === modal) closeModal();
|
| 530 |
+
});
|
| 531 |
+
|
| 532 |
+
// Real-time listener for Firebase data
|
| 533 |
+
onValue(examsRef, (snapshot) => {
|
| 534 |
+
const examsData = snapshot.val();
|
| 535 |
+
const examsList = [];
|
| 536 |
+
for (let id in examsData) {
|
| 537 |
+
examsList.push({ id, ...examsData[id] });
|
| 538 |
}
|
| 539 |
+
renderExams(examsList);
|
| 540 |
+
});
|
| 541 |
+
|
| 542 |
</script>
|
| 543 |
</body>
|
| 544 |
</html>
|