my personal site
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update privacy policy links and enhance routing for privacy page

- Changed destination URLs in seed-ad-active.json and seed-ad.json to point to the new privacy page.
- Updated footer links in ads-landing-html.ts and main-landing-html.ts to reflect the new privacy URL.
- Enhanced index.ts to handle requests for the privacy page, ensuring proper asset fetching and error handling.

+264 -4
gymtracker/public/docs/images/VT-Gym-Tracker-ios-Icon.png

This is a binary file and will not be displayed.

+240
gymtracker/public/docs/privacy-policy.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Privacy Policy - Gym Tracker</title> 7 + <link rel="icon" type="image/x-icon" href="/favicon/favicon.ico"> 8 + <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"> 9 + <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"> 10 + <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"> 11 + <script> 12 + if (localStorage.getItem('theme') === 'dark' || 13 + (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { 14 + document.documentElement.classList.add('dark-theme'); 15 + } 16 + </script> 17 + <style> 18 + :root { 19 + --bg-color: #ffffff; 20 + --text-color: #333333; 21 + --link-color: #444444; 22 + --link-hover-color: #222222; 23 + --vt-maroon: #861F41; 24 + --vt-maroon-light: #c95a7a; 25 + --vt-burntOrange: #E5751F; 26 + } 27 + .dark-theme { 28 + --bg-color: #1a1a1a; 29 + --text-color: #e0e0e0; 30 + --link-color: #bbbbbb; 31 + --link-hover-color: #ffffff; 32 + } 33 + @media (prefers-color-scheme: dark) { 34 + :root:not(.light-theme):not(.dark-theme) { 35 + --bg-color: #1a1a1a; 36 + --text-color: #e0e0e0; 37 + --link-color: #bbbbbb; 38 + --link-hover-color: #ffffff; 39 + } 40 + } 41 + @media (prefers-color-scheme: light) { 42 + :root:not(.light-theme):not(.dark-theme) { 43 + --bg-color: #ffffff; 44 + --text-color: #333333; 45 + --link-color: #444444; 46 + --link-hover-color: #222222; 47 + } 48 + } 49 + *, *::before, *::after { box-sizing: border-box; } 50 + html, body { 51 + margin: 0; 52 + padding: 0; 53 + background: var(--bg-color); 54 + color: var(--text-color); 55 + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 56 + font-size: 16px; 57 + line-height: 1.6; 58 + } 59 + .container { max-width: 800px; margin: 0 auto; padding: 1rem; } 60 + .visually-hidden { 61 + position: absolute !important; 62 + width: 1px !important; 63 + height: 1px !important; 64 + padding: 0 !important; 65 + margin: -1px !important; 66 + overflow: hidden !important; 67 + clip: rect(0,0,0,0) !important; 68 + white-space: nowrap !important; 69 + border: 0 !important; 70 + } 71 + .privacy-policy { 72 + max-width: 800px; 73 + margin: 0 auto; 74 + line-height: 1.7; 75 + padding: 2rem 1rem; 76 + letter-spacing: 0.01em; 77 + } 78 + .privacy-policy h1 { 79 + font-size: clamp(2rem, 5vw, 2.5rem); 80 + margin: 0 0 1.5rem 0; 81 + color: var(--text-color); 82 + font-weight: 700; 83 + letter-spacing: -0.02em; 84 + } 85 + .privacy-policy h2 { 86 + font-size: clamp(1.4rem, 4vw, 1.8rem); 87 + margin: 2rem 0 1rem 0; 88 + color: var(--text-color); 89 + font-weight: 600; 90 + letter-spacing: -0.01em; 91 + } 92 + .privacy-policy p { 93 + margin-bottom: 1.2rem; 94 + color: var(--text-color); 95 + font-size: clamp(1rem, 2.5vw, 1.1rem); 96 + } 97 + .privacy-policy a { 98 + color: var(--vt-maroon-light); 99 + text-decoration: none; 100 + transition: color 0.2s ease; 101 + } 102 + .privacy-policy a:hover { color: var(--vt-burntOrange); } 103 + .app-icon-container { 104 + display: flex; 105 + justify-content: flex-start; 106 + margin-bottom: 1rem; 107 + padding: 0.5rem 0; 108 + } 109 + .app-icon { 110 + width: clamp(100px, 8vw, 140px); 111 + height: clamp(100px, 8vw, 140px); 112 + border-radius: clamp(20px, 2vw, 28px); 113 + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); 114 + object-fit: cover; 115 + border: 2px solid rgba(255, 255, 255, 0.1); 116 + } 117 + @media (max-width: 480px) { 118 + .app-icon { width: clamp(70px, 12vw, 90px); height: clamp(70px, 12vw, 90px); border-radius: clamp(16px, 3vw, 20px); } 119 + .app-icon-container { margin-bottom: 0.75rem; padding: 0.25rem 0; } 120 + .privacy-policy h1 { font-size: clamp(1.6em, 5vw, 2em); margin: 0 0 0.8rem 0; } 121 + .privacy-policy h2 { font-size: clamp(1.3em, 4vw, 1.6em); margin: 1.2rem 0 0.6rem 0; } 122 + .privacy-policy p { font-size: clamp(0.9em, 3.5vw, 1em); margin-bottom: 1rem; line-height: 1.6; } 123 + .privacy-policy { padding: 1rem; margin: 0.5rem; } 124 + } 125 + .social-icons { 126 + display: flex; 127 + justify-content: center; 128 + align-items: center; 129 + gap: 1rem; 130 + margin: 2rem 0; 131 + padding: 1rem 0; 132 + } 133 + .social-icons a { 134 + display: flex; 135 + align-items: center; 136 + justify-content: center; 137 + width: 3rem; 138 + height: 3rem; 139 + border-radius: 50%; 140 + background: transparent; 141 + color: var(--text-color); 142 + text-decoration: none; 143 + transition: all 0.2s ease; 144 + min-width: 44px; 145 + min-height: 44px; 146 + } 147 + .social-icons a:hover, 148 + .social-icons a:focus { 149 + background: transparent; 150 + color: var(--link-hover-color); 151 + transform: translateY(-2px); 152 + } 153 + .social-icons .linkedin-icon, 154 + .social-icons .github-icon, 155 + .social-icons .bluesky-icon, 156 + .social-icons .website-icon { 157 + width: 1.4rem; 158 + height: 1.4rem; 159 + fill: currentColor; 160 + } 161 + @media (max-width: 650px) { 162 + .social-icons { margin: 1.5rem 0; padding: 0.5rem 0; gap: 1rem; } 163 + .social-icons a { width: 3.5rem; height: 3.5rem; min-width: 48px; min-height: 48px; } 164 + .social-icons .linkedin-icon, 165 + .social-icons .github-icon, 166 + .social-icons .bluesky-icon, 167 + .social-icons .website-icon { width: 1.6rem; height: 1.6rem; } 168 + } 169 + </style> 170 + </head> 171 + <body> 172 + <main class="container" id="main-content"> 173 + <div id="a11y-status" class="visually-hidden" aria-live="polite" aria-atomic="true"></div> 174 + 175 + <section class="privacy-policy"> 176 + <div class="app-icon-container"> 177 + <img src="/docs/images/VT-Gym-Tracker-ios-Icon.png" 178 + alt="Gym Tracker App Icon" 179 + class="app-icon" 180 + width="140" 181 + height="140" 182 + loading="eager" 183 + decoding="async" /> 184 + </div> 185 + <h1>Privacy Policy</h1> 186 + <p><strong>Last Updated:</strong> March 22, 2026</p> 187 + 188 + <p>Jack Hannon [I, Me, My] built Gym Tracker (the "App") as a free, non-commercial application. The App and any additional services provided now or in the future are provided "as is." This Privacy Policy outlines how I handle your information when you use my mobile application, Gym Tracker. By accessing or using the App, you agree to these privacy terms.</p> 189 + 190 + <h2>Information Collected</h2> 191 + <p>Gym Tracker stores campus ID details locally on your device when you choose to save them. Gym occupancy and event content are fetched from external services and displayed in-app.</p> 192 + 193 + <h2>Sponsored Content Analytics</h2> 194 + <p>Gym Tracker may show clearly labeled sponsored content from local businesses. To measure sponsor performance, the app records first-party analytics events for ad impressions and ad taps. These events may include ad ID, sponsor name, placement, destination host, campaign creative version, and a random per-app-session identifier.</p> 195 + <p>Gym Tracker does not use this sponsored-content analytics flow for cross-app tracking or personalized third-party ad profiling.</p> 196 + 197 + <h2>Analytics Processor</h2> 198 + <p>Gym Tracker uses PostHog to process product analytics events for operational reporting, including sponsored content performance metrics such as impressions, taps, and click-through rate (CTR).</p> 199 + 200 + <h2>Data Retention</h2> 201 + <p>Sponsored-content analytics events are retained for up to 12 months for campaign reporting and trend analysis, after which they are deleted or aggregated.</p> 202 + 203 + <h2>Third-Party Services</h2> 204 + <p>The App connects to the Virginia Tech Recreational Sports website ("VT Rec Sports") and uses PostHog for app analytics processing. Those providers may process data as described in their own policies. I encourage you to review their policies to understand how your information is handled when interacting with these services through the App.</p> 205 + 206 + <h2>Links to Third-Party Websites</h2> 207 + <p>The App may contain links to third-party websites or services that I don't operate. Once you leave the App, I have no control over these external sites and cannot be responsible for their privacy practices. Please review the privacy policies of any third-party sites you visit.</p> 208 + 209 + <h2>Changes to This Privacy Policy</h2> 210 + <p>I may update this Privacy Policy periodically. Any changes will be posted on this page with an updated "Last Updated" date.</p> 211 + 212 + <h2>Contact</h2> 213 + <p>If you have any questions or suggestions about this Privacy Policy, please contact me at <a href="mailto:gymtracker@jackhannon.net">gymtracker@jackhannon.net</a></p> 214 + </section> 215 + </main> 216 + 217 + <div class="social-icons"> 218 + <a href="https://jackhannon.net" target="_blank" rel="noopener noreferrer" aria-label="Personal Website"> 219 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" class="website-icon"> 220 + <path fill="currentColor" d="M16.5 24c0 1.9.085 3.742.243 5.5h14.514c.158-1.758.243-3.6.243-5.5s-.085-3.742-.244-5.5H16.745c-.16 1.758-.245 3.6-.245 5.5m-2.767-5.5A64 64 0 0 0 13.5 24c0 1.886.08 3.727.232 5.5H2.177C1.735 27.74 1.5 25.897 1.5 24s.235-3.74.677-5.5zm3.366-3H30.9c-.444-3.027-1.116-5.726-1.949-7.943c-.779-2.073-1.669-3.648-2.58-4.676C25.458 1.849 24.652 1.5 24 1.5s-1.458.35-2.372 1.38c-.911 1.03-1.801 2.604-2.58 4.677c-.833 2.217-1.505 4.916-1.95 7.943m17.169 3c.153 1.773.233 3.614.233 5.5s-.08 3.727-.232 5.5h11.555c.442-1.76.677-3.603.677-5.5s-.235-3.74-.677-5.5zm10.573-3H33.931c-.47-3.388-1.214-6.45-2.171-8.998c-.611-1.626-1.323-3.082-2.134-4.293c6.92 1.782 12.55 6.773 15.212 13.291m-30.77 0H3.161C5.822 8.982 11.453 3.991 18.373 2.21c-.81 1.21-1.523 2.666-2.134 4.292c-.957 2.548-1.7 5.61-2.17 8.998m-.003 17H3.161c2.66 6.515 8.286 11.504 15.2 13.288c-.81-1.21-1.52-2.666-2.13-4.29c-.955-2.55-1.697-5.61-2.165-8.998m14.894 7.944c.83-2.217 1.5-4.916 1.944-7.944H17.096c.443 3.028 1.113 5.727 1.944 7.944c.778 2.073 1.667 3.647 2.58 4.675c.912 1.03 1.72 1.381 2.38 1.381s1.468-.351 2.38-1.381c.913-1.028 1.802-2.602 2.58-4.675m2.809 1.053c.955-2.548 1.697-5.61 2.165-8.997h10.905c-2.66 6.515-8.286 11.504-15.2 13.288c.81-1.21 1.52-2.666 2.13-4.29"/> 221 + </svg> 222 + </a> 223 + <a href="https://www.linkedin.com/in/jackphannon/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn Profile"> 224 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" class="linkedin-icon"> 225 + <path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/> 226 + </svg> 227 + </a> 228 + <a href="https://github.com/Hann8n" target="_blank" rel="noopener noreferrer" aria-label="GitHub Profile"> 229 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512" class="github-icon"> 230 + <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/> 231 + </svg> 232 + </a> 233 + <a href="https://bsky.app/profile/did:plc:tjio2pnbsuc6ps77kocywwmc" target="_blank" rel="noopener noreferrer" aria-label="Bluesky Profile"> 234 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="bluesky-icon"> 235 + <path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"/> 236 + </svg> 237 + </a> 238 + </div> 239 + </body> 240 + </html>
+1 -1
gymtracker/seed-ad-active.json
··· 5 5 "headline": "Test ad for end-to-end verification.", 6 6 "subline": "Use this to confirm the app displays ads correctly.", 7 7 "cta": "Learn more", 8 - "destination_url": "https://gymtracker.jackhannon.net/docs/privacy-policy.html", 8 + "destination_url": "https://gymtracker.jackhannon.net/privacy", 9 9 "active": true, 10 10 "placement": "home_feed", 11 11 "creative_version": "1"
+1 -1
gymtracker/seed-ad.json
··· 5 5 "headline": "Configure your first sponsor ad in the admin.", 6 6 "subline": "Manage ads at gymtracker.jackhannon.net/admin", 7 7 "cta": "Get started", 8 - "destination_url": "https://gymtracker.jackhannon.net/docs/privacy-policy.html", 8 + "destination_url": "https://gymtracker.jackhannon.net/privacy", 9 9 "active": false, 10 10 "placement": "home_feed", 11 11 "creative_version": ""
+1 -1
gymtracker/src/ads-landing-html.ts
··· 1373 1373 <div class="container footer-inner"> 1374 1374 <span class="footer-copy">&copy; Jack Hannon</span> 1375 1375 <div class="footer-links"> 1376 - <a href="https://gymtracker.jackhannon.net/docs/privacy-policy.html">Privacy</a> 1376 + <a href="/privacy">Privacy</a> 1377 1377 <a href="https://jackhannon.net/">jackhannon.net</a> 1378 1378 </div> 1379 1379 </div>
+20
gymtracker/src/index.ts
··· 494 494 }); 495 495 } 496 496 497 + if (url.pathname === "/privacy" || url.pathname === "/privacy/") { 498 + const assetRequest = new Request( 499 + new URL("/docs/privacy-policy.html", url.origin).href, 500 + request 501 + ); 502 + const res = await env.ASSETS.fetch(assetRequest); 503 + if (res.status === 404) { 504 + return jsonResponse({ error: "Not found" }, 404, request); 505 + } 506 + return new Response(res.body, { 507 + status: res.status, 508 + headers: { 509 + "Content-Type": "text/html; charset=utf-8", 510 + "Cache-Control": isLocalRequest(request) 511 + ? "no-store, no-cache, must-revalidate" 512 + : "public, max-age=3600", 513 + }, 514 + }); 515 + } 516 + 497 517 if (url.pathname === "/ads" || url.pathname === "/ads/") { 498 518 return new Response(getAdsLandingHtml(), { 499 519 headers: {
+1 -1
gymtracker/src/main-landing-html.ts
··· 114 114 <a href="/ads">Advertise</a> 115 115 <a href="https://tangled.org/jack.orbyt.video/VTGymTracker" target="_blank" rel="noopener noreferrer">Tangled</a> 116 116 <a href="https://github.com/Hann8n/VTGymTracker" target="_blank" rel="noopener noreferrer">GitHub</a> 117 - <a href="/docs/privacy-policy.html">Privacy</a> 117 + <a href="/privacy">Privacy</a> 118 118 <a href="https://jackhannon.net" target="_blank" rel="noopener noreferrer">jackhannon.net</a> 119 119 </footer> 120 120 </body>