Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/public/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@
<meta name="twitter:description" content="No street by that name. Cities live at commit.city/&lt;user&gt; or commit.city/&lt;owner&gt;/&lt;repo&gt;.">
<meta name="twitter:image" content="https://commit.city/api/og">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
.nf { min-height: calc(100vh - var(--nav-h) - 200px); display: grid; place-items: center; text-align: center; padding: 64px 0; }
.nf .big { display: inline-flex; font: 700 clamp(72px, 14vw, 140px)/1 var(--mono); letter-spacing: -0.06em; background: linear-gradient(180deg, #e8eef7, #2c4a6e); -webkit-background-clip: text; background-clip: text; color: transparent; border: 0; }
.nf p { color: var(--ink-2); max-width: 520px; margin: 14px auto 26px; }
.nf form.omni { width: min(560px, 100%); margin: 0 auto; text-align: left; }
.lot { margin: 0 auto 8px; width: 180px; }
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body>
<header class="nav">
Expand Down
6 changes: 3 additions & 3 deletions web/public/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<meta name="twitter:description" content="How commit.city turns GitHub history into a 3D city: URLs, reading a city, the agent lens, the live view, sharing, privacy and opt-out, limits, self-hosting for private repos, and the API.">
<meta name="twitter:image" content="https://commit.city/api/og?page=about">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
.docs { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 56px; padding: 8px 0 100px; }
.toc { position: sticky; top: calc(var(--nav-h) + 24px); align-self: start; display: grid; gap: 2px; font-size: 14px; }
Expand Down Expand Up @@ -62,7 +62,7 @@
@media (max-width: 960px) { .docs { grid-template-columns: minmax(0, 1fr); } .toc { display: none; } }
@media (max-width: 640px) { .flow { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } .flow div::after { display: none; } }
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body>
<header class="nav">
Expand Down Expand Up @@ -179,7 +179,7 @@ <h2>Privacy</h2>
<ul>
<li><b>Public data only.</b> The builder reads public repositories with a token that can't see anything private. Private repos never appear on commit.city.</li>
<li><b>No source code is kept.</b> A city stores file paths, line counts and timestamps, all things anyone can already see on GitHub. Repo records are cached so the next visitor doesn't rebuild them.</li>
<li><b>No ads, no trackers, no analytics cookies.</b> A cookie is set only if you sign in, to remember who you are.</li>
<li><b>No ads, no trackers, no analytics cookies.</b> A cookie is set only if you sign in, to remember who you are. The cities you build are listed in your own browser's storage, so the home page and Explore can show them first; that list never leaves your browser.</li>
<li><b>Sign-in asks for no permissions.</b> It proves you own an account, so you can manage its city. Builds you start then use your own GitHub API quota.</li>
</ul>
</section>
Expand Down
4 changes: 2 additions & 2 deletions web/public/events.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<meta name="twitter:description" content="Watch GitHub's public timeline land as a 3D city in real time: every push, pull request, star and release a beam of light, sorted by humans, bots and AI agents.">
<meta name="twitter:image" content="https://commit.city/api/og?page=events">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
html, body { height: 100%; }
body.app { overflow: hidden; }
Expand Down Expand Up @@ -140,7 +140,7 @@
.tools .opt { display: none; }
}
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body class="app">
<header class="nav">
Expand Down
31 changes: 23 additions & 8 deletions web/public/explore.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<meta name="twitter:description" content="Browse every city built on commit.city: people, organisations and repositories, replayed from their git history as 3D city time-lapses. Filter by type, sort by size or by how much of it AI agents wrote.">
<meta name="twitter:image" content="https://commit.city/api/og?page=explore">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
.bar { position: sticky; top: calc(var(--nav-h) + env(safe-area-inset-top, 0px)); z-index: 10; padding: 14px 0; background: rgba(4, 6, 12, 0.82); backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px); border-bottom: 1px solid var(--line); }
Expand All @@ -44,7 +44,7 @@
.cta form.omni { width: 100%; }
@media (max-width: 860px) { .cta { grid-template-columns: minmax(0, 1fr); } .bar .count { display: none; } }
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body>
<header class="nav">
Expand Down Expand Up @@ -94,7 +94,11 @@ <h1 class="h1">Every city, so far.</h1>
<div class="results">
<div class="wrap">
<div id="featuredBox">
<div class="group-h"><h2>Featured</h2><small>hand-picked to show what a city can look like</small></div>
<div id="mineBox" hidden>
<div class="group-h"><h2>Yours</h2><small id="mineNote">built from this browser</small></div>
<div class="tiles" id="mine"></div>
</div>
<div class="group-h" id="featH"><h2>Featured</h2><small>hand-picked to show what a city can look like</small></div>
<div class="tiles" id="featured"></div>
<div class="group-h" id="freshH" style="margin-top:40px"><h2>Fresh off the press</h2><small>the latest builds, newest first</small></div>
</div>
Expand Down Expand Up @@ -136,18 +140,29 @@ <h1 class="h1">Every city, so far.</h1>
function render() {
if (!G) return;
const plain = !state.q && !state.kind && state.sort === 'recent';
$('#featuredBox').hidden = !plain || !G.featured.length;
if (plain) $('#featured').innerHTML = G.featured.map(e => tile(e)).join('');
const seen = new Set(plain ? G.featured.map(e => e.key) : []);
// the plain view is three shelves: yours, featured, then the latest builds, each city on one of them only
const mine = new Set(G.mine.map(e => e.key));
const featured = G.featured.filter(e => !mine.has(e.key));
$('#featuredBox').hidden = !plain || !(G.mine.length || G.featured.length);
$('#mineBox').hidden = !G.mine.length;
$('#featH').hidden = $('#featured').hidden = !featured.length;
$('#featH').style.marginTop = G.mine.length ? '40px' : '';
if (plain) {
$('#mine').innerHTML = G.mine.map(e => tile(e)).join('');
$('#mineNote').textContent = window.CCSite.me && G.mine.some(e => e.key === window.CCSite.me.toLowerCase())
? 'your own city, and the ones built from this browser' : 'built from this browser';
$('#featured').innerHTML = featured.map(e => tile(e)).join('');
}
const seen = new Set(plain ? [...mine, ...featured.map(e => e.key)] : []);
const q = state.q.trim().toLowerCase();
let list = (plain ? G.recent : G.all).filter(e => !seen.has(e.key) && (!state.kind || e.kind === state.kind) && (!q || e.target.toLowerCase().includes(q)));
list = list.slice().sort(SORTS[state.sort] || SORTS.recent);
$('#grid').innerHTML = list.length ? list.map(e => tile(e)).join('')
: `<div class="empty" style="grid-column:1/-1">${q ? `Nothing built matches <b>${esc(q)}</b> yet. <a href="/${esc(window.CCSite.parse(q) || '')}">Build it →</a>` : 'Nothing here yet.'}</div>`;
// with nothing beyond the featured cities yet, skip the empty 'fresh' shelf rather than show it
const bare = plain && !list.length && G.featured.length;
const bare = plain && !list.length && (G.mine.length || featured.length);
$('#freshH').hidden = $('#grid').hidden = !!bare;
const n = (plain ? G.featured.length : 0) + list.length;
const n = (plain ? G.mine.length + featured.length : 0) + list.length;
$('#count').textContent = `${n} ${n === 1 ? 'city' : 'cities'}`;
const qs = new URLSearchParams();
if (state.q) qs.set('q', state.q);
Expand Down
6 changes: 3 additions & 3 deletions web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<meta name="twitter:description" content="Swap github.com for commit.city on any GitHub profile, org or repo and watch a year of commits replay as a 3D city. Every file a tower, every commit a beam of light, every AI agent accounted for.">
<meta name="twitter:image" content="https://commit.city/api/og">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
/* hero */
.hero { position: relative; padding: clamp(40px, 6vw, 84px) 0 clamp(40px, 6vw, 72px); }
Expand Down Expand Up @@ -166,7 +166,7 @@
.closing .h2 { margin: 14px auto 26px; max-width: 760px; }
.closing form.omni { max-width: 620px; margin: 0 auto; text-align: left; }
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body class="home">
<header class="nav">
Expand Down Expand Up @@ -611,7 +611,7 @@ <h2 class="h2 rv d1">What does your year look like?</h2>
const cols = () => getComputedStyle($('#tiles')).gridTemplateColumns.split(' ').length;
const twoRows = () => Math.max(4, 2 * cols());
const SORTS = {
featured: () => 0, // the gallery's own order: featured, then latest built
featured: () => 0, // the gallery's own order: yours, featured, then latest built
recent: (a, b) => (b.v || 0) - (a.v || 0),
commits: (a, b) => b.commits - a.commits,
agent: (a, b) => (b.agent || 0) - (a.agent || 0) || b.commits - a.commits,
Expand Down
4 changes: 2 additions & 2 deletions web/public/readme.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<meta name="twitter:description" content="A live README card for your GitHub profile: your city's skyline, stats and language mix, rendered as one self-contained SVG that updates whenever your city is rebuilt.">
<meta name="twitter:image" content="https://commit.city/api/og?page=readme">
<link rel="preload" href="/s/f/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/s/site.css?v=4a58d6b3">
<link rel="stylesheet" href="/s/site.css?v=92373990">
<style>
.builder { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.3fr); gap: 24px; align-items: start; padding-bottom: 40px; }
.ctl { position: sticky; top: calc(var(--nav-h) + 20px); display: grid; grid-template-columns: minmax(0, 1fr); gap: 22px; }
Expand Down Expand Up @@ -59,7 +59,7 @@
.params td:first-child { white-space: nowrap; color: var(--ink); font-family: var(--mono); font-size: 13px; }
@media (max-width: 980px) { .builder, .more { grid-template-columns: minmax(0, 1fr); } .ctl { position: static; } }
</style>
<script src="/s/site.js?v=8b3577f9" defer></script>
<script src="/s/site.js?v=0cf12122" defer></script>
</head>
<body>
<header class="nav">
Expand Down
13 changes: 13 additions & 0 deletions web/public/s/city.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,23 @@
return fail(e);
}
clearInterval(clock);
remember(meta);
if (history.replaceState) history.replaceState(null, '', pagePath() + location.hash);
await open(meta);
}

