PageSourceSearch

https://myyaak.com/fill-that-hole

html myyaak.com collected 2026-10-04 08:24:24 UTC 39,087 bytes, 626 lines download raw bytes

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:'&copy; 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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:'&copy; 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.