1<!DOCTYPE html> 2<html lang="en-AU"> 3<head>
3<script src="/custom/js/auth-guard.js?v=3"></script>
3 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>Fill This Hole â myyaak</title> 7<link rel="preconnect" href="https://fonts.googleapis.com"> 8<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 9<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> 10<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css">
11<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
vendor: 1 bytes, line 11
11
12<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.heat/0.2.0/leaflet-heat.js"></script>
12 13<style> 14 :root{ 15 --purple:#6d28d9; --purple-dark:#5b21b6; --purple-light:#f5f3ff; 16 --ink:#1a1a2e; --muted:#6b7280; --line:#e5e7eb; --bg:#fff; --bg-soft:#f9fafb; 17 --amber:#d97706; --amber-bg:#fef3c7; --blue:#2563eb; --blue-bg:#dbeafe; 18 --green:#059669; --green-bg:#d1fae5; --danger:#dc2626; 19 --radius:12px; --shadow:0 10px 30px rgba(109,40,217,0.08); 20 } 21 *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;} 22 html{scroll-behavior:smooth;} 23 body{font-family:'Inter',system-ui,-apple-system,sans-serif;color:var(--ink);line-height:1.6;background:var(--bg);-webkit-font-smoothing:antialiased;} 24 a{text-decoration:none;color:inherit;} 25 img{max-width:100%;height:auto;display:block;} 26 button{font-family:inherit;cursor:pointer;} 27 .wrap{max-width:1180px;margin:0 auto;padding:0 24px;} 28 29 .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 22px;border-radius:8px;font-weight:600;font-size:14px;border:2px solid transparent;transition:all .2s ease;white-space:nowrap;} 30 .btn-primary{background:var(--purple);color:#fff;border-color:var(--purple);} 31 .btn-primary:hover{background:var(--purple-dark);border-color:var(--purple-dark);} 32 .btn-outline{background:transparent;color:var(--purple);border-color:var(--purple);} 33 .btn-outline:hover{background:var(--purple);color:#fff;} 34 .btn-block{width:100%;padding:13px 22px;font-size:15px;} 35 .btn:disabled{opacity:.5;cursor:not-allowed;} 36 37 /* HEADER */ 38 header.site{position:sticky;top:0;z-index:1000;background:#fff;border-bottom:1px solid #f0f0f0;height:64px;} 39 .nav-inner{max-width:1180px;margin:0 auto;padding:0 24px;display:flex;align-items:center;justify-content:space-between;height:64px;} 40 .logo-img{height:34px;width:auto;object-fit:contain;} 41 nav.links{display:flex;gap:6px;} 42 nav.links a{padding:8px 14px;border-radius:6px;font-size:14px;font-weight:500;color:var(--muted);transition:all .2s;} 43 nav.links a:hover,nav.links a.active{color:var(--purple);background:var(--purple-light);} 44 .header-right{display:flex;align-items:center;gap:10px;} 45 46 /* HERO */ 47 .hero{background:linear-gradient(180deg,#faf9ff 0%,#fff 100%);padding:64px 0 56px;border-bottom:1px solid #f2f0fb;} 48 .hero-grid{display:grid;grid-template-columns:1.15fr .85fr;gap:44px;align-items:center;} 49 .eyebrow{display:inline-flex;align-items:center;gap:8px;background:var(--purple-light);color:var(--purple);font-size:13px;font-weight:600;padding:6px 14px;border-radius:20px;margin-bottom:18px;} 50 .hero h1{font-size:46px;line-height:1.08;font-weight:800;letter-spacing:-.02em;} 51 .hero h1 em{font-style:normal;color:var(--purple);} 52 .hero p.lead{font-size:17px;color:var(--muted);max-width:470px;margin:18px 0 28px;} 53 .hero-actions{display:flex;gap:12px;flex-wrap:wrap;} 54 .hero-stats{display:flex;gap:30px;margin-top:36px;} 55 .hero-stats b{display:block;font-size:26px;font-weight:800;color:var(--purple);} 56 .hero-stats span{font-size:12.5px;color:var(--muted);} 57 .hero-visual{display:flex;align-items:center;justify-content:center;} 58 .pin-hero{width:190px;height:190px;filter:drop-shadow(0 18px 26px rgba(109,40,217,0.25));animation:float 4s ease-in-out infinite;} 59 @keyframes float{0%,100%{transform:translateY(0);}50%{transform:translateY(-12px);}} 60 61 section{padding:56px 0;} 62 .section-head{margin-bottom:28px;max-width:640px;} 63 .section-head .tag{font-size:12.5px;color:var(--purple);font-weight:700;letter-spacing:.04em;text-transform:uppercase;display:block;margin-bottom:8px;} 64 .section-head h2{font-size:30px;font-weight:800;letter-spacing:-.01em;} 65 .section-head p{color:var(--muted);margin-top:8px;font-size:15px;} 66 67 /* REPORT */ 68 .report-shell{background:#fff;border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow);overflow:hidden;} 69 .report-grid{display:grid;grid-template-columns:1fr 1fr;} 70 .report-left{padding:32px;border-right:1px solid var(--line);} 71 .report-right{background:var(--bg-soft);position:relative;} 72 #reportMap{height:100%;min-height:480px;width:100%;} 73 .field-label{font-size:12.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink);font-weight:700;margin-bottom:8px;display:block;} 74 .upload-box{border:2px dashed #d6cdf0;border-radius:12px;padding:22px;text-align:center;cursor:pointer;transition:all .15s;margin-bottom:22px;position:relative;min-height:150px;display:flex;flex-direction:column;align-items:center;justify-content:center;}
75 .upload-box:hover{border-color:var(--purple);background:var(--purple-light);} 76 .upload-box.has-image{padding:0;border-style:solid;border-color:var(--line);} 77 .upload-preview{display:none;width:100%;height:190px;object-fit:cover;border-radius:10px;} 78 .upload-box.has-image .upload-prompt{display:none;} 79 .upload-box.has-image .upload-preview{display:block;} 80 .upload-box input[type=file]{position:absolute;inset:0;opacity:0;cursor:pointer;} 81 .upload-box.has-image input[type=file]{display:none;} 82 .upload-icon{width:32px;height:32px;color:#a78bfa;margin-bottom:10px;} 83 .upload-box .hint{font-size:12.5px;color:var(--muted);margin-top:4px;} 84 .upload-remove{position:absolute;top:8px;right:8px;background:rgba(26,26,46,.72);color:#fff;border:none;border-radius:6px;width:28px;height:28px;font-size:14px;z-index:2;display:none;} 85 .upload-box.has-image .upload-remove{display:block;} 86 textarea,input[type=text]{width:100%;border:1.5px solid var(--line);border-radius:9px;padding:11px 13px;font-family:inherit;font-size:14px;resize:vertical;color:var(--ink);margin-bottom:22px;background:#fff;} 87 textarea:focus,input[type=text]:focus{outline:none;border-color:var(--purple);} 88 .pin-status{display:flex;align-items:center;gap:9px;font-size:13px;color:var(--muted);background:var(--bg-soft);border:1px solid var(--line);border-radius:8px;padding:10px 12px;margin-bottom:22px;} 89 .pin-status.set{color:var(--green);background:var(--green-bg);border-color:transparent;font-weight:600;} 90 .dot{width:8px;height:8px;border-radius:50%;background:var(--danger);flex-shrink:0;} 91 .dot.on{background:var(--green);} 92 .council-toggle{display:flex;align-items:flex-start;gap:11px;padding:14px 15px;background:var(--bg-soft);border-radius:10px;margin-bottom:16px;cursor:pointer;border:1.5px solid var(--line);} 93 .council-toggle.checked{border-color:var(--purple);background:var(--purple-light);} 94 .council-toggle input{margin-top:3px;accent-color:var(--purple);width:16px;height:16px;flex-shrink:0;} 95 .council-toggle .ct-title{font-weight:700;font-size:13.5px;} 96 .council-toggle .ct-sub{font-size:12.5px;color:var(--muted);margin-top:2px;} 97 .email-preview{display:none;border:1px solid var(--line);border-radius:10px;overflow:hidden;margin-bottom:22px;animation:slideIn .25s ease;} 98 .email-preview.show{display:block;} 99 @keyframes slideIn{from{opacity:0;transform:translateY(-6px);}to{opacity:1;transform:translateY(0);}} 100 .email-head{background:var(--purple);color:#fff;padding:10px 14px;font-size:12px;display:flex;align-items:center;gap:8px;} 101 .email-head b{font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;} 102 .email-body{padding:16px;font-size:13px;background:#fcfbff;} 103 .email-meta{font-size:12px;color:var(--muted);line-height:1.9;margin-bottom:12px;padding-bottom:12px;border-bottom:1px dashed var(--line);} 104 .email-meta b{color:var(--ink);} 105 .email-content{white-space:pre-line;line-height:1.7;font-size:13px;color:var(--ink);} 106 .email-content .fillname{background:var(--purple-light);padding:1px 4px;border-radius:3px;color:var(--purple);font-weight:600;} 107 108 /* FEED */ 109 .feed-header{display:flex;align-items:center;gap:16px;background:linear-gradient(135deg,var(--purple),#8b5cf6);color:#fff;border-radius:16px;padding:24px 26px;margin-bottom:26px;} 110 .feed-avatar{width:54px;height:54px;border-radius:14px;flex-shrink:0;background:rgba(255,255,255,.18);display:flex;align-items:center;justify-content:center;} 111 .feed-header h3{font-size:20px;font-weight:800;} 112 .feed-header .sub{color:rgba(255,255,255,.85);font-size:13px;margin-top:2px;} 113 .feed-header .followers{margin-left:auto;text-align:right;} 114 .feed-header .followers b{font-size:20px;font-weight:800;display:block;} 115 .feed-header .followers span{font-size:11px;color:rgba(255,255,255,.8);text-transform:uppercase;} 116 .feed-list{display:flex;flex-direction:column;gap:18px;} 117 .post{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden;box-shadow:0 4px 14px rgba(26,26,46,.04);} 118 .post-top{display:flex;align-items:center;gap:11px;padding:15px 16px 12px;} 119 .post-avatar{width:36px;height:36px;border-radius:50%;background:var(--purple);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;flex-shrink:0;} 120 .post-user{font-weight:700;font-size:13.5px;} 121 .post-meta{font-size:12px;color:var(--muted);} 122 .post-badge{margin-left:auto;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.03em;padding:5px 9px;border-radius:20px;background:var(--purple-light);color:var(--purple);display:flex;align-items:center;gap:5px;} 123 .post-img-wrap{position:relative;background:#f3f0fb;} 124 .post-img-wrap img{width:100%;display:block;max-height:340px;object-fit:cover;} 125 .post-pin-tag{position:absolute;
125left:12px;bottom:12px;background:rgba(26,26,46,.82);color:#fff;font-size:11.5px;padding:6px 10px;border-radius:7px;display:flex;align-items:center;gap:6px;} 126 .post-body{padding:14px 16px 6px;font-size:13.5px;} 127 .post-status{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;padding:4px 9px;border-radius:20px;margin:12px 16px 14px;text-transform:uppercase;letter-spacing:.03em;} 128 .post-status.open{background:var(--amber-bg);color:var(--amber);} 129 .post-status.progress{background:var(--blue-bg);color:var(--blue);} 130 .post-status.fixed{background:var(--green-bg);color:var(--green);} 131 .empty-feed{text-align:center;color:var(--muted);font-size:14px;padding:34px;background:#fff;border:1px dashed var(--line);border-radius:14px;} 132 133 /* HEATMAP */ 134 .heatmap-shell{background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden;box-shadow:var(--shadow);} 135 .heatmap-toolbar{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--line);flex-wrap:wrap;gap:12px;}
136 .toggle-group{display:flex;background:var(--bg-soft);border:1px solid var(--line);border-radius:9px;padding:3px;gap:3px;} 137 .toggle-group button{border:none;background:transparent;color:var(--muted);padding:7px 14px;border-radius:7px;font-size:12.5px;font-weight:600;} 138 .toggle-group button.active{background:var(--purple);color:#fff;} 139 .heat-legend{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);} 140 .heat-legend .bar{width:110px;height:8px;border-radius:4px;background:linear-gradient(90deg,#ede9fe,#a78bfa,#8b5cf6,#6d28d9,#4c1d95);} 141 #heatMap{height:480px;width:100%;} 142 .region-chips{display:flex;gap:8px;padding:14px 20px;flex-wrap:wrap;border-top:1px solid var(--line);} 143 .chip{background:var(--bg-soft);color:var(--ink);border:1px solid var(--line);padding:7px 13px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;} 144 .chip:hover{border-color:var(--purple);color:var(--purple);background:var(--purple-light);} 145 .pop{font-family:'Inter',sans-serif;font-size:12.5px;min-width:240px;max-width:270px;} 146 .leaflet-popup-content{margin:12px 14px;} 147 .pop b{font-size:12px;text-transform:uppercase;color:var(--purple);} 148 149 /* MODAL + TOAST */ 150 .modal-backdrop{position:fixed;inset:0;background:rgba(26,26,46,.55);display:none;align-items:center;justify-content:center;z-index:2000;padding:20px;} 151 .modal-backdrop.show{display:flex;} 152 .modal{background:#fff;border-radius:16px;width:100%;max-width:380px;padding:30px;box-shadow:var(--shadow);position:relative;animation:pop .18s ease;} 153 @keyframes pop{from{opacity:0;transform:scale(.96);}to{opacity:1;transform:scale(1);}} 154 .modal-close{position:absolute;top:14px;right:14px;background:var(--bg-soft);border:none;width:28px;height:28px;border-radius:50%;font-size:14px;color:var(--muted);} 155 .modal h3{font-size:22px;font-weight:800;margin-bottom:6px;} 156 .modal p.sub{color:var(--muted);font-size:13px;margin-bottom:22px;} 157 .modal label{font-size:12px;font-weight:700;text-transform:uppercase;color:var(--ink);display:block;margin-bottom:6px;letter-spacing:.03em;} 158 .modal .login-link{display:block;text-align:center;margin-top:14px;font-size:13px;color:var(--muted);} 159 .modal .login-link a{color:var(--purple);font-weight:700;} 160 .toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(20px);background:var(--ink);color:#fff;padding:13px 22px;border-radius:10px;font-size:13.5px;font-weight:600;display:flex;align-items:center;gap:10px;opacity:0;pointer-events:none;transition:all .25s ease;z-index:2200;box-shadow:var(--shadow);} 161 .toast.show{opacity:1;transform:translateX(-50%) translateY(0);} 162 footer{background:var(--bg-soft);color:var(--muted);padding:32px 0;margin-top:16px;border-top:1px solid var(--line);} 163 footer .wrap{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:14px;font-size:12.5px;} 164 165 @media (max-width:880px){ 166 .hero-grid{grid-template-columns:1fr;} 167 .hero h1{font-size:34px;} 168 .hero-visual{display:none;} 169 .report-grid{grid-template-columns:1fr;} 170 .report-left{border-right:none;border-bottom:1px solid var(--line);} 171 #reportMap{min-height:320px;} 172 nav.links{display:none;} 173 } 174 @media (max-width:560px){ 175 .wrap,.nav-inner{padding-left:16px;padding-right:16px;} 176 .hero{padding:40px 0 36px;} 177 .hero h1{font-size:27px;} 178 .hero p.lead{font-size:15px;} 179 .hero-actions{flex-direction:column;align-items:stretch;} 180 .hero-actions .btn{width:100%;} 181 .hero-stats{gap:16px;flex-wrap:wrap;margin-top:26px;} 182 .hero-stats b{font-size:22px;} 183 section{padding:38px 0;} 184 .section-head h2{font-size:23px;} 185 .section-head p{font-size:14px;} 186 .report-left{padding:22px 20px;} 187 #reportMap{min-height:280px;} 188 .logo-img{height:28px;} 189 .header-right .btn{padding:8px 12px;font-size:13px;} 190 .feed-header{flex-wrap:wrap;padding:20px;} 191 .feed-header .followers{margin-left:0;width:100%;text-align:left;margin-top:6px;} 192 #heatMap{height:360px;} 193 .heatmap-toolbar{padding:12px 14px;} 194 .region-chips{padding:12px 14px;} 195 .email-content{font-size:12px;} 196 } 197</style> 198</head> 199<body> 200 201<!-- ================= HEADER ================= --> 202<header class="site"> 203 <div class="nav-inner"> 204 <a class="logo" href="/"><img src="/custom/images/logo.jpg" alt="myyaak" class="logo-img"></a> 205 <nav class="links"> 206 <a href="/">Home</a> 207 <a href="#report" class="active">Fill This Hole</a> 208 <a href="#feed">Community</a> 209 <a href="#heatmap">VIC Heatmap</a> 210 </nav> 211 <div class="header-right"> 212 <a class="btn btn-outline" href="/login">Log in to myyaak</a> 213 </div> 214 </div> 215</header> 216 217<!-- ================= HERO ================= --> 218<section class="hero" id="home"> 219 <div class="wrap hero-grid"> 220 <div>
221 <span class="eyebrow">ð³ï¸ Fill This Hole · a myyaak community page</span> 222 <h1>Spot a pothole?<br><em>Report it in 30 seconds.</em></h1> 223 <p class="lead">Drop a photo and a pin, and we'll route it straight to your local Victorian council â while your post rallies the neighbourhood on myyaak to get it fixed faster.</p> 224 <div class="hero-actions"> 225 <a href="#report" class="btn btn-primary">ð Report a pothole</a> 226 <a href="#heatmap" class="btn btn-outline">View VIC heatmap</a> 227 </div> 228 <div class="hero-stats"> 229 <div><b id="statCount">0</b><span>Potholes reported</span></div> 230 <div><b id="statCouncil">0</b><span>Sent to council</span></div> 231 <div><b id="statFixed">0</b><span>Marked fixed</span></div> 232 </div> 233 </div> 234 <div class="hero-visual"> 235 <svg class="pin-hero" viewBox="0 0 200 220" xmlns="http://www.w3.org/2000/svg"> 236 <path d="M100 12 C58 12 26 44 26 86 C26 140 100 202 100 202 C100 202 174 140 174 86 C174 44 142 12 100 12 Z" fill="#6d28d9"/> 237 <path d="M100 12 C58 12 26 44 26 86 C26 140 100 202 100 202 C100 202 174 140 174 86 C174 44 142 12 100 12 Z" fill="none" stroke="#4c1d95" stroke-width="4" opacity=".4"/> 238 <path d="M 66 84 C 60 62 88 48 108 54 C 128 60 134 84 120 100 C 104 118 74 112 66 84 Z" fill="#1a1a2e"/> 239 <circle cx="86" cy="80" r="4" fill="#4b4b63"/> 240 </svg> 241 </div> 242 </div> 243</section> 244 245<!-- ================= REPORT FLOW ================= --> 246<section id="report"> 247 <div class="wrap"> 248 <div class="section-head"> 249 <span class="tag">01 â Report</span> 250 <h2>Show us the hole.</h2> 251 <p>Upload a photo, drop the pin exactly where it is, and choose whether to notify your local council. Anywhere in Victoria works â try dropping a pin near Ballarat.</p> 252 </div> 253 254 <div class="report-shell"> 255 <div class="report-grid"> 256 <div class="report-left"> 257 258 <label class="field-label">1. Photo of the pothole</label> 259 <div class="upload-box" id="uploadBox"> 260 <input type="file" id="photoInput" accept="image/*"> 261 <div class="upload-prompt" id="uploadPrompt"> 262 <svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 16V4M12 4l-4 4M12 4l4 4"/><path d="M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3"/></svg> 263 <div style="font-weight:600;font-size:13.5px;">Click or drag a photo here</div> 264 <div class="hint">JPG or PNG, road-level shot works best</div> 265 </div> 266 <img class="upload-preview" id="uploadPreview" alt="pothole photo"> 267 <button type="button" class="upload-remove" onclick="removePhoto(event)">â</button> 268 </div> 269 270 <label class="field-label">2. Location pin</label> 271 <div class="pin-status" id="pinStatus"> 272 <span class="dot" id="pinDot"></span> 273 <span id="pinText">No pin dropped yet â click the map on the right â</span> 274 </div> 275 276 <label class="field-label">3. Describe it</label> 277 <textarea id="caption" rows="3" placeholder="e.g. Deep pothole on the left lane, widening after last week's rain. Two cars have already clipped it."></textarea> 278 279 <label class="council-toggle" id="councilToggle"> 280 <input type="checkbox" id="councilCheck" onchange="toggleCouncil()"> 281 <div> 282 <div class="ct-title">ð¨ Also send this to my local council</div> 283 <div class="ct-sub">We'll auto-draft an email to the relevant Victorian council based on your pin and show you a preview before anything is sent.</div> 284 </div> 285 </label> 286 287 <div class="email-preview" id="emailPreview"> 288 <div class="email-head"><b>Council email preview</b><span id="councilNameTag">â City of Ballarat</span></div> 289 <div class="email-body"> 290 <div class="email-meta"> 291 <div><b>To:</b> <span id="emailTo">[email protected]</span></div> 292 <div><b>From:</b> [email protected] (on behalf of you)</div> 293 <div><b>Subject:</b> <span id="emailSubject">Pothole report â Sturt Street precinct, Ballarat</span></div> 294 </div> 295 <div class="email-content" id="emailContent"></div> 296 </div> 297 </div> 298 299 <button class="btn btn-primary btn-block" id="submitReport" onclick="submitReport()">Post to Fill This Hole</button> 300 </div> 301 302 <div class="report-right"> 303 <div id="reportMap"></div> 304 </div> 305 </div> 306 </div> 307 </div> 308</section> 309 310<!-- ================= FEED / COMMUNITY ================= --> 311<section id="feed" style="padding-top:0;"> 312 <div class="wrap"> 313 <div class="section-head">
314 <span class="tag">02 â Community</span> 315 <h2>Fill This Hole, on myyaak</h2> 316 <p>Every report lands here. Log in to myyaak to like, comment, and help push posts up the priority list.</p> 317 </div> 318 319 <div class="feed-header"> 320 <div class="feed-avatar"> 321 <svg width="28" height="28" viewBox="0 0 200 200"><circle cx="100" cy="100" r="80" fill="none" stroke="#fff" stroke-width="14"/><circle cx="100" cy="100" r="30" fill="#fff"/></svg> 322 </div> 323 <div> 324 <h3>Fill This Hole</h3> 325 <div class="sub">Community pothole watch · Victoria, Australia</div> 326 </div> 327 <div class="followers"><b id="followerCount">â</b><span>Reports</span></div> 328 </div> 329 330 <div class="feed-list" id="feedList"></div> 331 </div> 332</section> 333 334<!-- ================= HEATMAP ================= --> 335<section id="heatmap" style="padding-top:0;"> 336 <div class="wrap"> 337 <div class="section-head"> 338 <span class="tag">03 â Statewide view</span> 339 <h2>Where the holes are, across Victoria</h2> 340 <p>Live reports plus sample density across metro Melbourne and regional centres. Switch between the heat view and individual pins.</p> 341 </div> 342 343 <div class="heatmap-shell"> 344 <div class="heatmap-toolbar"> 345 <div class="toggle-group"> 346 <button id="btnHeat" class="active" onclick="setHeatMode('heat')">Heat view</button> 347 <button id="btnPins" onclick="setHeatMode('pins')">Pins view</button> 348 </div> 349 <div class="heat-legend"><span>Fewer</span><div class="bar"></div><span>More reports</span></div> 350 </div> 351 <div id="heatMap"></div> 352 <div class="region-chips"> 353 <span class="chip" onclick="flyTo(-37.8136,144.9631,11)">Melbourne CBD</span> 354 <span class="chip" onclick="flyTo(-37.5622,143.8503,12)">Ballarat</span> 355 <span class="chip" onclick="flyTo(-38.1499,144.3617,12)">Geelong</span> 356 <span class="chip" onclick="flyTo(-36.7570,144.2794,12)">Bendigo</span> 357 <span class="chip" onclick="flyTo(-36.3805,145.3990,12)">Shepparton</span> 358 <span class="chip" onclick="flyTo(-38.2340,146.5340,12)">Traralgon</span> 359 <span class="chip" onclick="flyTo(-37.6,144.7,9)">Whole state</span> 360 </div> 361 </div> 362 </div> 363</section> 364 365<footer> 366 <div class="wrap"> 367 <div>myyaak · Fill This Hole is a community page. Reports are saved and shown across myyaak.</div> 368 <div>Victoria, Australia</div> 369 </div> 370</footer> 371 372<!-- ================= NAME MODAL (guests) ================= --> 373<div class="modal-backdrop" id="nameModal"> 374 <div class="modal"> 375 <button class="modal-close" onclick="closeNameModal()">â</button> 376 <h3>Almost there</h3> 377 <p class="sub">Add your name so neighbours know who flagged it. Your report is saved to Fill This Hole either way.</p> 378 <label>Your name</label> 379 <input type="text" id="guestName" placeholder="e.g. Sam Nguyen"> 380 <button class="btn btn-primary btn-block" onclick="confirmName()">Post my report</button> 381 <span class="login-link">Have an account? <a href="/login">Log in to myyaak</a></span> 382 </div> 383</div> 384 385<div class="toast" id="toast"></div> 386
387<script> 388/* ============ CONFIG ============ */ 389var API_SUBMIT = '/custom/fill-that-hole/report'; 390var API_LIST = '/custom/fill-that-hole/reports'; 391var TOKEN_COOKIE = 'yA0JuFD6n6zkC1token'; 392 393function getCookie(name){ 394 var m=document.cookie.match(new RegExp('(?:^|;\\s*)'+name.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'=([^;]*)')); 395 return m?decodeURIComponent(m[1]):''; 396} 397function authHeaders(){ var h={}; var t=getCookie(TOKEN_COOKIE); if(t) h['Authorization']='Bearer '+t; return h; } 398function isLoggedIn(){ return !!getCookie(TOKEN_COOKIE); } 399 400/* ============ STATE ============ */ 401let reportPin = null; 402let reportPhotoFile = null; 403 404const COUNCILS = [ 405 {name:"City of Ballarat", lat:-37.5622, lng:143.8503, email:"[email protected]", suburb:"Sturt Street precinct, Ballarat"}, 406 {name:"City of Melbourne", lat:-37.8136, lng:144.9631, email:"[email protected]", suburb:"Melbourne CBD"}, 407 {name:"City of Greater Geelong", lat:-38.1499, lng:144.3617, email:"[email protected]", suburb:"Geelong"}, 408 {name:"City of Greater Bendigo", lat:-36.7570, lng:144.2794, email:"[email protected]", suburb:"Bendigo"}, 409 {name:"Greater Shepparton City Council", lat:-36.3805, lng:145.3990, email:"[email protected]", suburb:"Shepparton"}, 410]; 411function nearestCouncil(lat,lng){ 412 let best=COUNCILS[0], bd=Infinity; 413 COUNCILS.forEach(c=>{ const d=Math.hypot(c.lat-lat,c.lng-lng); if(d<bd){bd=d;best=c;} }); 414 return best; 415} 416function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2600); } 417 418/* ============ REPORT MAP ============ */ 419const reportMap = L.map('reportMap', {zoomControl:true}).setView([-37.5622,143.8503], 12); 420L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution:'© OpenStreetMap contributors', maxZoom:18}).addTo(reportMap); 421function potholeDivIcon(size){ 422 size = size || 40; 423 return L.divIcon({className:'', 424 html:`<svg width="${size}" height="${size}" viewBox="0 0 40 52" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 3px 4px rgba(0,0,0,0.3))"> 425 <path d="M20 2 C10 2 3 9 3 19 C3 32 20 50 20 50 C20 50 37 32 37 19 C37 9 30 2 20 2 Z" fill="#6d28d9"/> 426 <path d="M12 18 C10 11 18 8 24 10 C30 12 31 20 26 25 C20 30 14 27 12 18 Z" fill="#1a1a2e"/></svg>`, 427 iconSize:[size,size], iconAnchor:[size/2,size], popupAnchor:[0,-size]}); 428} 429let reportMarker = null; 430reportMap.on('click', function(e){ 431 reportPin = e.latlng; 432 if(reportMarker){ reportMarker.setLatLng(e.latlng); } 433 else { 434 reportMarker = L.marker(e.latlng, {icon:potholeDivIcon(42), draggable:true}).addTo(reportMap); 435 reportMarker.on('dragend', ()=>
435{ reportPin = reportMarker.getLatLng(); refreshPinUI(); }); 436 } 437 refreshPinUI(); 438}); 439function refreshPinUI(){ 440 const dot=document.getElementById('pinDot'), txt=document.getElementById('pinText'), status=document.getElementById('pinStatus'); 441 if(reportPin){ 442 dot.classList.add('on'); status.classList.add('set'); 443 txt.textContent = `Pin set at ${reportPin.lat.toFixed(4)}, ${reportPin.lng.toFixed(4)} â drag to fine-tune`; 444 } else { 445 dot.classList.remove('on'); status.classList.remove('set'); 446 txt.textContent = 'No pin dropped yet â click the map on the right â'; 447 } 448 if(document.getElementById('councilCheck').checked) updateEmailPreview(); 449} 450 451/* ============ PHOTO UPLOAD ============ */ 452document.getElementById('photoInput').addEventListener('change', function(e){ 453 const file = e.target.files[0]; 454 if(!file) return; 455 reportPhotoFile = file; 456 const reader = new FileReader(); 457 reader.onload = ev => { document.getElementById('uploadPreview').src = ev.target.result; document.getElementById('uploadBox').classList.add('has-image'); }; 458 reader.readAsDataURL(file); 459}); 460function removePhoto(e){ 461 if(e){ e.stopPropagation(); e.preventDefault(); } 462 reportPhotoFile = null; 463 document.getElementById('photoInput').value = ''; 464 document.getElementById('uploadBox').classList.remove('has-image'); 465} 466 467/* ============ COUNCIL EMAIL PREVIEW ============ */ 468function toggleCouncil(){ 469 const checked = document.getElementById('councilCheck').checked; 470 document.getElementById('councilToggle').classList.toggle('checked', checked); 471 document.getElementById('emailPreview').classList.toggle('show', checked); 472 if(checked) updateEmailPreview(); 473} 474function updateEmailPreview(){ 475 const pin = reportPin || {lat:-37.5622,lng:143.8503}; 476 const council = nearestCouncil(pin.lat, pin.lng); 477 document.getElementById('councilNameTag').textContent = `â ${council.name}`; 478 document.getElementById('emailTo').textContent = council.email; 479 document.getElementById('emailSubject').textContent = `Pothole report â ${council.suburb}`; 480 document.getElementById('emailContent').innerHTML = 481`Hi ${council.name} team, 482 483I'm reporting a pothole via <span class="fillname">Fill This Hole</span> on myyaak. 484 485Location: ${pin.lat.toFixed(5)}, ${pin.lng.toFixed(5)} (${council.suburb}) 486Description: "${document.getElementById('caption').value.trim() || 'Pothole on road surface, see attached photo.'}" 487 488A photo is attached. This report is also published publicly on the Fill This Hole community page so nearby residents can track its status. 489 490Thanks for looking into it, 491â sent via Fill This Hole à myyaak`; 492} 493document.getElementById('caption').addEventListener('input', ()=>{ if(document.getElementById('councilCheck').checked) updateEmailPreview(); }); 494 495/* ============ SUBMIT REPORT â DB ============ */ 496function submitReport(){ 497 if(!reportPhotoFile){ toast("Add a photo of the pothole first ð·"); return; } 498 if(!reportPin){ toast("Drop a pin on the map to mark the location ð"); return; } 499 if(isLoggedIn()){ doSubmit(null); } else { openNameModal(); } 500} 501function openNameModal(){ document.getElementById('nameModal').classList.add('show'); } 502function closeNameModal(){ document.getElementById('nameModal').classList.remove('show'); } 503function confirmName(){ const name = document.getElementById('guestName').value.trim(); closeNameModal(); doSubmit(name || 'Local resident'); } 504function doSubmit(name){ 505 const btn = document.getElementById('submitReport'); 506 const council = nearestCouncil(reportPin.lat, reportPin.lng); 507 const notify = document.getElementById('councilCheck').checked; 508 const fd = new FormData(); 509 fd.append('photo', reportPhotoFile); 510 fd.append('lat', reportPin.lat); fd.append('lng', reportPin.lng); 511 fd.append('description', document.getElementById('caption').value.trim()); 512 fd.append('notify_council', notify ? 1 : 0); 513 fd.append('suburb', council.suburb); fd.append('council_name', council.name); fd.append('council_email', council.email); 514 if(name) fd.append('name', name); 515 516 btn.disabled = true; btn.textContent = 'Postingâ¦'; 517 fetch(API_SUBMIT, { method:'POST', credentials:'include', headers:authHeaders(), body:fd }) 518 .then(r => r.json().then(j => ({ok:r.ok, j}))) 519 .then(({ok, j}) => { 520 btn.disabled = false; btn.textContent = 'Post to Fill This Hole'; 521 if(!ok || !j || !j.ok){ toast("Couldn't post â please try again"); return; } 522 const p = j.report; 523 feedData.unshift(p); renderFeed(); 524 heatData.push([p.lat, p.lng, 1]); if(heatLayer) heatLayer.setLatLngs(heatData); 525 addPinMarker(p); 526 toast(p.notify_council ? `Posted, and queued to ${p.council_name||'council'} â ` : "Posted to Fill This Hole â "); 527 refreshStats(); 528 removePhoto(); document.getElementById('caption').value=''; 529 document.getElementById('councilCheck').checked=false; toggleCouncil();
530 if(reportMarker){ reportMap.removeLayer(reportMarker); reportMarker=null; } 531 reportPin=null; refreshPinUI(); 532 document.getElementById('guestName').value=''; 533 document.getElementById('feed').scrollIntoView({behavior:'smooth'}); 534 }) 535 .catch(()=>{ btn.disabled=false; btn.textContent='Post to Fill This Hole'; toast("Network error â please try again"); }); 536} 537 538/* ============ FEED ============ */ 539let feedData = []; 540function initials(name){ return (name||'?').split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase(); } 541function statusLabel(s){ return s==='open'?'â Reported':s==='progress'?'ð In progress':'â Fixed'; } 542function escapeHtml(s){ return (s==null?'':String(s)).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } 543function renderFeed(){ 544 const list = document.getElementById('feedList'); 545 if(!feedData.length){ list.innerHTML = '<div class="empty-feed">No potholes reported yet â be the first to flag one above. ð</div>'; return; } 546 list.innerHTML = feedData.map(p=>` 547 <div class="post"> 548 <div class="post-top"> 549 <div class="post-avatar">${initials(p.name)}</div> 550 <div> 551 <div class="post-user">${escapeHtml(p.name)}</div> 552 <div class="post-meta">${escapeHtml(p.time||'Just now')} · ${escapeHtml(p.suburb||'Victoria')}</div> 553 </div> 554 ${p.notify_council && p.council_name ? `<div class="post-badge">ð¨ Sent to ${escapeHtml(p.council_name)}</div>` : ''} 555 </div> 556 ${p.photo ? `<div class="post-img-wrap"><img src="${p.photo}" alt="pothole photo" loading="lazy"><div class="post-pin-tag">ð ${(+p.lat).toFixed(4)}, ${(+p.lng).toFixed(4)}</div></div>` : ''} 557 <span class="post-status ${p.status||'open'}">${statusLabel(p.status||'open')}</span> 558 <div class="post-body">${escapeHtml(p.description||'Pothole spotted â flagging for repair.')}</div> 559 </div>`).join(''); 560} 561function refreshStats(){ 562 document.getElementById('statCount').textContent = feedData.length.toLocaleString(); 563 document.getElementById('statCouncil').textContent = feedData.filter(p=>p.notify_council).length.toLocaleString(); 564 document.getElementById('statFixed').textContent = feedData.filter(p=>p.status==='fixed').length.toLocaleString(); 565 document.getElementById('followerCount').textContent = feedData.length.toLocaleString(); 566} 567 568/* ============ HEATMAP ============ */ 569const heatMap = L.map('heatMap', {zoomControl:true}).setView([-37.6,144.7], 8); 570L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution:'© OpenStreetMap contributors', maxZoom:18}).addTo(heatMap); 571function jitter(lat,lng,n,spread){ const pts=[]; for(let i=0;i<n;i++){ pts.push([lat+(Math.random()-0.5)*spread, lng+(Math.random()-0.5)*spread, Math.random()*0.6+0.4]); } return pts; } 572let heatData = [ 573 ...jitter(-37.8136,144.9631,60,0.16), ...jitter(-37.5622,143.8503,30,0.09), 574 ...jitter(-38.1499,144.3617,24,0.08), ...jitter(-36.7570,144.2794,16,0.07), 575 ...jitter(-36.3805,145.3990,12,0.06), ...jitter(-38.2340,146.5340,12,0.07), 576]; 577let heatLayer = L.heatLayer(heatData, {radius:22, blur:26, maxZoom:12, 578 gradient:{0.2:'#ede9fe',0.4:'#a78bfa',0.6:'#8b5cf6',0.8:'#6d28d9',1:'#4c1d95'}}).addTo(heatMap); 579let pinLayerGroup = L.layerGroup(); 580function addPinMarker(p){ 581 const img = p.photo ? `<img src="${p.photo}" alt="pothole photo" style="width:100%;height:175px;object-fit:cover;border-radius:8px;margin-bottom:8px;display:block;">` : ''; 582 const desc = p.description ? `<div style="margin-top:5px;color:#374151;line-height:1.45;">${escapeHtml(p.description)}</div>` : ''; 583 const m = L.marker([p.lat,p.lng], {icon:potholeDivIcon(34)}); 584 m.bindPopup(`<div class="pop">${img}<b>${statusLabel(p.status||'open')}</b><br>${escapeHtml(p.suburb||'Victoria')}<br><span style="color:#6b7280">reported by ${escapeHtml(p.name||'a resident')}</span>${desc}</div>`); 585 pinLayerGroup.addLayer(m); 586} 587// Sample pins across VIC regional centres so the Pins view isn't empty before 588// real reports arrive (matches the heatmap's sample density). Real report pins 589// are added on top by loadReports() / on submit. 590const pinSeedPosts = [ 591 {lat:-37.5615,lng:143.8496,suburb:"Ballarat",status:"progress",name:"A resident",photo:"/storage/potholes/demo-1.jpg",description:"Deep pothole widening after the rain."}, 592 {lat:-37.8195,lng:144.9631,suburb:"Melbourne CBD",status:"open",name:"A resident",photo:"/storage/potholes/demo-2.jpg",description:"Cracked road surface breaking up near the lights."}, 593 {lat:-38.1499,lng:144.3617,suburb:"Geelong",status:"fixed",name:"A resident",photo:"/storage/potholes/demo-3.jpg",description:"Was patched by council within the week."}, 594 {lat:-36.7570,lng:144.2794,suburb:"Bendigo",status:"open",name:"A resident",photo:"/storage/potholes/demo-4.jpg",description:"Cluster of potholes forming b
594y the kerb."}, 595 {lat:-36.3805,lng:145.3990,suburb:"Shepparton",status:"open",name:"A resident",photo:"/storage/potholes/demo-1.jpg",description:"Sharp edge â already clipped a few tyres."}, 596 {lat:-38.2340,lng:146.5340,suburb:"Traralgon",status:"progress",name:"A resident",photo:"/storage/potholes/demo-2.jpg",description:"Reported, awaiting inspection."}, 597]; 598pinSeedPosts.forEach(addPinMarker); 599function setHeatMode(mode){ 600 document.getElementById('btnHeat').classList.toggle('active', mode==='heat'); 601 document.getElementById('btnPins').classList.toggle('active', mode==='pins'); 602 if(mode==='heat'){ if(heatMap.hasLayer(pinLayerGroup)) heatMap.removeLayer(pinLayerGroup); heatLayer.addTo(heatMap); } 603 else { if(heatMap.hasLayer(heatLayer)) heatMap.removeLayer(heatLayer); pinLayerGroup.addTo(heatMap); } 604} 605function flyTo(lat,lng,zoom){ heatMap.flyTo([lat,lng], zoom, {duration:0.8}); } 606 607/* ============ LOAD REAL REPORTS ============ */ 608function loadReports(){ 609 fetch(API_LIST, { credentials:'include', headers:authHeaders() }) 610 .then(r=>r.json()) 611 .then(res=>{ 612 if(!res || !res.ok || !Array.isArray(res.items)) return; 613 feedData = res.items; renderFeed(); refreshStats(); 614 res.items.forEach(p=>{ heatData.push([p.lat, p.lng, 0.9]); addPinMarker(p); }); 615 if(heatLayer) heatLayer.setLatLngs(heatData); 616 }) 617 .catch(()=>{}); 618} 619renderFeed(); 620loadReports(); 621 622setTimeout(()=>{ reportMap.invalidateSize(); heatMap.invalidateSize(); }, 300); 623window.addEventListener('resize', ()=>{ reportMap.invalidateSize(); heatMap.invalidateSize(); }); 624</script>
624 625<link rel="stylesheet" href="/custom/css/myyaak-override.css?v=32">
625<script src="/custom/js/layout-center.js?v=3"></script>
625<script src="/custom/js/logout-redirect.js?v=4"></script>
625<script src="/custom/js/admin-menu.js?v=4"></script>
625<script src="/custom/js/admin-login-as.js?v=5"></script>
625<script src="/custom/js/impersonation-banner.js?v=3"></script>
625<script src="/custom/js/yaak.js?v=8"></script>
625<script src="/custom/js/feed-banners.js?v=18"></script>
625<script src="/custom/js/bank-widget.js?v=9"></script>
625<script src="/custom/js/claim-business.js?v=5"></script>
625<script src="/custom/js/deposit-menu.js?v=12"></script>
625<script src="/custom/js/default-post-as-page.js?v=9"></script>
625<script src="/custom/js/social-share.js?v=8"></script>
625<script src="/custom/js/admin-yuk-badge.js?v=1"></script>
625<script src="/custom/js/page-info-widget.js?v=13"></script>
625<script src="/custom/js/moderation-alert.js?v=2"></script>
625<script src="/custom/js/interaction-highlight.js?v=11"></script>
625<script src="/custom/js/open-in-app.js?v=1"></script>
625<script src="/custom/js/purple-tick.js?v=9"></script>
625<script src="/custom/js/feed-scope.js?v=3"></script>
625<script src="/birthday-widget.js?v=3"></script>
625</body> 626</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.