The bingo card ran inside the asxpio container, dispatched by Host. Give the public site its own repo, image and deploy so it can be released and mirrored independently of asxp.io. Editing stays in the asxp.io admin, which keeps owning the schema and the photo pipeline; this app only reads the shared bingo tables and the bingo/ photos, through a read-only DB role and MinIO user. Tests load a snapshot of the asxpio-built schema (test/schema.sql). Assisted-by: Claude:opus-5
117 lines
5.1 KiB
Text
117 lines
5.1 KiB
Text
<% b = @board %>
|
|
<main>
|
|
<header>
|
|
<h1><%= t(:lead) %> <span><%= t(:accent) %></span> <small><%= t(:tagline) %></small></h1>
|
|
<div class="switches">
|
|
<nav class="toggle" aria-label="<%= t(:view_label) %>">
|
|
<% Bingo::VIEWS.each do |v| %>
|
|
<a href="<%= card_path(@locale, v) %>" <%== 'aria-current="page"' if v == @view %>><%= t(:"view_#{v}") %></a>
|
|
<% end %>
|
|
</nav>
|
|
<nav class="langs">
|
|
<% Bingo::LOCALES.each do |l| %>
|
|
<a href="<%= card_path(l) %>" hreflang="<%= l %>" lang="<%= l %>" <%== 'aria-current="page"' if l == @locale %>><%= l %></a>
|
|
<% end %>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<p class="lead"><%= t(:desc) %></p>
|
|
<% provs = Bingo::Board.providers(@view) %>
|
|
<form class="providers" method="get" action="/<%= @locale %>">
|
|
<% if @view == 'all' %><input type="hidden" name="view" value="all"><% end %>
|
|
<fieldset>
|
|
<legend><%= t(:providers_label) %></legend>
|
|
<% provs.each do |slug, total, crossed| %>
|
|
<label><input type="checkbox" name="p[]" value="<%= slug %>" <%= 'checked' if @providers.include?(slug) %>> <%= Bingo.provider_name(slug, @locale) %> <span><%= crossed %>/<%= total %></span></label>
|
|
<% end %>
|
|
</fieldset>
|
|
<div class="provider-actions">
|
|
<button type="submit"><%= t(:providers_apply) %></button>
|
|
<% if @board.filtered? %><a href="<%= card_path(@locale, @view, []) %>"><%= t(:providers_reset) %></a><% end %>
|
|
</div>
|
|
</form>
|
|
<p class="meta">
|
|
<%= t(:crossed) %> <b><%= b.hit.count(true) %></b>/<%= b.cells.size %>
|
|
· <%= t(:lines) %> <b><%= b.wins.size %></b>
|
|
<% if b.updated_on %>· <%= t(:updated) %> <b><%= fmt_date(b.updated_on) %></b><% end %>
|
|
</p>
|
|
|
|
<% if b.wins.any? %>
|
|
<div class="banner">
|
|
<%= b.wins.size == Bingo::LINES.size ? t(:full) : b.wins.size > 1 ? format(t(:bingo_n), b.wins.size) : t(:bingo) %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="letters" aria-hidden="true"><% t(:letters).each do |ch| %><div><%= ch %></div><% end %></div>
|
|
<div class="grid">
|
|
<% b.cells.each_with_index do |dc, idx| %>
|
|
<% photo = photo_path(dc, 480) %>
|
|
<a href="<%= dc_path(dc) %>" data-dlg="dlg-<%= dc.slug %>"
|
|
class="cell<%= ' photo' if photo %><%= ' hit' if b.hit[idx] %><%= ' win' if b.win_cells.include?(idx) %>"
|
|
aria-label="<%= dc.l(:name, @locale) %><%= " — #{kind_label(dc.incidents.first.kind)}" if b.hit[idx] %>">
|
|
<% if photo %><img class="bg" src="<%= photo %>" alt="" width="480" height="480" decoding="async"<%== ' loading="lazy"' if idx >= 10 %>><% end %>
|
|
<div>
|
|
<div class="n">#<%= idx + 1 %><%= " · #{mw(dc, tag: false)}" if mw(dc) %></div>
|
|
<% if status_label(dc) %><div class="tag"><%= status_label(dc) %></div><% end %>
|
|
</div>
|
|
<div>
|
|
<div class="name"><%= dc.l(:name, @locale) %></div>
|
|
<div class="sub"><%= dc.l(:city, @locale) %></div>
|
|
</div>
|
|
<% if b.hit[idx] %><div class="stamp"><%= kind_icon(dc.incidents.first.kind) %></div><% end %>
|
|
</a>
|
|
<% end %>
|
|
<% (b.cells.size...Bingo::SIZE**2).each do %><div class="cell empty" aria-hidden="true"></div><% end %>
|
|
</div>
|
|
|
|
<section>
|
|
<h2><%= t(:log) %></h2>
|
|
<ul class="log">
|
|
<% if b.incidents.empty? %>
|
|
<li class="empty"><%= t(:empty) %></li>
|
|
<% end %>
|
|
<% b.incidents.each do |i| %>
|
|
<li>
|
|
<time datetime="<%= i.occurred_on %>"><%= fmt_date(i.occurred_on) %></time>
|
|
<span><%= kind_icon(i.kind) %></span>
|
|
<div>
|
|
<% if i.url %><a href="<%= i.url %>" target="_blank" rel="noopener nofollow"><%= i.l(:title, @locale) %></a><% else %><%= i.l(:title, @locale) %><% end %>
|
|
<div class="dc"><a href="<%= dc_path(i.datacenter) %>"><%= i.datacenter.l(:name, @locale) %></a> · <%= kind_label(i.kind) %></div>
|
|
</div>
|
|
</li>
|
|
<% end %>
|
|
</ul>
|
|
</section>
|
|
|
|
<footer>
|
|
<p><%= t(:footer) %></p>
|
|
<%== erb :_credits, layout: false %>
|
|
<p>© <%= Date.today.year %> <a href="https://asxp.io">asxp.io</a></p>
|
|
</footer>
|
|
</main>
|
|
|
|
<% b.cells.each do |dc| %>
|
|
<dialog id="dlg-<%= dc.slug %>">
|
|
<% if (photo = photo_path(dc, 1200)) %><img src="<%= photo %>" alt="<%= dc.l(:name, @locale) %>, <%= dc.l(:city, @locale) %>" loading="lazy" decoding="async"><% end %>
|
|
<div class="body">
|
|
<h3><%= dc.l(:name, @locale) %></h3>
|
|
<%== erb :_dc_meta, layout: false, locals: { dc: dc } %>
|
|
<%== erb :_dc_incidents, layout: false, locals: { dc: dc } %>
|
|
<div class="actions">
|
|
<a href="<%= dc_path(dc) %>"><%= t(:dc_page) %></a>
|
|
<form method="dialog"><button><%= t(:close) %></button></form>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
<% end %>
|
|
|
|
<script>
|
|
// Cells are links to the datacenter pages; a plain click opens the dialog
|
|
// instead, modified clicks (new tab etc.) keep the link.
|
|
document.querySelectorAll('[data-dlg]').forEach(a => a.addEventListener('click', e => {
|
|
if (e.button || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
e.preventDefault();
|
|
document.getElementById(a.dataset.dlg).showModal();
|
|
}));
|
|
</script>
|