Kemp.rest / Examples / Code / Blade screens
KEMP.REST / EXAMPLES / CODE
Blade screens for the grid and the listing
Written by Kevin Kemp in Laravel Blade, HTML, CSS, and JavaScript for AnswerPins. The Google Maps JavaScript API draws the grid. API credentials stay in the environment, not in this code.
View the codeThe scans and the Business Profile API are only useful if an owner can see them. These Blade views are that layer. One draws the rank grid on a Google Map, with a plain grid when no map key is set. The other rebuilds the Google listing the way a customer sees it, and sets the business next to the top three on Maps.
Type of code
Laravel Blade templates. A controller passes the latest scan, the keyword list, and the profile payload. The views do not call DataForSEO or Google.
Technology
HTML, CSS, JavaScript, Blade, and the Google Maps JavaScript API, plus Laravel route helpers. Rank colors are the same in the fallback grid and on the map: green for 1–3, amber for 4–10, orange for 11–20, red when the business is outside the top 20. The map key is a variable, not a key written into the template.
What it does
- If no scan exists yet, the page says the first grid is running, or that it runs within the hour. That matches the scheduler, which queues a new grid at minute 15.
- Keyword tabs keep the same screen and swap the search. First scan and latest scan sit side by side once a second scan exists.
- Each point is a marker labeled with the rank. The map fits the bounds of those points.
- Without a map key, the same points render as a CSS grid in row order, which is north to south because the scanner stored row 0 as the north edge.
- The profile view prints photos, rating, category, hours, website, and phone, then a comparison table. A number that trails the top three is marked. Category ideas come from what those businesses are listed as, and the connect button starts the Google OAuth flow.
Problem it solved
The owner sees where the business ranks across the map, and what the Google listing looks like next to the businesses above it, without opening Google’s own tools.
Code
Back to summary · grids.blade.php, _grid.blade.php, _gbp_profile.blade.php
// —— resources/views/dashboard/grids.blade.php ——
@extends('layouts.app', ['title' => 'Local grid', 'wide' => true])
@section('content')
<div class="dash">
<h1>Local grid</h1>
<p class="lead">Where you show up on Google Maps across your area. Each pin is a real Google Maps search from that spot.</p>
@include('dashboard._nav')
@if (! $latest)
<p class="flash">{{ $pending ? 'Your first grid is running now. It takes a few minutes.' : 'Your first grid runs within the hour.' }}</p>
@else
<nav class="kwtabs" aria-label="Keyword">
@foreach ($keywords as $k)<a href="{{ route('dashboard.grids', ['keyword' => $k]) }}" @if ($k === $keyword) aria-current="page" @endif>{{ $k }}</a>@endforeach
</nav>
<p class="legend"><span class="g g1">1–3</span> <span class="g g2">4–10</span> <span class="g g3">11–20</span> <span class="g g4">20+</span> Your position on Google Maps for "{{ $keyword }}"</p>
<div class="grids {{ $first ? 'two' : '' }}">
@if ($first)@include('dashboard._grid', ['scan' => $first, 'label' => 'First scan'])@endif
@include('dashboard._grid', ['scan' => $latest, 'label' => $first ? 'Latest' : 'Latest scan'])
</div>
@if ($account->plan === 'owner')
<p class="steer"><strong>Pro Managed</strong> checks a bigger 7×7 grid every week, not 5×5 monthly. <a href="{{ config('answerpins.whmcs.url') }}/upgrade.php?type=package&id={{ $account->whmcs_service_id }}">Upgrade</a></p>
@endif
@endif
</div>
@endsection
@if ($latest && $mapsKey)
@push('scripts')
<script>
function apGridMaps() {
var colour = function (r) { return !r ? '#d93025' : r <= 3 ? '#188038' : r <= 10 ? '#f9ab00' : '#e8710a'; };
document.querySelectorAll('.gmap').forEach(function (el) {
var d = JSON.parse(el.getAttribute('data-scan'));
var map = new google.maps.Map(el, { center: { lat: d.lat, lng: d.lng }, zoom: 11, streetViewControl: false, mapTypeControl: false, fullscreenControl: true });
var bounds = new google.maps.LatLngBounds();
d.points.forEach(function (p) {
var pos = { lat: p.lat, lng: p.lng };
bounds.extend(pos);
new google.maps.Marker({ position: pos, map: map, title: p.rank ? 'Rank ' + p.rank : 'Not in the top 20',
label: { text: p.rank ? String(p.rank) : '20+', color: '#fff', fontSize: '12px', fontWeight: '700' },
icon: { path: google.maps.SymbolPath.CIRCLE, scale: 15, fillColor: colour(p.rank), fillOpacity: 1, strokeColor: '#fff', strokeWeight: 2 } });
});
map.fitBounds(bounds, 30);
});
}
</script>
<script src="https://maps.googleapis.com/maps/api/js?key={{ urlencode($mapsKey) }}&callback=apGridMaps" async defer></script>
@endpush
@endif
// —— resources/views/dashboard/_grid.blade.php ——
{{-- One grid scan: a Google Map with a coloured pin per point, or a plain coloured grid when no map key is set. --}}
<figure class="gridbox">
<figcaption><strong>{{ $label }}</strong> · {{ $scan->scanned_at?->format('M j, Y') }} · {{ $scan->size }}×{{ $scan->size }}, {{ rtrim(rtrim(number_format($scan->spacing_miles, 2), '0'), '.') }} mile apart</figcaption>
@if ($mapsKey)
<div class="gmap" data-scan="{{ json_encode(['lat' => $scan->center_lat, 'lng' => $scan->center_lng, 'points' => $scan->points]) }}"></div>
@else
<div class="cgrid" style="grid-template-columns: repeat({{ $scan->size }}, 1fr)">
@foreach ($scan->points as $p)
<span class="g {{ $p['rank'] ? ($p['rank'] <= 3 ? 'g1' : ($p['rank'] <= 10 ? 'g2' : 'g3')) : 'g4' }}">{{ $p['rank'] ?: '20+' }}</span>
@endforeach
</div>
@endif
<p class="gstats">Average rank <strong>{{ $scan->avg_rank >= 21 ? '20+' : number_format($scan->avg_rank, 1) }}</strong> · Top 3 at <strong>{{ $scan->top3 }}</strong> of {{ collect($scan->points)->filter(fn ($p) => ! empty($p['top']) || ! empty($p['rank']))->count() }} spots with businesses · Shown at {{ $scan->found }}</p>
</figure>
// —— resources/views/dashboard/_gbp_profile.blade.php ——
{{-- The Google Business Profile as customers see it on Google, with what's costing customers marked in red, category
fixes taken from what the businesses beating you use, and you against the top 3. --}}
@once
@push('head')
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&display=swap">
@endpush
@endonce
@php($arrow = '<svg viewBox="0 0 96 64" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M90 8c-22-4-50 2-66 20-6 7-9 16-10 28"/><path d="M25 46L14 58 5 43"/></svg>')
@php($you = $gbp['you'])
@php($rivals = collect($gbp['rivals'])->sortBy('rank')->take(3)->values())
@php($leader = $rivals->first())
@php($yourCatCount = 1 + count($you['extra'] ?? []))
@php($leaderCatCount = $leader ? 1 + count($leader['extra'] ?? []) : 0)
@php($hours = collect($listing['hours'] ?? [])->map(fn ($h) => trim(\Illuminate\Support\Str::after($h, ':'))))
@php($sameHours = $hours->count() === 7 && $hours->unique()->count() === 1)
<h3 class="gbpv-h">Your Google Business Profile</h3>
<p class="cx-intro">This is what customers see when they find you on Google. We check it every morning and mark anything that's costing you customers.</p>
<div class="gbpv">
<div>
<div class="gbpv-callout">{!! $arrow !!}<span class="gbpv-note">what customers see</span></div>
<div class="gbpv-gl">
@if ($gbp['photos'])
<div class="gbpv-photos">
@foreach (array_slice($listing['photos'], 0, 3) as $i => $ph)
@if ($i === 2)
<div class="more"><img src="{{ route('dashboard.listing.photo', $i) }}" alt=""><span>{{ $you['photos'] ?? $gbp['photos'] }} photos</span></div>
@else
<img src="{{ route('dashboard.listing.photo', $i) }}" alt="">
@endif
@endforeach
</div>
@endif
<div class="gbpv-body">
<p class="gbpv-name">{{ $listing['name'] }}</p>
@if ($listing['rating'])<p class="gbpv-rate"><b>{{ number_format($listing['rating'], 1) }}</b> <span class="st">★★★★★</span> ({{ number_format((int) $listing['reviews']) }})</p>@endif
<p class="gbpv-cat"><span @class(['flag' => $gbp['catSuggestions']])>{{ $you['category'] ?? 'No category' }}</span>@if (! empty($you['extra'])) · {{ implode(' · ', $you['extra']) }}@endif</p>
<div class="gbpv-btns"><span><i>🌐</i>Website</span><span><i>➜</i>Directions</span><span><i>☎</i>Call</span><span><i>🔖</i>Save</span><span><i>⤴</i>Share</span></div>
@if ($listing['address'])<p class="gbpv-row"><span class="ic">📍</span>{{ preg_replace('/,\s*USA$/', '', $listing['address']) }}</p>@endif
@if ($hours->isNotEmpty())<p class="gbpv-row"><span class="ic">🕘</span><span>@if ($listing['open_now'] !== null)<span class="{{ $listing['open_now'] ? 'open' : 'closed' }}">{{ $listing['open_now'] ? 'Open' : 'Closed' }}</span> · @endif{{ $sameHours ? $hours->first().' every day' : 'Hours set for '.$hours->reject(fn ($h) => $h === 'Closed')->count().' days' }}</span></p>@endif
@if ($listing['website'])<p class="gbpv-row"><span class="ic">🌐</span>{{ preg_replace('#^https?://(www\.)?|/$#', '', $listing['website']) }}</p>@endif
@if ($listing['phone'])<p class="gbpv-row"><span class="ic">☎</span>{{ $listing['phone'] }}</p>@endif
@if (! empty($listing['review_list']))
<div class="gbpv-revs">
<h4>Reviews</h4>
@foreach (array_slice($listing['review_list'], 0, 3) as $r)
<p class="gbpv-rev"><b>{{ $r['author'] }}</b> <span class="st">{{ str_repeat('★', (int) $r['rating']) }}</span> <span class="when">{{ $r['when'] }}</span><br>"{{ \Illuminate\Support\Str::limit($r['text'], 170) }}"</p>
@endforeach
</div>
@endif
@if ($listing['maps_url'])<p class="gbpv-row"><a class="lk" href="{{ $listing['maps_url'] }}" target="_blank" rel="noopener">Open it on Google Maps ↗</a></p>@endif
</div>
</div>
</div>
<div>
@if ($gbp['catSuggestions'])
<div class="gbpv-callout">{!! $arrow !!}<span class="gbpv-note">{{ $yourCatCount }} {{ $yourCatCount === 1 ? 'category' : 'categories' }}.@if ($leader && $leaderCatCount > $yourCatCount) The #1 has {{ $leaderCatCount }}.@endif</span></div>
<div class="gbpv-fix">
<h4>Add categories: Google only shows you for what you're listed as</h4>
<p>You're listed as <strong>{{ implode(', ', array_filter([$you['category'], ...($you['extra'] ?? [])])) }}</strong>.@if ($leader) {{ $leader['name'] }}, #{{ $leader['rank'] }} on Google Maps, is listed as <strong>{{ $leader['category'] }}</strong>{{ count($leader['extra']) ? ' with '.count($leader['extra']).' more' : '' }}.@endif You can have one main category and up to 9 more. Tick the ones that fit you:</p>
<ul class="gbpv-cats">
@foreach ($gbp['catSuggestions'] as $c)
<li><label><input type="checkbox" @checked($loop->index < 2)><span><strong>{{ $c['name'] }}</strong><small>{{ $c['main'] ? 'Main category of '.implode(' and ', $c['main']) : 'Used by '.implode(' and ', $c['by']) }}</small></span></label></li>
@endforeach
</ul>
<a class="btn dark" href="{{ route('dashboard.google.connect') }}">Connect Google to add them</a>
</div>
@endif
@if ($rivals->isNotEmpty())
<h4 class="gbpv-h4">You vs the top 3 on Google Maps</h4>
<div class="gbpv-vs-wrap"><table class="gbpv-vs">
<thead><tr><th></th><th>You</th>@foreach ($rivals as $r)<th>{{ \Illuminate\Support\Str::limit($r['name'], 24) }} #{{ $r['rank'] }}</th>@endforeach</tr></thead>
<tbody>
<tr><th>Main category</th><td class="you">{{ $you['category'] ?? '–' }}</td>@foreach ($rivals as $r)<td>{{ $r['category'] ?? '–' }}</td>@endforeach</tr>
<tr><th>Extra categories</th><td class="you"><span @class(['bad' => count($you['extra'] ?? []) < $rivals->max(fn ($r) => count($r['extra']))])>{{ count($you['extra'] ?? []) }}</span></td>@foreach ($rivals as $r)<td>{{ count($r['extra']) }}</td>@endforeach</tr>
<tr><th>Reviews</th><td class="you"><span @class(['bad' => (int) $you['reviews'] < $rivals->max('reviews')])>{{ number_format((int) $you['reviews']) }}</span></td>@foreach ($rivals as $r)<td>{{ $r['reviews'] !== null ? number_format($r['reviews']) : '–' }}</td>@endforeach</tr>
<tr><th>Rating</th><td class="you">{{ $you['rating'] ? number_format($you['rating'], 1) : '–' }}</td>@foreach ($rivals as $r)<td>{{ $r['rating'] ? number_format($r['rating'], 1) : '–' }}</td>@endforeach</tr>
@if ($you['photos'] !== null)<tr><th>Photos</th><td class="you"><span @class(['bad' => $you['photos'] < $rivals->max('photos')])>{{ number_format($you['photos']) }}</span></td>@foreach ($rivals as $r)<td>{{ $r['photos'] !== null ? number_format($r['photos']) : '–' }}</td>@endforeach</tr>@endif
</tbody>
</table></div>
<p class="cx-hint">From Google Maps, checked monthly.</p>
@endif
</div>
</div>