@bongos/core 1.21.55 → 1.21.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +54 -29
- package/docs/copy-inventory.md +62 -24
- package/docs/copy-registry.json +456 -89
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +32 -4
- package/docs/page-readings.json +421 -354
- package/modules/hall-ui/public/street-draw.js +1 -3
- package/modules/hall-ui/public/street.css +0 -1
- package/modules/hall-ui/public/street.html +0 -1
- package/modules/hall-ui/public/street.js +0 -2
- package/modules/hall-ui/records/governor-street.md +10 -0
- package/modules/public-landing/public/guild.html +571 -0
- package/modules/public-landing/public/guild.probes.json +53 -0
- package/modules/public-landing/public/guild.states.json +38 -0
- package/modules/ui-design/kit/fixtures/guilds.json +73 -0
- package/modules/ui-design/kit/serve.js +46 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +13 -0
- package/tests/hall_matter_card.mjs +1 -1
- package/tests/hall_street.mjs +12 -3
- package/tests/hub_guild_page.mjs +537 -0
- package/tests/privacy_matrix.mjs +126 -14
- package/tests/render_check.mjs +2 -2
- package/tests/ui_design_kit.mjs +3 -3
|
@@ -289,7 +289,6 @@
|
|
|
289
289
|
// the tiers whose label shows without a hover, most important first
|
|
290
290
|
const LABEL_TIERS = Object.freeze(['urgent', 'needs', 'queue']);
|
|
291
291
|
const MAX_CRATES = 3;
|
|
292
|
-
const HINT = 'work moves left to right · orange waits on you · white is moving without you';
|
|
293
292
|
const READING_KEYS = Object.freeze(['flow', 'promises', 'money', 'people', 'fairness', 'rules']);
|
|
294
293
|
const READING_LABELS = Object.freeze({ flow: 'flow', promises: 'promises', money: 'money', people: 'people', fairness: 'fairness', rules: 'rules at work' });
|
|
295
294
|
const READING_BUILDING = Object.freeze({ flow: 'review', promises: 'planning', money: 'treasury', people: 'people', fairness: 'disputes', rules: 'rules' });
|
|
@@ -626,7 +625,6 @@
|
|
|
626
625
|
flat: flat, blds: blds, crates: crates, cols: cols, groups: groups, pins: pins, doors: doors, signs: signs,
|
|
627
626
|
hud: { needs: docket ? String(needs) : '—', moving: docket ? String(moving) : '—', share: money(share), hot: needs > 0 },
|
|
628
627
|
line: docket ? String(docket.line || '') : FALLBACK_LINE,
|
|
629
|
-
hint: docket && !evening ? HINT : '',
|
|
630
628
|
eveningLine: 'this month · ' + moving + ' moving · ' + settled + ' settled · ' + money(share) + ' to your share',
|
|
631
629
|
label: 'The street: the nine civic buildings in the order work moves, and '
|
|
632
630
|
+ (goals.length === 1 ? 'one district' : goals.length + ' districts') + ', one for each open goal'
|
|
@@ -661,7 +659,7 @@
|
|
|
661
659
|
whenWord: whenWord, reviewHeldOf: reviewHeldOf, monthShare: monthShare, money: money, parseHash: parseHash, buildingHref: buildingHref, districtHref: districtHref,
|
|
662
660
|
streetModel: streetModel, cardFor: cardFor, placeLabels: placeLabels, labelBox: labelBox, shownLabels: shownLabels, pinLabel: pinLabel,
|
|
663
661
|
G: G, SEGS: SEGS, GROUP_NAMES: GROUP_NAMES, CIVIC_KEYS: CIVIC_KEYS, READING_KEYS: READING_KEYS, ARTBOARD_W: ARTBOARD_W,
|
|
664
|
-
PIN_W: PIN_W, PIN_LABEL: PIN_LABEL, LABEL_MAX_W: LABEL_MAX_W, PIN_STACK: PIN_STACK, MAX_CRATES: MAX_CRATES, MAX_DISTRICTS: MAX_DISTRICTS, GOALS_HREF: GOALS_HREF,
|
|
662
|
+
PIN_W: PIN_W, PIN_LABEL: PIN_LABEL, LABEL_MAX_W: LABEL_MAX_W, PIN_STACK: PIN_STACK, MAX_CRATES: MAX_CRATES, MAX_DISTRICTS: MAX_DISTRICTS, GOALS_HREF: GOALS_HREF, FALLBACK_LINE: FALLBACK_LINE, HOME_HREF: '/builders/city',
|
|
665
663
|
};
|
|
666
664
|
root.GovStreet = api;
|
|
667
665
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
@@ -93,7 +93,6 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
93
93
|
.street-layer { position: absolute; left: 0; top: 0; width: 100%; height: 900px; pointer-events: none; }
|
|
94
94
|
.street-layer > * { pointer-events: auto; }
|
|
95
95
|
|
|
96
|
-
.street-hint { position: absolute; left: 0; right: 0; top: 84px; margin: 0; text-align: center; font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); pointer-events: none; z-index: 2; }
|
|
97
96
|
.street-evening { position: absolute; left: 0; right: 0; top: 104px; display: flex; flex-direction: column; align-items: center; gap: 10px; pointer-events: none; z-index: 2; }
|
|
98
97
|
.street-evening[hidden] { display: none; }
|
|
99
98
|
.street-evening__h { margin: 0; font-size: 46px; font-weight: 300; letter-spacing: -0.042em; line-height: 1; }
|
|
@@ -96,7 +96,6 @@
|
|
|
96
96
|
<div class="street-window" id="streetWindow">
|
|
97
97
|
<div class="street-sizer" id="streetSizer">
|
|
98
98
|
<div class="street-board" id="streetBoard" role="group" aria-label="The street">
|
|
99
|
-
<p class="street-hint" id="streetHint"></p>
|
|
100
99
|
<div class="street-evening" id="streetEvening" hidden>
|
|
101
100
|
<p class="street-evening__h">nothing waits on you.</p>
|
|
102
101
|
<p class="street-evening__line" id="streetEveningLine"></p>
|
|
@@ -377,8 +377,6 @@
|
|
|
377
377
|
var signs = $('streetSigns'); clear(signs);
|
|
378
378
|
m.signs.forEach(function (sg) { signs.appendChild(drawSign(sg)); });
|
|
379
379
|
|
|
380
|
-
var hint = $('streetHint');
|
|
381
|
-
if (hint) { hint.textContent = m.hint; hint.hidden = !m.hint || !!view.open || showInside; }
|
|
382
380
|
var eve = $('streetEvening');
|
|
383
381
|
if (eve) eve.hidden = !(m.evening && !view.open) || showInside;
|
|
384
382
|
var eveLine = $('streetEveningLine');
|
|
@@ -96,3 +96,13 @@ Owner decision 2026-10-04: Work categories was the last floor row that still lef
|
|
|
96
96
|
Walked in the hall preview harness at 1440 and 375, light and dark: the rows, an open detail with its move chips, the rename and add fields, and a rename sent (the write answered inside the page, since the harness refuses writes) with its pip, toast and record line. Nothing was changed on the live site. The old Project settings page keeps its own category list.
|
|
97
97
|
|
|
98
98
|
Tests: `tests/hall_street_desk.mjs` (the `1004580` cases) hold the server rows and their gates, every act's route, the move arithmetic, and on the page rename, retire and bring back, a move from the open detail, an add, and a refused act and a refused move that count nothing. `tests/goal_category.mjs` holds the grouped count.
|
|
99
|
+
|
|
100
|
+
## No legend hint (task 1004581)
|
|
101
|
+
|
|
102
|
+
Owner decision 2026-10-04: the street should read at a glance without words, so the small grey line over the sky ("work moves left to right · orange waits on you · white is moving without you") is gone, in both themes and at every width. This is a deliberate departure from the prototype, whose `Street.dc.html` still draws the line; everything else on the reference board stands. The line was also wrong in light mode, where moving pins are black, not white.
|
|
103
|
+
|
|
104
|
+
- **Gone with it:** the `streetHint` element, the `.street-hint` style, the `HINT` constant and the model's `hint` field in `street-draw.js`, the code in `street.js` that showed and hid it, and its entry in `docs/page-readings.json` (regenerated).
|
|
105
|
+
- **No gap where it stood.** The line was laid over the sky, not in the flow, so the header and the reading band keep their places; walked in the hall preview harness at 1440 and 375, light and dark.
|
|
106
|
+
- **The evening view is unchanged** ("nothing waits on you." and its line still show when nothing needs you).
|
|
107
|
+
|
|
108
|
+
Tests: `tests/hall_street.mjs` asserts the model has no hint field, the module exports no `HINT`, and none of the street's four files carries the line; putting the markup or the constant back reds it.
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-mode="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<!-- THE GUILD PAGE (task 1002301, R25; ADR 0336 D4/D6, ADR 0337 D4.4). A guild
|
|
7
|
+
is a group of builders across projects, so its page lives on the hub, at the
|
|
8
|
+
/g/<slug> namespace ADR 0336 D6 reserved. One shell for every slug: the
|
|
9
|
+
script reads the slug from the path and asks two routes of platform-identity —
|
|
10
|
+
GET /guilds/:slug the PUBLIC read: name, public roster, size and
|
|
11
|
+
the rounded weekly totals; 404 for an unknown
|
|
12
|
+
slug AND for an unlisted guild, to everyone;
|
|
13
|
+
GET /guilds/:slug/members the OWN-SCOPE read, signed in only: a member's
|
|
14
|
+
door to their guild, unlisted ones included,
|
|
15
|
+
with their own two switches;
|
|
16
|
+
and writes one: PATCH /guilds/:slug/membership (those two switches). A
|
|
17
|
+
stranger, an unknown slug and an unlisted guild seen from outside are one
|
|
18
|
+
"no guild here", so the page never confirms an unlisted guild exists.
|
|
19
|
+
Hub-only, like /account: platform-server.js serves /g/<slug> only where
|
|
20
|
+
platform-identity is on. noindex while one shell answers every slug (a guild
|
|
21
|
+
that does not exist would otherwise index as a page); putting guilds in
|
|
22
|
+
search is the SEO goal's call (ADR 0336 D6). Server-stamped: {{…}} tokens are
|
|
23
|
+
filled by landingPage(). -->
|
|
24
|
+
<title>{{productName}} — Guild</title>
|
|
25
|
+
<meta name="description" content="A guild on {{productName}}: the builders in it, and what they have built together.">
|
|
26
|
+
<meta name="robots" content="noindex">
|
|
27
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
28
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
29
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
|
|
30
|
+
<link rel="stylesheet" href="/assets/cosmos.css">
|
|
31
|
+
<!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
32
|
+
<style>
|
|
33
|
+
/* The guild page's own layout, in the account page's voice (account.html): a
|
|
34
|
+
lowercase light heading, hairline sections with the title left and the body
|
|
35
|
+
right, the choice cells with the dot. No :root block — the pack's injected
|
|
36
|
+
theme owns the tokens; everything below reads them. The bar and the bands are
|
|
37
|
+
cosmos.css's own rules, not restyled here. */
|
|
38
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
39
|
+
.skip:focus{position:fixed;top:8px;left:8px;z-index:100;width:auto;height:auto;margin:0;overflow:visible;clip-path:none;white-space:normal;
|
|
40
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
41
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
42
|
+
.gld{position:relative;z-index:1;max-width:1060px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 56px;}
|
|
43
|
+
.gld h1{font-family:var(--font-display);font-weight:300;font-size:clamp(30px,4.2vw,56px);letter-spacing:-.04em;line-height:1.02;text-transform:lowercase;color:var(--ink);margin:6px 0 0;overflow-wrap:anywhere;}
|
|
44
|
+
.gld .lede{margin:12px 0 0;max-width:62ch;font-size:15.5px;font-weight:400;line-height:1.55;color:var(--ink-soft);white-space:pre-line;overflow-wrap:anywhere;}
|
|
45
|
+
.gld .loading{font-size:15px;color:var(--ink-soft);}
|
|
46
|
+
.gKicker,.gMeta{margin:18px 0 0;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
47
|
+
.gMeta{margin:16px 0 26px;font-variant-numeric:tabular-nums;}
|
|
48
|
+
.gMiss{padding:0 0 10px;max-width:62ch;}
|
|
49
|
+
.gMiss h1{margin-top:28px;}
|
|
50
|
+
.gMiss .lede{margin-bottom:22px;}
|
|
51
|
+
.gMiss .gh{width:18px;height:18px;fill:currentColor;}
|
|
52
|
+
.gMiss .btn{text-transform:none;}
|
|
53
|
+
/* the sections: the account page's .aSec grid */
|
|
54
|
+
.aSec{display:grid;grid-template-columns:minmax(200px,300px) 1fr;grid-template-rows:auto 1fr;gap:6px 40px;border-top:1px solid var(--rule);padding:30px 0 34px;}
|
|
55
|
+
.aSec>h2{grid-column:1;font-family:var(--font-display);font-weight:300;font-size:clamp(22px,2.4vw,28px);letter-spacing:-.03em;line-height:1.08;text-transform:lowercase;color:var(--ink);margin:0;}
|
|
56
|
+
.aSec>.aSide{grid-column:1;align-self:start;margin:4px 0 0;max-width:34ch;font-size:13.5px;line-height:1.5;color:var(--ink-faint);}
|
|
57
|
+
.aSec>.aBody{grid-column:2;grid-row:1/span 2;display:grid;gap:18px;align-content:start;min-width:0;}
|
|
58
|
+
.aLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
59
|
+
.aBand{display:grid;gap:8px;}
|
|
60
|
+
.aBand [role="radiogroup"]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
61
|
+
.aOpt{position:relative;display:block;margin:0;padding:16px 18px 18px;border-left:1px solid var(--rule-soft);cursor:pointer;}
|
|
62
|
+
.aOpt:first-child{border-left:0;padding-left:0;}
|
|
63
|
+
.aOpt input{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);}
|
|
64
|
+
.aOpt>span{display:block;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
65
|
+
.aOpt b{display:flex;align-items:center;gap:10px;margin-bottom:6px;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
|
|
66
|
+
.aOpt b::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
|
|
67
|
+
.aOpt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
68
|
+
.aOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
69
|
+
.aWhat{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-soft);max-width:70ch;}
|
|
70
|
+
.gld .actions{margin:0;gap:14px;}
|
|
71
|
+
.gld .btn{min-height:44px;padding:0 22px;font-family:var(--font-body);font-weight:500;font-size:15px;text-transform:lowercase;white-space:nowrap;box-shadow:none;}
|
|
72
|
+
.gld .btn:hover,.gld .btn:active{transform:none;box-shadow:none;}
|
|
73
|
+
.gld .projMsg{margin:0;min-height:0;font-weight:400;}
|
|
74
|
+
/* the prompt after a guild goes public (ADR 0336 D4 §3): asked, never assumed */
|
|
75
|
+
.gAsk{display:grid;gap:14px;padding:18px 0 20px;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
76
|
+
.gAsk p{margin:0;font-size:14.5px;line-height:1.55;color:var(--ink-soft);max-width:62ch;}
|
|
77
|
+
.gAsk p b{display:block;margin-bottom:4px;font-family:var(--font-body);font-weight:500;font-size:17px;color:var(--ink);}
|
|
78
|
+
/* the record: four band floors as figures, two by two so a long band ("200,000+")
|
|
79
|
+
keeps its cell, a hairline above each (the ledger grammar) */
|
|
80
|
+
.gFigs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 28px;margin:0;}
|
|
81
|
+
.gFigs>div{display:grid;gap:6px;align-content:start;padding:14px 0 4px;border-top:1px solid var(--rule-soft);min-width:0;}
|
|
82
|
+
.gFigs dt{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
83
|
+
.gFigs dd{margin:0;font-family:var(--font-display);font-weight:300;font-size:clamp(28px,3.2vw,44px);letter-spacing:-.035em;line-height:1;color:var(--ink);font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
84
|
+
.gFigs dd.under{font-size:clamp(20px,2vw,26px);letter-spacing:-.02em;color:var(--ink-soft);}
|
|
85
|
+
/* the roster: one hairline row per member */
|
|
86
|
+
.gRoster{list-style:none;margin:0;padding:0;}
|
|
87
|
+
.gRoster li{display:grid;grid-template-columns:36px minmax(0,1fr) auto;gap:0 14px;align-items:center;padding:12px 0;border-top:1px solid var(--rule-soft);}
|
|
88
|
+
.gRoster li:last-child{border-bottom:1px solid var(--rule-soft);}
|
|
89
|
+
.gRoster .gAv{width:36px;height:36px;border-radius:50%;object-fit:cover;background:var(--bg-card);}
|
|
90
|
+
.gRoster span.gAv{display:grid;place-items:center;font-family:var(--font-body);font-weight:500;font-size:15px;text-transform:uppercase;color:var(--ink-soft);border:1px solid var(--rule);box-sizing:border-box;}
|
|
91
|
+
.gRoster .gWho{display:grid;gap:2px;min-width:0;}
|
|
92
|
+
.gRoster .gName{font-family:var(--font-body);font-weight:500;font-size:15.5px;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
93
|
+
.gRoster .gHandle{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
94
|
+
.gRoster .gKind{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-soft);}
|
|
95
|
+
@media (max-width:900px){
|
|
96
|
+
.aSec{grid-template-columns:1fr;gap:12px;}
|
|
97
|
+
.aSec>h2,.aSec>.aSide,.aSec>.aBody{grid-column:1;grid-row:auto;}
|
|
98
|
+
.aSec>.aSide{max-width:none;}
|
|
99
|
+
}
|
|
100
|
+
@media (max-width:560px){
|
|
101
|
+
.gFigs{gap:0 18px;}
|
|
102
|
+
.aBand [role="radiogroup"]{grid-template-columns:1fr;}
|
|
103
|
+
.aOpt{padding:14px 0 16px;border-left:0;border-top:1px solid var(--rule-soft);}
|
|
104
|
+
.aOpt:first-child{border-top:0;}
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html, task 1004455):
|
|
110
|
+
the account page's markup with no current place, so /projects → /g/<slug>
|
|
111
|
+
keeps the chrome. assets/hub-bar.js runs it. The sign-in doors name no return
|
|
112
|
+
in the markup — one shell serves every slug — so the page script points them
|
|
113
|
+
back at this guild once it has read the slug (and without script, the sign-in
|
|
114
|
+
route falls back to the Referer, which is this page). -->
|
|
115
|
+
<a class="skip vh" href="#guild">Skip to content</a>
|
|
116
|
+
<header class="shell bar">
|
|
117
|
+
<div class="barLeft">
|
|
118
|
+
<a class="barMark" id="barMark" href="/">
|
|
119
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
120
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
121
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
122
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
123
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
124
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
125
|
+
</svg>
|
|
126
|
+
<span>{{productName}}</span>
|
|
127
|
+
</a>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="barRight">
|
|
130
|
+
<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
131
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from GET /me.
|
|
132
|
+
Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
133
|
+
<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden><img id="whoImg" alt="" hidden><span class="whoName" id="whoLogin"></span><svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>
|
|
134
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?repos=1">Sign in</a>
|
|
135
|
+
</div>
|
|
136
|
+
</header>
|
|
137
|
+
|
|
138
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
139
|
+
<div class="bandIn">
|
|
140
|
+
<ul class="bandRow">
|
|
141
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
142
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
143
|
+
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
144
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
149
|
+
<div class="bandIn bandProfile">
|
|
150
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
151
|
+
<ul class="bandRow">
|
|
152
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
153
|
+
<li class="bandCell"><a class="bandT" href="/account">Account</a></li>
|
|
154
|
+
<!-- the honest hover target; the delegated click upgrades it to the POST -->
|
|
155
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
156
|
+
</ul>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<main class="gld" id="guild">
|
|
161
|
+
<p class="loading" id="loading">Reading this guild…</p>
|
|
162
|
+
<div class="alert" id="pageAlert" role="alert"></div>
|
|
163
|
+
|
|
164
|
+
<!-- NO GUILD HERE: an unknown slug, a malformed one, and an unlisted guild seen
|
|
165
|
+
by anyone but its members get this one answer (ADR 0336 D4). Signed out,
|
|
166
|
+
it offers sign-in, because a member reaches an unlisted guild that way. -->
|
|
167
|
+
<section class="gMiss" id="missing" hidden aria-labelledby="missing-h">
|
|
168
|
+
<h1 id="missing-h">no guild here</h1>
|
|
169
|
+
<p class="lede" id="missingWhy"></p>
|
|
170
|
+
<a class="btn" id="missingSignin" hidden href="/api/bongos/auth/web/start?repos=1">
|
|
171
|
+
<!-- keep the spaces inside this path's numbers: same shape, and they are load-bearing (task 1004401) -->
|
|
172
|
+
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23 .82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2 .82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12 .51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29 .25.54.73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15 .46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
173
|
+
Sign in with GitHub
|
|
174
|
+
</a>
|
|
175
|
+
</section>
|
|
176
|
+
|
|
177
|
+
<!-- THE GUILD: ships hidden, revealed once either read answers -->
|
|
178
|
+
<div id="guildBody" hidden>
|
|
179
|
+
<p class="gKicker" id="gKicker">Guild</p>
|
|
180
|
+
<h1 id="gName"></h1>
|
|
181
|
+
<p class="lede" id="gDesc" hidden></p>
|
|
182
|
+
<p class="gMeta" id="gMeta"></p>
|
|
183
|
+
|
|
184
|
+
<!-- YOUR PLACE — members only, from the own-scope read. Two switches, each the
|
|
185
|
+
member's own (ADR 0336 D4, ADR 0337 D4.4): shown on the public page, and
|
|
186
|
+
counted in the totals. Only what changed is sent. -->
|
|
187
|
+
<section class="aSec" id="gYou" hidden aria-labelledby="gYou-h">
|
|
188
|
+
<h2 id="gYou-h">your place</h2>
|
|
189
|
+
<p class="aSide" id="gYouSide"></p>
|
|
190
|
+
<form class="aBody" id="youForm" novalidate>
|
|
191
|
+
<!-- the prompt after an unlisted → public flip: a member who joined an
|
|
192
|
+
unlisted guild is not shown on its public page until they say yes -->
|
|
193
|
+
<div class="gAsk" id="showAsk" hidden>
|
|
194
|
+
<p><b>Show you on this guild’s public page?</b>This guild is public, and you’re a member who isn’t on its public page. Nobody is listed there until they say yes.</p>
|
|
195
|
+
<div class="actions"><button type="button" class="btn" id="askYes">show me</button><button type="button" class="btn ghost" id="askNo">not now</button></div>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="aBand" id="shownBand">
|
|
198
|
+
<span class="aLabel" id="shownLabel">On this guild’s public page</span>
|
|
199
|
+
<div role="radiogroup" aria-labelledby="shownLabel">
|
|
200
|
+
<label class="aOpt"><input type="radio" name="shown" value="on"><span><b>Show me</b>Your name and picture are listed on its public page, while your account is public.</span></label>
|
|
201
|
+
<label class="aOpt"><input type="radio" name="shown" value="off"><span><b>Keep me off</b>You stay a member, and the public page doesn’t list you.</span></label>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
<p class="aWhat" id="shownUnlisted" hidden></p>
|
|
205
|
+
<div class="aBand" id="countBand">
|
|
206
|
+
<span class="aLabel" id="countLabel">In this guild’s totals</span>
|
|
207
|
+
<div role="radiogroup" aria-labelledby="countLabel">
|
|
208
|
+
<label class="aOpt"><input type="radio" name="counted" value="on"><span><b>Count my numbers</b>They add to this guild’s totals.</span></label>
|
|
209
|
+
<label class="aOpt"><input type="radio" name="counted" value="off"><span><b>Leave them out</b>The totals add up everyone else.</span></label>
|
|
210
|
+
</div>
|
|
211
|
+
<p class="aWhat" id="countWhat"></p>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="actions"><button type="submit" class="btn" id="youSave">save</button></div>
|
|
214
|
+
<p class="projMsg" id="youMsg" role="status" aria-live="polite"></p>
|
|
215
|
+
</form>
|
|
216
|
+
</section>
|
|
217
|
+
|
|
218
|
+
<!-- THE RECORD — R26's rounded weekly totals (ADR 0337 D4.4), from the public
|
|
219
|
+
read only: band floors ("100+"), never an exact sum, and no count of who
|
|
220
|
+
is in them. An unlisted guild publishes none. -->
|
|
221
|
+
<section class="aSec" id="gRecord" aria-labelledby="gRecord-h">
|
|
222
|
+
<h2 id="gRecord-h">the record</h2>
|
|
223
|
+
<p class="aSide">What its members have built together, across every project. Rounded down to a band, and counted once a week.</p>
|
|
224
|
+
<div class="aBody">
|
|
225
|
+
<dl class="gFigs" id="gFigs">
|
|
226
|
+
<div><dt>Works shipped</dt><dd id="tShipped">—</dd></div>
|
|
227
|
+
<div><dt>Credits</dt><dd id="tCredits">—</dd></div>
|
|
228
|
+
<div><dt>Karma</dt><dd id="tKarma">—</dd></div>
|
|
229
|
+
<div><dt>Projects</dt><dd id="tProjects">—</dd></div>
|
|
230
|
+
</dl>
|
|
231
|
+
<p class="aWhat" id="gRecordNote"></p>
|
|
232
|
+
</div>
|
|
233
|
+
</section>
|
|
234
|
+
|
|
235
|
+
<!-- MEMBERS — the public roster (ADR 0336 D4: handle, display name, avatar,
|
|
236
|
+
kind; never a GitHub id or login), or, for a member, the members' roster,
|
|
237
|
+
which lists everyone in the guild. -->
|
|
238
|
+
<section class="aSec" id="gMembers" aria-labelledby="gMembers-h">
|
|
239
|
+
<h2 id="gMembers-h">members</h2>
|
|
240
|
+
<p class="aSide" id="gMembersSide"></p>
|
|
241
|
+
<div class="aBody">
|
|
242
|
+
<ul class="gRoster" id="gRoster"></ul>
|
|
243
|
+
<p class="aWhat" id="gRosterEmpty" hidden>Nobody is listed on this page yet.</p>
|
|
244
|
+
</div>
|
|
245
|
+
</section>
|
|
246
|
+
</div>
|
|
247
|
+
</main>
|
|
248
|
+
|
|
249
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
250
|
+
|
|
251
|
+
<script src="/assets/hub-bar.js"></script>
|
|
252
|
+
<script>
|
|
253
|
+
(function () {
|
|
254
|
+
'use strict';
|
|
255
|
+
var API = '/api/bongos';
|
|
256
|
+
function $(id) { return document.getElementById(id); }
|
|
257
|
+
// The bar's chip, bands and sign out are assets/hub-bar.js (task 1004489), loaded
|
|
258
|
+
// just above; bar.ready settles with GET /me's answer, null when nobody is
|
|
259
|
+
// signed in. Without the asset the page still shows the public guild.
|
|
260
|
+
var bar = window.HubBar || { paint: function () {}, ready: Promise.resolve(null) };
|
|
261
|
+
|
|
262
|
+
// The slug rule is guilds.js's (normalizeHandle's charset, a two-character
|
|
263
|
+
// floor, at most 39) — boundary-forced duplication (ADR 0091 §2): a page cannot
|
|
264
|
+
// import a module. A path that cannot be a slug is "no guild here" without a
|
|
265
|
+
// request. tests/hub_guild_page.mjs holds this to the module's rule.
|
|
266
|
+
var SLUG_RE = /^(?=.{2,39}$)[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/;
|
|
267
|
+
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
268
|
+
var BASE_TITLE = document.title;
|
|
269
|
+
|
|
270
|
+
// What the server last said: the public read and the own-scope read. Every
|
|
271
|
+
// save re-paints from the server's answer, never from what was clicked.
|
|
272
|
+
var slug = null;
|
|
273
|
+
var pub = null;
|
|
274
|
+
var mine = null;
|
|
275
|
+
|
|
276
|
+
function request(method, path, body) {
|
|
277
|
+
var init = { method: method, credentials: 'same-origin', headers: { Accept: 'application/json' } };
|
|
278
|
+
if (body !== undefined) { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
279
|
+
return fetch(API + path, init).then(function (res) {
|
|
280
|
+
return res.json().catch(function () { return null; }).then(function (data) {
|
|
281
|
+
return { status: res.status, ok: res.ok, data: data };
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function serverWords(res, fallback) {
|
|
286
|
+
var m = res && res.data && res.data.error && res.data.error.message;
|
|
287
|
+
if (typeof m !== 'string' || !m) return fallback;
|
|
288
|
+
return m.charAt(0).toUpperCase() + m.slice(1) + (/[.!?]$/.test(m) ? '' : '.');
|
|
289
|
+
}
|
|
290
|
+
function say(el, text, kind) {
|
|
291
|
+
el.className = 'projMsg' + (kind ? ' ' + kind : '');
|
|
292
|
+
el.textContent = text;
|
|
293
|
+
}
|
|
294
|
+
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many); }
|
|
295
|
+
function monthYear(iso) {
|
|
296
|
+
var d = new Date(iso);
|
|
297
|
+
return isNaN(d.getTime()) ? '' : MONTHS[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
|
|
298
|
+
}
|
|
299
|
+
function dayMonthYear(iso) {
|
|
300
|
+
var d = new Date(iso);
|
|
301
|
+
return isNaN(d.getTime()) ? '' : d.getUTCDate() + ' ' + MONTHS[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
|
|
302
|
+
}
|
|
303
|
+
// A total is published as its band floor (guild-totals.js bandFloor): 0 means
|
|
304
|
+
// below the first band, which a page says as "under 10".
|
|
305
|
+
function band(n) {
|
|
306
|
+
var v = Number(n);
|
|
307
|
+
if (!isFinite(v) || v <= 0) return { text: 'under 10', under: true };
|
|
308
|
+
return { text: Math.floor(v).toLocaleString('en-US') + '+', under: false };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ---- the slug, and the sign-in doors that return to it ----
|
|
312
|
+
function slugFromPath(pathname) {
|
|
313
|
+
var m = /^\/g\/([^/]+)\/?$/.exec(pathname || '');
|
|
314
|
+
if (!m) return null;
|
|
315
|
+
var s;
|
|
316
|
+
try { s = decodeURIComponent(m[1]).toLowerCase(); } catch (e) { return null; }
|
|
317
|
+
return SLUG_RE.test(s) ? s : null;
|
|
318
|
+
}
|
|
319
|
+
function pointSigninHere() {
|
|
320
|
+
// the slug is SLUG_RE-clean ([a-z0-9-]), so it needs no encoding: the shape the
|
|
321
|
+
// other hub doors send (return=/account&repos=1)
|
|
322
|
+
var href = API + '/auth/web/start?return=/g/' + slug + '&repos=1';
|
|
323
|
+
$('signinTop').setAttribute('href', href);
|
|
324
|
+
$('missingSignin').setAttribute('href', href);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- the page's states: reading, no guild here, the guild ----
|
|
328
|
+
function showAlert(text) {
|
|
329
|
+
$('loading').hidden = true;
|
|
330
|
+
var a = $('pageAlert');
|
|
331
|
+
a.textContent = text;
|
|
332
|
+
a.className = 'alert on';
|
|
333
|
+
}
|
|
334
|
+
function showMissing(signedIn) {
|
|
335
|
+
$('loading').hidden = true;
|
|
336
|
+
$('guildBody').hidden = true;
|
|
337
|
+
$('missingWhy').textContent = signedIn
|
|
338
|
+
? 'There is no guild at this address that you can see. A guild that is unlisted is shown only to its members.'
|
|
339
|
+
: 'There is no public guild at this address. If it is an unlisted guild you belong to, sign in to see it.';
|
|
340
|
+
$('missingSignin').hidden = !!signedIn;
|
|
341
|
+
$('missing').hidden = false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function load() {
|
|
345
|
+
slug = slugFromPath(location.pathname);
|
|
346
|
+
if (!slug) { return bar.ready.then(function (me) { showMissing(!!me); }); }
|
|
347
|
+
pointSigninHere();
|
|
348
|
+
return Promise.all([
|
|
349
|
+
request('GET', '/guilds/' + slug),
|
|
350
|
+
// the own-scope read only for someone signed in: a stranger's page makes one call
|
|
351
|
+
bar.ready.then(function (me) { return me ? request('GET', '/guilds/' + slug + '/members') : null; }),
|
|
352
|
+
]).then(function (both) {
|
|
353
|
+
var p = both[0], m = both[1];
|
|
354
|
+
var signedIn = !!m && m.status !== 401;
|
|
355
|
+
pub = p.ok && p.data && p.data.guild ? p.data : null;
|
|
356
|
+
mine = m && m.ok && m.data && m.data.guild ? m.data : null;
|
|
357
|
+
if (!pub && !mine) {
|
|
358
|
+
if (p.status === 404 && (!m || m.status === 404 || m.status === 401)) { showMissing(signedIn); return; }
|
|
359
|
+
showAlert('Couldn’t read this guild right now — refresh in a moment.');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
paint();
|
|
363
|
+
$('loading').hidden = true;
|
|
364
|
+
$('missing').hidden = true;
|
|
365
|
+
$('guildBody').hidden = false;
|
|
366
|
+
}, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---- paint: every region from the server's answers ----
|
|
370
|
+
function paint() { paintHead(); paintYou(); paintRecord(); paintRoster(); }
|
|
371
|
+
|
|
372
|
+
function guild() { return (mine && mine.guild) || pub.guild; }
|
|
373
|
+
|
|
374
|
+
function paintHead() {
|
|
375
|
+
var g = guild();
|
|
376
|
+
document.title = g.name + ' — ' + BASE_TITLE.replace(/ — Guild$/, '');
|
|
377
|
+
$('gName').textContent = g.name;
|
|
378
|
+
var desc = g.description ? String(g.description).trim() : '';
|
|
379
|
+
$('gDesc').textContent = desc;
|
|
380
|
+
$('gDesc').hidden = !desc;
|
|
381
|
+
// a member is told which kind of guild this is; the public sees only public ones
|
|
382
|
+
$('gKicker').textContent = mine ? 'Guild · ' + (mine.guild.visibility === 'public' ? 'public' : 'unlisted') : 'Guild';
|
|
383
|
+
var parts = [];
|
|
384
|
+
if (mine) {
|
|
385
|
+
parts.push(plural(mine.count, 'member', 'members'));
|
|
386
|
+
if (pub) parts.push(pub.size + ' on the public page');
|
|
387
|
+
} else {
|
|
388
|
+
parts.push(plural(pub.size, 'member', 'members'));
|
|
389
|
+
}
|
|
390
|
+
var since = pub && pub.guild.created_at ? monthYear(pub.guild.created_at) : '';
|
|
391
|
+
if (since) parts.push('since ' + since);
|
|
392
|
+
$('gMeta').textContent = parts.join(' · ');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---- your place: the member's own two switches ----
|
|
396
|
+
var ASK_KEY = 'cb-guild-ask-';
|
|
397
|
+
function askDismissed() {
|
|
398
|
+
try { return window.localStorage.getItem(ASK_KEY + slug) === 'no'; } catch (e) { return false; }
|
|
399
|
+
}
|
|
400
|
+
function dismissAsk() {
|
|
401
|
+
try { window.localStorage.setItem(ASK_KEY + slug, 'no'); } catch (e) { /* a per-viewer convenience only */ }
|
|
402
|
+
}
|
|
403
|
+
function pick(name, value) {
|
|
404
|
+
document.querySelectorAll('input[name="' + name + '"]').forEach(function (r) { r.checked = r.value === value; });
|
|
405
|
+
}
|
|
406
|
+
function picked(name) {
|
|
407
|
+
var r = document.querySelector('input[name="' + name + '"]:checked');
|
|
408
|
+
return r ? r.value : null;
|
|
409
|
+
}
|
|
410
|
+
function paintYou() {
|
|
411
|
+
if (!mine) { $('gYou').hidden = true; return; }
|
|
412
|
+
var you = mine.you;
|
|
413
|
+
var isPublic = mine.guild.visibility === 'public';
|
|
414
|
+
$('gYou').hidden = false;
|
|
415
|
+
$('gYouSide').textContent = (you.membership_kind === 'owner' ? 'You own this guild.' : 'You’re a member of this guild.') +
|
|
416
|
+
' Only you see this, and only you can change it.';
|
|
417
|
+
// Asked, never assumed: while the prompt stands it IS the control, so the pair
|
|
418
|
+
// for the same switch waits until it is answered.
|
|
419
|
+
var asking = isPublic && !you.shown_publicly && !askDismissed();
|
|
420
|
+
$('showAsk').hidden = !asking;
|
|
421
|
+
$('shownBand').hidden = !isPublic || asking;
|
|
422
|
+
$('shownUnlisted').hidden = isPublic;
|
|
423
|
+
// A visibility change flips only the OWNER's own flag (ADR 0336 D4 §3), so the
|
|
424
|
+
// owner is listed the moment they make it public; everyone else is asked.
|
|
425
|
+
$('shownUnlisted').textContent = 'This guild is unlisted, so it has no public page. ' + (you.membership_kind === 'owner'
|
|
426
|
+
? 'If you make it public, you’ll be listed on it as its owner, and each member will be asked before they are.'
|
|
427
|
+
: 'If it is made public, you’ll be asked here before you’re listed on it.');
|
|
428
|
+
pick('shown', you.shown_publicly ? 'on' : 'off');
|
|
429
|
+
pick('counted', you.counted_in_totals ? 'on' : 'off');
|
|
430
|
+
paintCountWhat();
|
|
431
|
+
}
|
|
432
|
+
// The notice IS the consent (ADR 0337 D4.4): said for the choice as it stands
|
|
433
|
+
// on the page, before anything is saved.
|
|
434
|
+
function paintCountWhat() {
|
|
435
|
+
$('countWhat').textContent = picked('counted') === 'off'
|
|
436
|
+
? 'Your numbers are left out of this guild’s totals, from its next weekly count.'
|
|
437
|
+
: 'Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. ' +
|
|
438
|
+
'They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. ' +
|
|
439
|
+
'Leave them out of any guild you’d rather not share them with.';
|
|
440
|
+
}
|
|
441
|
+
document.querySelectorAll('input[name="counted"]').forEach(function (r) { r.addEventListener('change', paintCountWhat); });
|
|
442
|
+
|
|
443
|
+
// Re-read both after a save: the public roster and size move with the switch.
|
|
444
|
+
function reread() {
|
|
445
|
+
return Promise.all([
|
|
446
|
+
request('GET', '/guilds/' + slug),
|
|
447
|
+
request('GET', '/guilds/' + slug + '/members'),
|
|
448
|
+
]).then(function (both) {
|
|
449
|
+
if (both[0].ok && both[0].data && both[0].data.guild) pub = both[0].data;
|
|
450
|
+
else if (both[0].status === 404) pub = null;
|
|
451
|
+
if (both[1].ok && both[1].data && both[1].data.guild) mine = both[1].data;
|
|
452
|
+
paint();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function saveSwitches(body, done, btns) {
|
|
456
|
+
var msg = $('youMsg');
|
|
457
|
+
btns.forEach(function (b) { b.disabled = true; });
|
|
458
|
+
say(msg, 'Saving…');
|
|
459
|
+
return request('PATCH', '/guilds/' + slug + '/membership', body).then(function (res) {
|
|
460
|
+
btns.forEach(function (b) { b.disabled = false; });
|
|
461
|
+
if (res.status === 401) { say(msg, 'Your session expired — sign in again to save.', 'err'); bar.paint(null); return; }
|
|
462
|
+
if (!res.ok) { say(msg, serverWords(res, 'Couldn’t save that. Try again.'), 'err'); return; }
|
|
463
|
+
mine.you.shown_publicly = !!res.data.shown_publicly;
|
|
464
|
+
mine.you.counted_in_totals = !!res.data.counted_in_totals;
|
|
465
|
+
paint();
|
|
466
|
+
say(msg, done, 'ok');
|
|
467
|
+
return reread();
|
|
468
|
+
}, function () { btns.forEach(function (b) { b.disabled = false; }); say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
469
|
+
}
|
|
470
|
+
$('youForm').addEventListener('submit', function (ev) {
|
|
471
|
+
ev.preventDefault();
|
|
472
|
+
var you = mine.you;
|
|
473
|
+
var body = {};
|
|
474
|
+
var said = [];
|
|
475
|
+
var shown = picked('shown');
|
|
476
|
+
if (mine.guild.visibility === 'public' && shown && (shown === 'on') !== !!you.shown_publicly) {
|
|
477
|
+
body.shown_publicly = shown === 'on';
|
|
478
|
+
said.push(shown === 'on' ? 'you’re listed on its public page' : 'you’re off its public page');
|
|
479
|
+
// a member who answered here has answered the prompt too
|
|
480
|
+
dismissAsk();
|
|
481
|
+
}
|
|
482
|
+
var counted = picked('counted');
|
|
483
|
+
if (counted && (counted === 'on') !== !!you.counted_in_totals) {
|
|
484
|
+
body.counted_in_totals = counted === 'on';
|
|
485
|
+
said.push(counted === 'on' ? 'your numbers count toward its totals' : 'your numbers are left out of its totals');
|
|
486
|
+
}
|
|
487
|
+
if (!Object.keys(body).length) { say($('youMsg'), 'Nothing has changed.'); return; }
|
|
488
|
+
saveSwitches(body, 'Saved — ' + said.join(', and ') + '.', [$('youSave')]);
|
|
489
|
+
});
|
|
490
|
+
$('askYes').addEventListener('click', function () {
|
|
491
|
+
dismissAsk();
|
|
492
|
+
saveSwitches({ shown_publicly: true }, 'Saved — you’re listed on its public page.', [$('askYes'), $('askNo'), $('youSave')]);
|
|
493
|
+
});
|
|
494
|
+
$('askNo').addEventListener('click', function () {
|
|
495
|
+
dismissAsk();
|
|
496
|
+
$('showAsk').hidden = true;
|
|
497
|
+
$('shownBand').hidden = false;
|
|
498
|
+
say($('youMsg'), 'You’re staying off its public page. You can change that here at any time.');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// ---- the record ----
|
|
502
|
+
function paintRecord() {
|
|
503
|
+
var t = pub && pub.totals;
|
|
504
|
+
[['tShipped', 'works_shipped'], ['tCredits', 'credits'], ['tKarma', 'karma'], ['tProjects', 'projects']].forEach(function (pair) {
|
|
505
|
+
var dd = $(pair[0]);
|
|
506
|
+
if (!t) { dd.textContent = '—'; dd.className = 'under'; return; }
|
|
507
|
+
var b = band(t[pair[1]]);
|
|
508
|
+
dd.textContent = b.text;
|
|
509
|
+
dd.className = b.under ? 'under' : '';
|
|
510
|
+
});
|
|
511
|
+
$('gFigs').hidden = !pub;
|
|
512
|
+
var note;
|
|
513
|
+
if (!pub) note = 'This guild is unlisted, so its record isn’t published anywhere.';
|
|
514
|
+
else if (!t) note = 'Not counted yet. A guild’s record is counted once a week.';
|
|
515
|
+
else note = 'Counted on ' + dayMonthYear(t.as_of) + '. Projects are the public projects its listed members build on.';
|
|
516
|
+
$('gRecordNote').textContent = note;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---- the roster ----
|
|
520
|
+
function avatarOf(m) {
|
|
521
|
+
var url = typeof m.avatar_url === 'string' && /^https?:\/\//i.test(m.avatar_url) ? m.avatar_url : null;
|
|
522
|
+
if (url) {
|
|
523
|
+
var img = document.createElement('img');
|
|
524
|
+
img.className = 'gAv';
|
|
525
|
+
img.alt = '';
|
|
526
|
+
img.loading = 'lazy';
|
|
527
|
+
img.src = url;
|
|
528
|
+
return img;
|
|
529
|
+
}
|
|
530
|
+
var span = document.createElement('span');
|
|
531
|
+
span.className = 'gAv';
|
|
532
|
+
span.setAttribute('aria-hidden', 'true');
|
|
533
|
+
span.textContent = Array.from(String(m.display_name || m.handle || '?').trim())[0] || '?';
|
|
534
|
+
return span;
|
|
535
|
+
}
|
|
536
|
+
function rowOf(m) {
|
|
537
|
+
var li = document.createElement('li');
|
|
538
|
+
li.appendChild(avatarOf(m));
|
|
539
|
+
var who = document.createElement('span');
|
|
540
|
+
who.className = 'gWho';
|
|
541
|
+
var name = document.createElement('span');
|
|
542
|
+
name.className = 'gName';
|
|
543
|
+
name.textContent = m.display_name || m.handle;
|
|
544
|
+
var handle = document.createElement('span');
|
|
545
|
+
handle.className = 'gHandle';
|
|
546
|
+
handle.textContent = m.handle;
|
|
547
|
+
who.appendChild(name);
|
|
548
|
+
who.appendChild(handle);
|
|
549
|
+
li.appendChild(who);
|
|
550
|
+
var kind = document.createElement('span');
|
|
551
|
+
kind.className = 'gKind';
|
|
552
|
+
kind.textContent = m.membership_kind === 'owner' ? 'Owner' : '';
|
|
553
|
+
li.appendChild(kind);
|
|
554
|
+
return li;
|
|
555
|
+
}
|
|
556
|
+
function paintRoster() {
|
|
557
|
+
var list = mine ? mine.members : pub.members;
|
|
558
|
+
$('gMembersSide').textContent = mine
|
|
559
|
+
? 'Everyone in the guild. Only its members see this list; the public page lists only the members who chose to be on it.'
|
|
560
|
+
: 'The members who chose to be listed on this page.';
|
|
561
|
+
var ul = $('gRoster');
|
|
562
|
+
ul.textContent = '';
|
|
563
|
+
(list || []).forEach(function (m) { ul.appendChild(rowOf(m)); });
|
|
564
|
+
$('gRosterEmpty').hidden = !!(list && list.length);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
load();
|
|
568
|
+
})();
|
|
569
|
+
</script>
|
|
570
|
+
</body>
|
|
571
|
+
</html>
|