├── favicon.ico ├── close.svg ├── README.md ├── LICENSE ├── app.js ├── styles.css ├── index.html └── ads.js /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thegeorgenikhil/75attendance/HEAD/favicon.ico -------------------------------------------------------------------------------- /close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Attendance75 Calculator 2 | 3 | Born out of my laziness to attend classes, this website calculates the numbers of days you can afford to bunk and still maintain a 75% attendance. Else if its less than 75%, it calculates the number of classes you need to attend to get to 75%. 4 | 5 | Get it here: [attendance75.in](https://attendance75.in) 6 | 7 | ## Contributing 8 | 9 | Like the project? Want to contribute? Create a pull request and I'll review it. 10 | 11 | If you have any suggestions or issues, feel free to open an issue. 12 | 13 | ## License 14 | 15 | Copyright (c) Nikhil George. All rights reserved. Licensed under the MIT License 16 | 17 |
18 | Deployed on 19 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nikhil George 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const percentageSelect = document.getElementById("percentage"); 2 | const presentInput = document.getElementById("present-input"); 3 | const totalInput = document.getElementById("total-input"); 4 | const btn = document.getElementById("btn"); 5 | const outputDiv = document.getElementById("output-div"); 6 | const banner = document.getElementById("banner"); 7 | 8 | btn.addEventListener("click", () => { 9 | let present = parseInt(presentInput.value); 10 | let total = parseInt(totalInput.value); 11 | let percentage = parseInt(percentageSelect.value); 12 | 13 | if (isNaN(present) || isNaN(total) || isNaN(percentage)) { 14 | return (outputDiv.innerText = "Proper values please ¯\\_(ツ)_/¯"); 15 | } 16 | 17 | if (present < 0 || total <= 0 || present > total) { 18 | return (outputDiv.innerText = "Proper values please ¯\\_(ツ)_/¯"); 19 | } 20 | 21 | if (present / total >= percentage / 100) { 22 | const daysAvailableToBunk = daysToBunk(present, total, percentage); 23 | return (outputDiv.innerHTML = daysToBunkText( 24 | daysAvailableToBunk, 25 | present, 26 | total 27 | )); 28 | } 29 | 30 | const attendanceNeeded = reqAttendance(present, total, percentage); 31 | return (outputDiv.innerHTML = daysToAttendClassText( 32 | attendanceNeeded, 33 | present, 34 | total, 35 | percentage 36 | )); 37 | }); 38 | 39 | const reqAttendance = (present, total, percentage) => { 40 | return Math.ceil((percentage * total - 100 * present) / (100 - percentage)); 41 | }; 42 | 43 | const daysToBunk = (present, total, percentage) => { 44 | return Math.floor((100 * present - percentage * total) / percentage); 45 | }; 46 | 47 | const daysToBunkText = (daysAvailableToBunk, present, total) => 48 | `You can bunk for ${daysAvailableToBunk} more days.
Current Attendance: ${present}/${total} -> ${( 49 | (present / total) * 50 | 100 51 | ).toFixed(2)}%
Attendance Then: ${present}/${ 52 | daysAvailableToBunk + total 53 | } -> ${( 54 | (present / (daysAvailableToBunk + total)) * 55 | 100 56 | ).toFixed(2)}%`; 57 | 58 | const daysToAttendClassText = (attendanceNeeded, present, total, percentage) => 59 | `You need to attend ${attendanceNeeded} more classes to attain ${percentage}% attendance
Current Attendance: ${present}/${total} -> ${( 60 | (present / total) * 61 | 100 62 | ).toFixed(2)}%
Attendance Required: ${ 63 | attendanceNeeded + present 64 | }/${attendanceNeeded + total} -> ${( 65 | ((attendanceNeeded + present) / (attendanceNeeded + total)) * 66 | 100 67 | ).toFixed(2)}%`; -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | font-family: "Geist", sans-serif; 10 | } 11 | 12 | strong { 13 | font-size: 1.1rem; 14 | color: black; 15 | font-family: "Geist", sans-serif; 16 | } 17 | 18 | .banner { 19 | background-color: #6e46e7; 20 | color: #fff; 21 | padding: 8px; 22 | text-align: center; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | position: relative; 27 | } 28 | 29 | .banner a { 30 | color: inherit; 31 | text-decoration: underline; 32 | font-weight: 500; 33 | } 34 | 35 | .close-banner-btn { 36 | position: absolute; 37 | right: 0; 38 | margin-right: 1rem; 39 | cursor: pointer; 40 | } 41 | 42 | .navbar { 43 | padding: 2rem 1rem 0 1rem; 44 | } 45 | 46 | .navbar-sub-heading { 47 | color: #666666; 48 | margin-top: 0.6rem; 49 | } 50 | 51 | .divider { 52 | width: 100%; 53 | height: 1px; 54 | background-color: #ececec; 55 | margin: 1rem 0 2rem 0; 56 | } 57 | 58 | .container-centered { 59 | max-width: fit-content; 60 | margin: auto; 61 | } 62 | 63 | .input-container { 64 | display: flex; 65 | align-items: center; 66 | } 67 | 68 | .input-container label { 69 | color: #666; 70 | background-color: #fafafa; 71 | border: 1px solid #eaeaea; 72 | height: 42px; 73 | display: flex; 74 | align-items: center; 75 | padding: 0 1rem; 76 | border-top-left-radius: 3px; 77 | border-bottom-left-radius: 3px; 78 | } 79 | 80 | .input-container input { 81 | border: 1px solid #eaeaea; 82 | height: 42px; 83 | width: 100%; 84 | padding: 0 1rem; 85 | outline: none; 86 | border-top-right-radius: 3px; 87 | border-bottom-right-radius: 3px; 88 | transition: all 0.3s ease; 89 | font-size: 1rem; 90 | } 91 | 92 | .input-container input:focus { 93 | border: 1px solid #888; 94 | } 95 | 96 | .user-input-container { 97 | max-width: 20rem; 98 | display: flex; 99 | flex-direction: column; 100 | gap: 1rem; 101 | } 102 | 103 | .percentage-select-container { 104 | margin-bottom: 1rem; 105 | height: 42px; 106 | max-width: 20rem; 107 | } 108 | 109 | .percentage-select-container label { 110 | width: 100%; 111 | } 112 | 113 | .percentage-select { 114 | height: 42px; 115 | font-size: 1rem; 116 | border: 1px solid #eaeaea; 117 | transition: all 0.3s ease; 118 | background-color: transparent; 119 | border-top-right-radius: 3px; 120 | border-bottom-right-radius: 3px; 121 | padding: 0 0.5rem; 122 | } 123 | 124 | .percentage-select:focus { 125 | outline: none; 126 | } 127 | 128 | .calculate-btn { 129 | height: 42px; 130 | width: 100%; 131 | font-family: inherit; 132 | background-color: #000; 133 | color: #fff; 134 | border-radius: 3px; 135 | outline: none; 136 | border: none; 137 | font-size: 1rem; 138 | transition: all 0.2s ease; 139 | } 140 | 141 | .calculate-btn:hover { 142 | cursor: pointer; 143 | opacity: 0.89; 144 | } 145 | 146 | .output-text { 147 | color: #666666; 148 | text-align: center; 149 | font-size: 1rem; 150 | margin: 1rem; 151 | } 152 | 153 | /* media query for less than 480px */ 154 | @media screen and (max-width: 480px) { 155 | .navbar-sub-heading { 156 | font-size: 0.95rem; 157 | } 158 | 159 | .banner { 160 | font-size: 0.9rem; 161 | } 162 | 163 | .banner p { 164 | max-width: 30ch; 165 | } 166 | } 167 | 168 | /* Ad Component Styles */ 169 | .ad-container { 170 | position: fixed; 171 | overflow: hidden; 172 | z-index: 50; 173 | bottom: 1rem; 174 | left: 1rem; 175 | right: 1rem; 176 | border: 1px solid #eaeaea; 177 | padding: 1rem; 178 | background-color: #fff; 179 | height: 100px; 180 | opacity: 0; 181 | transform: translateY(100%); 182 | transition: transform 0.3s ease, opacity 0.3s ease; 183 | } 184 | 185 | .ad-container.show { 186 | opacity: 1; 187 | transform: translateY(0); 188 | } 189 | 190 | .ad-container.fade-out { 191 | opacity: 0; 192 | transform: translateY(20px); 193 | } 194 | 195 | .ad-icon { 196 | position: absolute; 197 | left: 4px; 198 | top: 1rem; 199 | object-fit: contain; 200 | width: 50px; 201 | height: 50px; 202 | } 203 | 204 | .ad-content { 205 | display: flex; 206 | justify-content: space-between; 207 | padding-left: 8px; 208 | } 209 | 210 | .ad-text { 211 | display: flex; 212 | flex-direction: column; 213 | gap: 0.125rem; 214 | padding-left: 40px; 215 | } 216 | 217 | .ad-title { 218 | display: flex; 219 | gap: 0.5rem; 220 | align-items: center; 221 | font-size: 0.875rem; 222 | font-weight: 500; 223 | } 224 | 225 | .ad-description { 226 | font-size: 0.75rem; 227 | color: #878787; 228 | } 229 | 230 | .ad-close-btn { 231 | position: absolute; 232 | right: 0.375rem; 233 | top: 0.375rem; 234 | color: #878787; 235 | background: none; 236 | border: none; 237 | cursor: pointer; 238 | padding: 4px; 239 | } 240 | 241 | .ad-close-icon { 242 | width: 1rem; 243 | height: 1rem; 244 | } 245 | 246 | /* Media query for desktop */ 247 | @media screen and (min-width: 768px) { 248 | .ad-container { 249 | left: auto; 250 | max-width: 365px; 251 | height: 88px; 252 | } 253 | } 254 | 255 | .heading-container { 256 | display: flex; 257 | align-items: center; 258 | gap: 0.75rem; 259 | } 260 | 261 | .github-link { 262 | color: #666; 263 | transition: color 0.2s ease; 264 | display: flex; 265 | align-items: center; 266 | } 267 | 268 | .github-link:hover { 269 | color: #000; 270 | } 271 | 272 | .github-icon { 273 | width: 1.5rem; 274 | height: 1.5rem; 275 | } 276 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Attendance Percentage Calculator - attendance75 47 | 48 | 49 | 50 | 69 |
70 |
71 |
72 | 73 | 82 |
83 |
84 |
85 | 86 | 87 |
88 |
89 | 90 | 91 |
92 | 93 |
94 |
95 |
96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /ads.js: -------------------------------------------------------------------------------- 1 | let currentAdIndex = 0; 2 | let adContainer = null; 3 | let rotationInterval = null; 4 | 5 | const TRANSITION_DURATION = 300; 6 | 7 | const firebaseConfig = { 8 | apiKey: "AIzaSyBpdvNze3lDLIGjFn9Dx0VFS0BnNaMTpwo", 9 | authDomain: "attendance75-b740c.firebaseapp.com", 10 | projectId: "attendance75-b740c", 11 | storageBucket: "attendance75-b740c.firebasestorage.app", 12 | messagingSenderId: "461722758532", 13 | appId: "1:461722758532:web:25df0747bc2f274d6f41d1", 14 | measurementId: "G-TMZ8FHG55P", 15 | }; 16 | 17 | firebase.initializeApp(firebaseConfig); 18 | const remoteConfig = firebase.remoteConfig(); 19 | 20 | remoteConfig.defaultConfig = { 21 | ads: JSON.stringify([]), 22 | adRotationInterval: 8000, 23 | }; 24 | remoteConfig.settings.minimumFetchIntervalMillis = 1000 * 60 * 60; // 1 Hour 25 | 26 | function preloadImage(url) { 27 | return new Promise((resolve, reject) => { 28 | const img = new Image(); 29 | img.onload = () => resolve(); 30 | img.onerror = () => reject(new Error(`Failed to load image: ${url}`)); 31 | img.src = url; 32 | }); 33 | } 34 | 35 | function createAdHTML(ad) { 36 | return ` 37 |
38 | ${ad.title} icon 39 |
40 | 41 |
42 |
${ad.title}
43 |

${ad.description} ↗

44 |
45 |
46 | ${ 47 | ad.canBeClosed 48 | ? `` 51 | : "" 52 | } 53 |
54 |
55 | `; 56 | } 57 | 58 | function trackAdClick(ad, index) { 59 | if (typeof gtag !== "undefined") { 60 | gtag("event", "ad_click", { 61 | event_category: "Ads", 62 | event_label: ad.id, 63 | ad_id: ad.id, 64 | ad_title: ad.title, 65 | ad_description: ad.description, 66 | ad_link: ad.link, 67 | ad_position: index, // Useful if you track carousel/index 68 | timestamp: new Date().toISOString() 69 | }); 70 | } else { 71 | console.warn("gtag not defined – ad click not tracked"); 72 | } 73 | } 74 | 75 | function setupAdEventHandlers(ad, index) { 76 | const adLink = adContainer.querySelector(".ad-link"); 77 | if (adLink) { 78 | adLink.addEventListener("click", () => trackAdClick(ad, index)); 79 | } 80 | 81 | if (ad.canBeClosed) { 82 | const closeBtn = adContainer.querySelector(".ad-close-btn"); 83 | if (closeBtn) { 84 | closeBtn.addEventListener("click", (e) => { 85 | e.preventDefault(); 86 | const adElement = adContainer.querySelector(".ad-container"); 87 | adElement.classList.add("fade-out"); 88 | setTimeout(() => { 89 | adContainer.remove(); 90 | clearInterval(rotationInterval); 91 | }, TRANSITION_DURATION); 92 | }); 93 | } 94 | } 95 | } 96 | 97 | function renderAd(ad, index) { 98 | preloadImage(ad.imageUrl) 99 | .then(() => { 100 | adContainer.innerHTML = createAdHTML(ad); 101 | setupAdEventHandlers(ad, index); 102 | 103 | const newAdElement = adContainer.querySelector(".ad-container"); 104 | newAdElement.offsetHeight; 105 | requestAnimationFrame(() => newAdElement.classList.add("show")); 106 | }) 107 | .catch((error) => { 108 | console.error("Error preloading ad image:", error); 109 | }); 110 | } 111 | 112 | function showNextAd(adsArray) { 113 | if (!adContainer || adsArray.length === 0) return; 114 | 115 | const currentAdElement = adContainer.querySelector(".ad-container"); 116 | if (currentAdElement) { 117 | currentAdElement.classList.add("fade-out"); 118 | setTimeout(() => { 119 | const ad = adsArray[currentAdIndex]; 120 | renderAd(ad, currentAdIndex); 121 | currentAdIndex = (currentAdIndex + 1) % adsArray.length; 122 | }, TRANSITION_DURATION); 123 | } else { 124 | const ad = adsArray[currentAdIndex]; 125 | renderAd(ad, currentAdIndex); 126 | currentAdIndex = (currentAdIndex + 1) % adsArray.length; 127 | } 128 | } 129 | 130 | function initAds() { 131 | remoteConfig.fetchAndActivate() 132 | .then(() => { 133 | try { 134 | const adsValue = remoteConfig.getValue("ads").asString(); 135 | const adsArray = JSON.parse(adsValue); 136 | const adRotationTime = remoteConfig.getValue("adRotationInterval").asNumber(); 137 | 138 | if (!document.getElementById("ad-container")) { 139 | adContainer = document.createElement("div"); 140 | adContainer.id = "ad-container"; 141 | document.body.appendChild(adContainer); 142 | } else { 143 | adContainer = document.getElementById("ad-container"); 144 | } 145 | 146 | if (adsArray.length > 0) { 147 | preloadImage(adsArray[0].imageUrl) 148 | .then(() => { 149 | showNextAd(adsArray); 150 | rotationInterval = setInterval(() => showNextAd(adsArray), adRotationTime); 151 | }) 152 | .catch((err) => { 153 | console.error("Image preload failed, still rotating:", err); 154 | showNextAd(adsArray); 155 | rotationInterval = setInterval(() => showNextAd(adsArray), adRotationTime); 156 | }); 157 | } else { 158 | console.warn("No ads to show"); 159 | } 160 | } catch (error) { 161 | console.error("Ad config parse error:", error); 162 | } 163 | }) 164 | .catch((error) => { 165 | console.error("Failed to fetch remote config:", error); 166 | }); 167 | } 168 | 169 | document.addEventListener("DOMContentLoaded", initAds); 170 | --------------------------------------------------------------------------------