// The cities this browser built, newest first, as gallery entries: the home page and /explore lead with them
// (site.js reads the list). It stays in this browser.
function remember(m) {
const share = n => (m.commits ? Math.round((1000 * n) / m.commits) / 1000 : 0);
const e = { key: m.key, target: m.target, kind: m.kind, avatar: m.avatar, commits: m.commits, repos: m.repos, lines: m.lines, v: m.v,
agent: share((m.who || {}).agent || 0), bot: share((m.who || {}).bot || 0) };
try {
const kept = JSON.parse(localStorage.getItem('cc.mine') || '[]').filter(x => x && x.key !== e.key);
localStorage.setItem('cc.mine', JSON.stringify([e, ...kept].slice(0, 24)));
} catch { /* storage off: the gallery still lists it */ }
}

function fail(e) {
const info = (e && e.info) || {};
const s = info.status;
Expand Down
1 change: 1 addition & 0 deletions web/public/s/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ select.select { height: 38px; padding: 0 30px 0 12px; border-radius: 10px; borde
.tile .badge { position: absolute; left: 10px; top: 10px; padding: 3px 9px; border-radius: 99px; font-size: 11px; font-weight: 600; color: var(--ink-2); background: rgba(4, 6, 12, 0.72);
border: 1px solid var(--line); backdrop-filter: blur(6px); }
.tile .badge.ag { left: auto; right: 10px; color: var(--agent); border-color: rgba(197, 139, 255, 0.35); }
.tile .badge.me { color: var(--accent); border-color: rgba(95, 211, 243, 0.4); }
.tile .meta { display: flex; align-items: center; gap: 11px; padding: 13px 14px 12px; }
.tile .meta img { width: 32px; height: 32px; border-radius: 50%; background: var(--bg-3); flex: none; }
.tile .meta img.sq { border-radius: 8px; }
Expand Down
18 changes: 14 additions & 4 deletions web/public/s/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,24 @@
const avatarOf = (owner, size = 64) => `https://github.com/${encodeURIComponent(owner)}.png?size=${size}`;

// ─────────────────────────────────────────────── gallery feed (one fetch per page)
// `mine`: your own city when signed in, then the cities this browser built (city.js keeps that list), marked
// `mine` and first in `all`. The gallery's copy of one wins: it may be a newer build. One that has dropped out
// of the gallery's latest builds still shows, from the browser's copy.
let galleryP = null;
function gallery() {
if (!galleryP) {
galleryP = fetch('/api/gallery').then(r => (r.ok ? r.json() : { featured: [], recent: [] })).catch(() => ({ featured: [], recent: [] }))
.then(g => {
const seen = new Set(), all = [];
for (const e of [...(g.featured || []), ...(g.recent || [])]) if (!seen.has(e.key)) { seen.add(e.key); all.push(e); }
return { featured: g.featured || [], recent: g.recent || [], all };
const listed = new Map();
for (const e of [...(g.featured || []), ...(g.recent || [])]) if (!listed.has(e.key)) listed.set(e.key, e);
let kept = [];
try { kept = JSON.parse(localStorage.getItem('cc.mine') || '[]'); } catch { /* storage off */ }
const mine = [], seen = new Set();
const add = e => { if (e && e.key && !seen.has(e.key)) { seen.add(e.key); mine.push({ ...e, mine: true }); } };
if (me && LOGIN.test(me)) add(listed.get(me.toLowerCase()));
for (const e of Array.isArray(kept) ? kept : []) add(listed.get(e && e.key) || e);
const all = [...mine, ...[...listed.values()].filter(e => !seen.has(e.key))];
return { featured: g.featured || [], recent: g.recent || [], mine, all };
});
}
return galleryP;
Expand Down Expand Up @@ -200,7 +210,7 @@
const split = (agent || bot) ? `<div class="split" title="${Math.round(human * 100)}% human · ${Math.round(bot * 100)}% bots · ${Math.round(agent * 100)}% AI agents">
<span class="h" style="flex-grow:${human}"></span><span class="b" style="flex-grow:${bot}"></span><span class="a" style="flex-grow:${agent}"></span></div>` : '';
return `<a class="tile" href="/${esc(n)}"><div class="sky"><img src="/api/card?t=${encodeURIComponent(e.target)}&layout=thumb&v=${e.v || 0}" alt="" loading="lazy" decoding="async">
<span class="badge">${kindLabel(e.kind)}</span>${agent >= 0.01 ? `<span class="badge ag">${Math.round(agent * 100)}% agent</span>` : ''}</div>
<span class="badge${e.mine ? ' me' : ''}">${e.mine ? 'yours · ' : ''}${kindLabel(e.kind)}</span>${agent >= 0.01 ? `<span class="badge ag">${Math.round(agent * 100)}% agent</span>` : ''}</div>
<div class="meta"><img class="${e.kind === 'user' ? '' : 'sq'}" src="${esc(e.avatar)}${String(e.avatar).includes('?') ? '&' : '?'}s=64" alt="" loading="lazy">
<div><b>${esc(e.target)}</b><small>${compact(e.commits)} commits · ${e.repos} repo${e.repos === 1 ? '' : 's'} · ${compact(e.lines)} lines</small></div></div>${opts.split === false ? '' : split}</a>`;
}
Expand Down
2 changes: 1 addition & 1 deletion web/site/about.html
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ <h2>Privacy</h2>
<ul>
<li><b>Public data only.</b> The builder reads public repositories with a token that can't see anything private. Private repos never appear on commit.city.</li>
<li><b>No source code is kept.</b> A city stores file paths, line counts and timestamps, all things anyone can already see on GitHub. Repo records are cached so the next visitor doesn't rebuild them.</li>
<li><b>No ads, no trackers, no analytics cookies.</b> A cookie is set only if you sign in, to remember who you are.</li>
<li><b>No ads, no trackers, no analytics cookies.</b> A cookie is set only if you sign in, to remember who you are. The cities you build are listed in your own browser's storage, so the home page and Explore can show them first; that list never leaves your browser.</li>
<li><b>Sign-in asks for no permissions.</b> It proves you own an account, so you can manage its city. Builds you start then use your own GitHub API quota.</li>
</ul>
</section>
Expand Down
Loading
Loading