Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Neon Exam Countdown</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --neon-pink: #FF006E; | |
| --neon-blue: #00D9FF; | |
| --neon-purple: #8B39FF; | |
| --neon-green: #39FF14; | |
| --neon-yellow: #FFFF00; | |
| --dark-bg: #0A0A0F; | |
| } | |
| body { | |
| font-family: 'Space Grotesk', sans-serif; | |
| background: var(--dark-bg); | |
| min-height: 100vh; | |
| color: #fff; | |
| overflow-x: hidden; | |
| position: relative; | |
| } | |
| /* Animated gradient background */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 20% 80%, rgba(255, 0, 110, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 20%, rgba(0, 217, 255, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 40%, rgba(139, 57, 255, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 90% 70%, rgba(57, 255, 20, 0.05) 0%, transparent 50%); | |
| animation: gradientShift 15s ease infinite; | |
| z-index: 0; | |
| } | |
| @keyframes gradientShift { | |
| 0%, 100% { transform: rotate(0deg) scale(1); } | |
| 33% { transform: rotate(120deg) scale(1.1); } | |
| 66% { transform: rotate(240deg) scale(1.05); } | |
| } | |
| /* Grid background */ | |
| body::after { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-image: | |
| linear-gradient(rgba(0, 217, 255, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0, 217, 255, 0.03) 1px, transparent 1px); | |
| background-size: 50px 50px; | |
| animation: gridMove 10s linear infinite; | |
| z-index: 0; | |
| } | |
| @keyframes gridMove { | |
| 0% { transform: translate(0, 0); } | |
| 100% { transform: translate(50px, 50px); } | |
| } | |
| /* Enhanced particles */ | |
| .particles { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| z-index: 1; | |
| } | |
| .particle { | |
| position: absolute; | |
| border-radius: 50%; | |
| pointer-events: none; | |
| } | |
| .particle.orb { | |
| width: 6px; | |
| height: 6px; | |
| background: radial-gradient(circle, var(--neon-blue) 0%, transparent 70%); | |
| box-shadow: 0 0 10px var(--neon-blue), 0 0 20px var(--neon-blue); | |
| animation: floatOrb 20s infinite linear; | |
| } | |
| .particle.star { | |
| width: 2px; | |
| height: 2px; | |
| background: white; | |
| box-shadow: 0 0 6px white; | |
| animation: twinkle 3s infinite; | |
| } | |
| @keyframes floatOrb { | |
| from { | |
| transform: translateY(100vh) translateX(0) scale(0); | |
| opacity: 0; | |
| } | |
| 10% { | |
| opacity: 1; | |
| transform: translateY(90vh) translateX(10px) scale(1); | |
| } | |
| 90% { | |
| opacity: 1; | |
| transform: translateY(10vh) translateX(-10px) scale(1); | |
| } | |
| to { | |
| transform: translateY(-100vh) translateX(100px) scale(0); | |
| opacity: 0; | |
| } | |
| } | |
| @keyframes twinkle { | |
| 0%, 100% { opacity: 0; transform: scale(0.5); } | |
| 50% { opacity: 1; transform: scale(1); } | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| /* Enhanced header */ | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 4rem; | |
| padding: 2rem 0; | |
| position: relative; | |
| } | |
| .header::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 100%; | |
| height: 1px; | |
| background: linear-gradient(90deg, | |
| transparent, | |
| var(--neon-blue) 20%, | |
| var(--neon-pink) 50%, | |
| var(--neon-purple) 80%, | |
| transparent); | |
| animation: lineGlow 3s ease-in-out infinite; | |
| } | |
| @keyframes lineGlow { | |
| 0%, 100% { opacity: 0.3; } | |
| 50% { opacity: 1; } | |
| } | |
| h1 { | |
| font-family: 'Orbitron', monospace; | |
| font-size: 3.5rem; | |
| font-weight: 900; | |
| text-transform: uppercase; | |
| letter-spacing: 3px; | |
| background: linear-gradient(135deg, | |
| var(--neon-blue) 0%, | |
| var(--neon-pink) 25%, | |
| var(--neon-purple) 50%, | |
| var(--neon-green) 75%, | |
| var(--neon-blue) 100%); | |
| background-size: 300% 300%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: gradientFlow 4s ease infinite; | |
| filter: drop-shadow(0 0 30px rgba(0, 217, 255, 0.5)) | |
| drop-shadow(0 0 60px rgba(255, 0, 110, 0.3)); | |
| } | |
| @keyframes gradientFlow { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| /* Enhanced settings button */ | |
| .settings-btn { | |
| width: 60px; | |
| height: 60px; | |
| background: linear-gradient(135deg, | |
| rgba(0, 217, 255, 0.1) 0%, | |
| rgba(255, 0, 110, 0.1) 100%); | |
| backdrop-filter: blur(20px); | |
| border: 2px solid transparent; | |
| background-origin: border-box; | |
| background-clip: padding-box, border-box; | |
| border-image: linear-gradient(135deg, var(--neon-blue), var(--neon-pink)) 1; | |
| border-radius: 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .settings-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 100%; | |
| height: 100%; | |
| background: radial-gradient(circle, var(--neon-blue) 0%, transparent 70%); | |
| transform: translate(-50%, -50%) scale(0); | |
| transition: transform 0.5s ease; | |
| z-index: -1; | |
| } | |
| .settings-btn:hover { | |
| transform: scale(1.1) rotate(90deg); | |
| box-shadow: | |
| 0 0 30px rgba(0, 217, 255, 0.6), | |
| 0 0 60px rgba(255, 0, 110, 0.4), | |
| inset 0 0 20px rgba(0, 217, 255, 0.2); | |
| } | |
| .settings-btn:hover::before { | |
| transform: translate(-50%, -50%) scale(2); | |
| } | |
| .settings-btn svg { | |
| width: 28px; | |
| height: 28px; | |
| fill: url(#iconGradient); | |
| filter: drop-shadow(0 0 10px var(--neon-blue)); | |
| transition: transform 0.4s ease; | |
| } | |
| .settings-btn:hover svg { | |
| transform: rotate(-90deg); | |
| } | |
| /* Enhanced exam grid */ | |
| .exams-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); | |
| gap: 2.5rem; | |
| padding: 2rem 0; | |
| } | |
| /* Ultra enhanced exam cards */ | |
| .exam-card { | |
| background: linear-gradient(135deg, | |
| rgba(0, 217, 255, 0.05) 0%, | |
| rgba(255, 0, 110, 0.05) 100%); | |
| backdrop-filter: blur(40px) saturate(150%); | |
| border: 1px solid transparent; | |
| background-origin: border-box; | |
| background-clip: padding-box, border-box; | |
| border-image: linear-gradient(135deg, | |
| rgba(0, 217, 255, 0.3), | |
| rgba(255, 0, 110, 0.3)) 1; | |
| border-radius: 24px; | |
| padding: 2.5rem; | |
| text-align: center; | |
| transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| position: relative; | |
| overflow: hidden; | |
| transform-style: preserve-3d; | |
| perspective: 1000px; | |
| } | |
| .exam-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, | |
| rgba(0, 217, 255, 0.1) 0%, | |
| transparent 40%); | |
| animation: cardPulse 4s ease-in-out infinite; | |
| pointer-events: none; | |
| } | |
| .exam-card::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(105deg, | |
| transparent 40%, | |
| rgba(255, 255, 255, 0.05) 45%, | |
| rgba(255, 255, 255, 0.1) 50%, | |
| rgba(255, 255, 255, 0.05) 55%, | |
| transparent 60%); | |
| transform: translateX(-100%); | |
| transition: transform 0.6s; | |
| pointer-events: none; | |
| } | |
| @keyframes cardPulse { | |
| 0%, 100% { transform: scale(1) rotate(0deg); opacity: 0.5; } | |
| 50% { transform: scale(1.2) rotate(180deg); opacity: 0.8; } | |
| } | |
| .exam-card:hover { | |
| transform: translateY(-10px) rotateX(5deg) scale(1.02); | |
| box-shadow: | |
| 0 20px 40px rgba(0, 217, 255, 0.3), | |
| 0 40px 80px rgba(255, 0, 110, 0.2), | |
| inset 0 0 30px rgba(0, 217, 255, 0.1); | |
| border-image: linear-gradient(135deg, | |
| var(--neon-blue), | |
| var(--neon-pink)) 1; | |
| } | |
| .exam-card:hover::after { | |
| transform: translateX(100%); | |
| } | |
| .exam-name { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| margin-bottom: 1.5rem; | |
| background: linear-gradient(135deg, #fff 0%, #00D9FF 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| filter: drop-shadow(0 2px 10px rgba(0, 217, 255, 0.5)); | |
| } | |
| .countdown-wrapper { | |
| display: flex; | |
| justify-content: center; | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .countdown-item { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .countdown-number { | |
| font-family: 'Orbitron', monospace; | |
| font-size: 3rem; | |
| font-weight: 900; | |
| color: var(--neon-blue); | |
| text-shadow: 0 0 20px var(--neon-blue); | |
| line-height: 1; | |
| } | |
| .countdown-label { | |
| font-size: 0.8rem; | |
| color: rgba(255, 255, 255, 0.7); | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| font-weight: 300; | |
| } | |
| .exam-date { | |
| font-size: 0.95rem; | |
| color: rgba(0, 217, 255, 0.7); | |
| margin-top: 1.5rem; | |
| padding: 0.5rem 1rem; | |
| background: rgba(0, 217, 255, 0.1); | |
| border-radius: 20px; | |
| display: inline-block; | |
| border: 1px solid rgba(0, 217, 255, 0.3); | |
| } | |
| /* Enhanced delete button */ | |
| .delete-btn { | |
| position: absolute; | |
| top: 1.5rem; | |
| right: 1.5rem; | |
| width: 36px; | |
| height: 36px; | |
| background: linear-gradient(135deg, | |
| rgba(255, 0, 110, 0.2) 0%, | |
| rgba(255, 0, 0, 0.2) 100%); | |
| border: 2px solid rgba(255, 0, 110, 0.5); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| opacity: 0; | |
| transform: scale(0.8); | |
| } | |
| .exam-card:hover .delete-btn { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| .delete-btn:hover { | |
| background: linear-gradient(135deg, | |
| rgba(255, 0, 110, 0.4) 0%, | |
| rgba(255, 0, 0, 0.4) 100%); | |
| transform: scale(1.2) rotate(90deg); | |
| box-shadow: 0 0 20px rgba(255, 0, 110, 0.6); | |
| } | |
| /* Enhanced modal */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.9); | |
| backdrop-filter: blur(10px); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| animation: modalFadeIn 0.3s ease; | |
| } | |
| @keyframes modalFadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .modal.active { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: linear-gradient(135deg, | |
| rgba(20, 20, 30, 0.98) 0%, | |
| rgba(30, 20, 40, 0.98) 100%); | |
| backdrop-filter: blur(40px) saturate(150%); | |
| border: 2px solid transparent; | |
| background-origin: border-box; | |
| background-clip: padding-box, border-box; | |
| border-image: linear-gradient(135deg, var(--neon-blue), var(--neon-pink)) 1; | |
| border-radius: 30px; | |
| padding: 3rem; | |
| width: 100%; | |
| max-width: 500px; | |
| box-shadow: | |
| 0 30px 60px rgba(0, 0, 0, 0.5), | |
| 0 0 100px rgba(0, 217, 255, 0.2), | |
| inset 0 0 30px rgba(0, 217, 255, 0.05); | |
| animation: modalSlideIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .modal-content::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: radial-gradient(circle, | |
| rgba(0, 217, 255, 0.1) 0%, | |
| transparent 40%); | |
| animation: modalGlow 4s ease-in-out infinite; | |
| } | |
| @keyframes modalSlideIn { | |
| from { transform: translateY(-50px) scale(0.9); opacity: 0; } | |
| to { transform: translateY(0) scale(1); opacity: 1; } | |
| } | |
| @keyframes modalGlow { | |
| 0%, 100% { transform: rotate(0deg); } | |
| 50% { transform: rotate(180deg); } | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 2.5rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .modal-title { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, var(--neon-blue) 0%, var(--neon-pink) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .close-btn { | |
| width: 40px; | |
| height: 40px; | |
| background: rgba(255, 0, 110, 0.1); | |
| border: 2px solid rgba(255, 0, 110, 0.3); | |
| border-radius: 12px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.3s ease; | |
| } | |
| .close-btn:hover { | |
| transform: rotate(90deg) scale(1.1); | |
| background: rgba(255, 0, 110, 0.2); | |
| box-shadow: 0 0 20px rgba(255, 0, 110, 0.5); | |
| } | |
| /* Enhanced form */ | |
| .form-group { | |
| margin-bottom: 2rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 0.75rem; | |
| font-size: 0.95rem; | |
| color: var(--neon-blue); | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| font-weight: 500; | |
| } | |
| .form-group input { | |
| width: 100%; | |
| padding: 1.2rem; | |
| background: rgba(0, 217, 255, 0.05); | |
| border: 2px solid rgba(0, 217, 255, 0.2); | |
| border-radius: 12px; | |
| color: #fff; | |
| font-size: 1.1rem; | |
| transition: all 0.3s ease; | |
| font-family: 'Space Grotesk', sans-serif; | |
| } | |
| .form-group input:focus { | |
| outline: none; | |
| border-color: var(--neon-blue); | |
| background: rgba(0, 217, 255, 0.1); | |
| box-shadow: | |
| 0 0 20px rgba(0, 217, 255, 0.3), | |
| inset 0 0 10px rgba(0, 217, 255, 0.1); | |
| } | |
| .form-group input::placeholder { | |
| color: rgba(255, 255, 255, 0.3); | |
| } | |
| /* Enhanced add button */ | |
| .add-btn { | |
| width: 100%; | |
| padding: 1.5rem; | |
| background: linear-gradient(135deg, var(--neon-blue) 0%, var(--neon-pink) 100%); | |
| border: none; | |
| border-radius: 16px; | |
| color: #fff; | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| cursor: pointer; | |
| transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| position: relative; | |
| overflow: hidden; | |
| z-index: 1; | |
| } | |
| .add-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| background: radial-gradient(circle, | |
| rgba(255, 255, 255, 0.3) 0%, | |
| transparent 70%); | |
| transform: translate(-50%, -50%); | |
| transition: width 0.6s, height 0.6s; | |
| } | |
| .add-btn:hover { | |
| transform: translateY(-3px) scale(1.02); | |
| box-shadow: | |
| 0 10px 30px rgba(0, 217, 255, 0.5), | |
| 0 20px 60px rgba(255, 0, 110, 0.3); | |
| } | |
| .add-btn:hover::before { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| /* Enhanced empty state */ | |
| .empty-state { | |
| text-align: center; | |
| padding: 6rem 2rem; | |
| position: relative; | |
| } | |
| .empty-state::before { | |
| content: '📚'; | |
| font-size: 6rem; | |
| display: block; | |
| margin-bottom: 2rem; | |
| animation: bounce 2s ease-in-out infinite; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-20px); } | |
| } | |
| .empty-state p { | |
| font-size: 1.4rem; | |
| background: linear-gradient(135deg, | |
| rgba(255, 255, 255, 0.8) 0%, | |
| rgba(0, 217, 255, 0.8) 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| margin-bottom: 2rem; | |
| font-weight: 500; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| h1 { font-size: 2.5rem; } | |
| .countdown-number { font-size: 2rem; } | |
| .exams-grid { grid-template-columns: 1fr; } | |
| .header { flex-direction: column; gap: 1rem; text-align: center;} | |
| .modal-content { padding: 2rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg width="0" height="0"> | |
| <defs> | |
| <linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%"> | |
| <stop offset="0%" style="stop-color:#00D9FF;stop-opacity:1" /> | |
| <stop offset="100%" style="stop-color:#FF006E;stop-opacity:1" /> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| <div class="particles" id="particles"></div> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>Exam Countdown</h1> | |
| <div class="settings-btn" id="openModalBtn"> | |
| <svg viewBox="0 0 24 24"> | |
| <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"/> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="exams-grid" id="examsGrid"></div> | |
| <div class="empty-state" id="emptyState" style="display: none;"> | |
| <p>No exams scheduled yet. Click the + button to add your first exam!</p> | |
| </div> | |
| </div> | |
| <div class="modal" id="modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h2 class="modal-title">Add New Exam</h2> | |
| <button class="close-btn" id="closeModalBtn"> | |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="#FF006E"> | |
| <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"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <form id="addExamForm"> | |
| <div class="form-group"> | |
| <label for="examName">Exam Name</label> | |
| <input type="text" id="examName" required placeholder="e.g., Physics Final"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="examDate">Exam Date & Time</label> | |
| <input type="datetime-local" id="examDate" required> | |
| </div> | |
| <button type="submit" class="add-btn">Add Exam</button> | |
| </form> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| // Firebase SDK modules | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js"; | |
| import { getDatabase, ref, onValue, push, remove } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-database.js"; | |
| // Your web app's Firebase configuration | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyCBGYdGdPjYJiKsTMjVYZ9mf9F82ns7g4Q", | |
| authDomain: "pikachu-rxppbp.firebaseapp.com", | |
| databaseURL: "https://pikachu-rxppbp.firebaseio.com", | |
| projectId: "pikachu-rxppbp", | |
| storageBucket: "pikachu-rxppbp.appspot.com", | |
| messagingSenderId: "241970333280", | |
| appId: "1:241970333280:web:704e8930bd591c138d6505" | |
| }; | |
| // Initialize Firebase and Database | |
| const app = initializeApp(firebaseConfig); | |
| const database = getDatabase(app); | |
| const examsRef = ref(database, 'exams'); | |
| // DOM Element References | |
| const elements = { | |
| openModalBtn: document.getElementById('openModalBtn'), | |
| closeModalBtn: document.getElementById('closeModalBtn'), | |
| modal: document.getElementById('modal'), | |
| addExamForm: document.getElementById('addExamForm'), | |
| examNameInput: document.getElementById('examName'), | |
| examDateInput: document.getElementById('examDate'), | |
| examsGrid: document.getElementById('examsGrid'), | |
| emptyState: document.getElementById('emptyState'), | |
| particlesContainer: document.getElementById('particles') | |
| }; | |
| let exams = []; // This will hold our exams from Firebase | |
| // --- Enhanced Particle System --- | |
| function createParticles() { | |
| if (elements.particlesContainer.children.length > 0) return; | |
| // Create orbs | |
| for (let i = 0; i < 15; i++) { | |
| const particle = document.createElement('div'); | |
| particle.classList.add('particle', 'orb'); | |
| particle.style.left = Math.random() * 100 + '%'; | |
| particle.style.animationDelay = Math.random() * 20 + 's'; | |
| particle.style.animationDuration = (15 + Math.random() * 10) + 's'; | |
| const colors = ['#00D9FF', '#FF006E', '#8B39FF', '#39FF14']; | |
| const color = colors[Math.floor(Math.random() * colors.length)]; | |
| particle.style.background = `radial-gradient(circle, ${color} 0%, transparent 70%)`; | |
| particle.style.boxShadow = `0 0 10px ${color}, 0 0 20px ${color}`; | |
| elements.particlesContainer.appendChild(particle); | |
| } | |
| // Create stars | |
| for (let i = 0; i < 30; i++) { | |
| const star = document.createElement('div'); | |
| star.classList.add('particle', 'star'); | |
| star.style.left = Math.random() * 100 + '%'; | |
| star.style.top = Math.random() * 100 + '%'; | |
| star.style.animationDelay = Math.random() * 3 + 's'; | |
| elements.particlesContainer.appendChild(star); | |
| } | |
| } | |
| // --- Modal & Form Logic --- | |
| const openModal = () => elements.modal.classList.add('active'); | |
| const closeModal = () => { | |
| elements.modal.classList.remove('active'); | |
| elements.addExamForm.reset(); | |
| }; | |
| const addExam = (event) => { | |
| event.preventDefault(); | |
| const examIcons = ['📚', '📖', '✏️', '📝', '🎓', '📊', '🧮', '🔬', '💻', '🎨']; | |
| const newExam = { | |
| name: elements.examNameInput.value, | |
| date: new Date(elements.examDateInput.value).getTime(), | |
| icon: examIcons[Math.floor(Math.random() * examIcons.length)] | |
| }; | |
| push(examsRef, newExam).catch(err => console.error("Failed to add exam:", err)); | |
| closeModal(); | |
| }; | |
| const deleteExam = (id) => { | |
| if (!id) return; | |
| remove(ref(database, `exams/${id}`)).catch(err => console.error("Failed to delete exam:", err)); | |
| }; | |
| // --- Rendering Logic --- | |
| function getCountdown(targetDate) { | |
| const now = new Date().getTime(); | |
| const distance = targetDate - now; | |
| if (distance < 0) { | |
| return { days: 0, hours: 0, minutes: 0, seconds: 0, isPast: true }; | |
| } | |
| return { | |
| days: Math.floor(distance / (1000 * 60 * 60 * 24)), | |
| hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), | |
| minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)), | |
| seconds: Math.floor((distance % (1000 * 60)) / 1000), | |
| isPast: false | |
| }; | |
| } | |
| function renderExams() { | |
| if (exams.length === 0) { | |
| elements.examsGrid.innerHTML = ''; | |
| elements.emptyState.style.display = 'block'; | |
| return; | |
| } | |
| elements.emptyState.style.display = 'none'; | |
| // Sort by date so the soonest is first | |
| const sortedExams = [...exams].sort((a, b) => a.date - b.date); | |
| elements.examsGrid.innerHTML = sortedExams.map(exam => { | |
| const countdown = getCountdown(exam.date); | |
| const examDate = new Date(exam.date); | |
| let content; | |
| if(countdown.isPast){ | |
| content = `<div style="font-size: 2rem; color: var(--neon-pink); font-family: 'Orbitron', monospace;">EXAM COMPLETE</div>`; | |
| } else { | |
| content = ` | |
| <div class="countdown-wrapper"> | |
| <div class="countdown-item"> | |
| <div class="countdown-number">${String(countdown.days).padStart(2, '0')}</div> | |
| <div class="countdown-label">Days</div> | |
| </div> | |
| <div class="countdown-item"> | |
| <div class="countdown-number">${String(countdown.hours).padStart(2, '0')}</div> | |
| <div class="countdown-label">Hours</div> | |
| </div> | |
| <div class="countdown-item"> | |
| <div class="countdown-number">${String(countdown.minutes).padStart(2, '0')}</div> | |
| <div class="countdown-label">Minutes</div> | |
| </div> | |
| <div class="countdown-item"> | |
| <div class="countdown-number">${String(countdown.seconds).padStart(2, '0')}</div> | |
| <div class="countdown-label">Seconds</div> | |
| </div> | |
| </div>`; | |
| } | |
| return ` | |
| <div class="exam-card" data-id="${exam.id}"> | |
| <div class="delete-btn" data-delete-id="${exam.id}"> | |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="#fff" style="pointer-events: none;"> | |
| <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"/> | |
| </svg> | |
| </div> | |
| <div class="exam-name">${exam.icon} ${exam.name}</div> | |
| ${content} | |
| <div class="exam-date"> | |
| 📅 ${examDate.toLocaleDateString('en-US', { | |
| weekday: 'long', month: 'long', day: 'numeric', | |
| hour: '2-digit', minute: '2-digit' | |
| })} | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // --- Event Delegation for Delete Buttons --- | |
| elements.examsGrid.addEventListener('click', (event) => { | |
| if(event.target.matches('.delete-btn')) { | |
| const idToDelete = event.target.dataset.deleteId; | |
| deleteExam(idToDelete); | |
| } | |
| }); | |
| // --- App Initialization --- | |
| createParticles(); | |
| elements.examDateInput.min = new Date().toISOString().slice(0, 16); | |
| // Event Listeners for Modal | |
| elements.openModalBtn.addEventListener('click', openModal); | |
| elements.closeModalBtn.addEventListener('click', closeModal); | |
| elements.addExamForm.addEventListener('submit', addExam); | |
| window.addEventListener('click', e => { if (e.target === elements.modal) closeModal(); }); | |
| // Firebase real-time data listener | |
| onValue(examsRef, (snapshot) => { | |
| const data = snapshot.val(); | |
| // Convert Firebase object to an array and store it | |
| exams = data ? Object.entries(data).map(([id, value]) => ({ id, ...value })) : []; | |
| // Do an initial render as soon as data is received | |
| renderExams(); | |
| }, (error) => { | |
| console.error("Firebase read failed:", error); | |
| alert("Could not connect to the database. Check console for errors and verify database rules."); | |
| }); | |
| // Set an interval to update the countdowns every second | |
| // This makes the timers tick without re-fetching from Firebase | |
| setInterval(renderExams, 1000); | |
| </script> | |
| </body> | |
| </html> |