@bongos/core 1.21.56 → 1.21.58
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 +106 -46
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +13 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +2 -0
- package/docs/adr/0365-a-guild-engages-a-project-through-each-members-own-door.md +58 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +233 -6
- package/docs/api-reference.md +8 -5
- package/docs/copy-inventory.md +361 -315
- package/docs/copy-registry.json +868 -419
- package/docs/file-map.md +4 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +856 -771
- package/modules/platform-identity/application-echo.js +11 -5
- package/modules/platform-identity/guild-engagements.js +255 -0
- package/modules/platform-identity/guilds.js +3 -1
- package/modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql +64 -0
- package/modules/platform-identity/routes/guilds.js +35 -1
- package/modules/platform-identity/routes/my-projects.js +27 -3
- package/modules/platform-identity/routes/projects.js +44 -0
- package/modules/platform-identity/routes/sso.js +21 -3
- package/modules/public-landing/public/assets/cosmos.css +7 -0
- package/modules/public-landing/public/guild.html +633 -0
- package/modules/public-landing/public/guild.probes.json +63 -0
- package/modules/public-landing/public/guild.states.json +43 -0
- package/modules/public-landing/public/projects.html +252 -16
- package/modules/public-landing/public/projects.probes.json +26 -1
- package/modules/public-landing/public/projects.states.json +4 -0
- package/modules/ui-design/kit/fixtures/guilds.json +73 -0
- package/modules/ui-design/kit/fixtures/me__guild-engagements.json +7 -0
- package/modules/ui-design/kit/serve.js +99 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +13 -0
- package/tests/applicant_profile_boundary.mjs +104 -1
- package/tests/application_echo.mjs +91 -5
- package/tests/guild_engagement.mjs +881 -0
- package/tests/guild_engagement_db.mjs +240 -0
- package/tests/hub_engagement_cards.mjs +307 -0
- package/tests/hub_guild_page.mjs +624 -0
- package/tests/privacy_matrix.mjs +216 -14
- package/tests/project_invite_ui.mjs +187 -3
- package/tests/render_check.mjs +2 -2
- package/tests/ui_design_kit.mjs +3 -3
|
@@ -0,0 +1,633 @@
|
|
|
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 two: PATCH /guilds/:slug/membership (those two switches) and, for
|
|
17
|
+
the guild's owner only, POST /guilds/:slug/engagements (the guild applying to
|
|
18
|
+
a project, task 1002303, R27; ADR 0365 D2). A
|
|
19
|
+
stranger, an unknown slug and an unlisted guild seen from outside are one
|
|
20
|
+
"no guild here", so the page never confirms an unlisted guild exists.
|
|
21
|
+
Hub-only, like /account: platform-server.js serves /g/<slug> only where
|
|
22
|
+
platform-identity is on. noindex while one shell answers every slug (a guild
|
|
23
|
+
that does not exist would otherwise index as a page); putting guilds in
|
|
24
|
+
search is the SEO goal's call (ADR 0336 D6). Server-stamped: {{…}} tokens are
|
|
25
|
+
filled by landingPage(). -->
|
|
26
|
+
<title>{{productName}} — Guild</title>
|
|
27
|
+
<meta name="description" content="A guild on {{productName}}: the builders in it, and what they have built together.">
|
|
28
|
+
<meta name="robots" content="noindex">
|
|
29
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
30
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
31
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
|
|
32
|
+
<link rel="stylesheet" href="/assets/cosmos.css">
|
|
33
|
+
<!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
34
|
+
<style>
|
|
35
|
+
/* The guild page's own layout, in the account page's voice (account.html): a
|
|
36
|
+
lowercase light heading, hairline sections with the title left and the body
|
|
37
|
+
right, the choice cells with the dot. No :root block — the pack's injected
|
|
38
|
+
theme owns the tokens; everything below reads them. The bar and the bands are
|
|
39
|
+
cosmos.css's own rules, not restyled here. */
|
|
40
|
+
/* the skip link un-hides itself on focus, reversing everything .vh sets (the hub's rule) */
|
|
41
|
+
.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;
|
|
42
|
+
font-size:14px;font-weight:800;text-decoration:none;color:var(--ink);background:var(--bg-card);
|
|
43
|
+
border:2px solid var(--ctl-line);border-radius:12px;padding:10px 16px;}
|
|
44
|
+
.gld{position:relative;z-index:1;max-width:1060px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 56px;}
|
|
45
|
+
.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;}
|
|
46
|
+
.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;}
|
|
47
|
+
.gld .loading{font-size:15px;color:var(--ink-soft);}
|
|
48
|
+
.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);}
|
|
49
|
+
.gMeta{margin:16px 0 26px;font-variant-numeric:tabular-nums;}
|
|
50
|
+
.gMiss{padding:0 0 10px;max-width:62ch;}
|
|
51
|
+
.gMiss h1{margin-top:28px;}
|
|
52
|
+
.gMiss .lede{margin-bottom:22px;}
|
|
53
|
+
.gMiss .gh{width:18px;height:18px;fill:currentColor;}
|
|
54
|
+
.gMiss .btn{text-transform:none;}
|
|
55
|
+
/* the sections: the account page's .aSec grid */
|
|
56
|
+
.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;}
|
|
57
|
+
.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;}
|
|
58
|
+
.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);}
|
|
59
|
+
.aSec>.aBody{grid-column:2;grid-row:1/span 2;display:grid;gap:18px;align-content:start;min-width:0;}
|
|
60
|
+
.aLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
61
|
+
.aBand{display:grid;gap:8px;}
|
|
62
|
+
.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);}
|
|
63
|
+
.aOpt{position:relative;display:block;margin:0;padding:16px 18px 18px;border-left:1px solid var(--rule-soft);cursor:pointer;}
|
|
64
|
+
.aOpt:first-child{border-left:0;padding-left:0;}
|
|
65
|
+
.aOpt input{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);}
|
|
66
|
+
.aOpt>span{display:block;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
67
|
+
.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);}
|
|
68
|
+
.aOpt b::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
|
|
69
|
+
.aOpt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
70
|
+
.aOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
71
|
+
.aWhat{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-soft);max-width:70ch;}
|
|
72
|
+
.gld .actions{margin:0;gap:14px;}
|
|
73
|
+
.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;}
|
|
74
|
+
.gld .btn:hover,.gld .btn:active{transform:none;box-shadow:none;}
|
|
75
|
+
.gld .projMsg{margin:0;min-height:0;font-weight:400;}
|
|
76
|
+
/* the prompt after a guild goes public (ADR 0336 D4 §3): asked, never assumed */
|
|
77
|
+
.gAsk{display:grid;gap:14px;padding:18px 0 20px;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
78
|
+
.gAsk p{margin:0;font-size:14.5px;line-height:1.55;color:var(--ink-soft);max-width:62ch;}
|
|
79
|
+
.gAsk p b{display:block;margin-bottom:4px;font-family:var(--font-body);font-weight:500;font-size:17px;color:var(--ink);}
|
|
80
|
+
/* the owner's one field (apply to a project): the account page's .aField, a bare
|
|
81
|
+
input closed by one hairline that takes ink on focus */
|
|
82
|
+
.aField{display:grid;gap:4px;min-width:0;max-width:52ch;}
|
|
83
|
+
.aField input{width:100%;box-sizing:border-box;min-height:44px;font-family:var(--font-body);font-size:15.5px;font-weight:400;line-height:1.5;color:var(--ink);background:none;border:0;border-bottom:1px solid var(--ctl-line);border-radius:0;padding:8px 0;}
|
|
84
|
+
.aField input::placeholder{color:var(--ink-faint);}
|
|
85
|
+
.aField input:focus{outline:none;border-bottom-color:var(--ink);box-shadow:0 1px 0 var(--ink);}
|
|
86
|
+
/* the record: four band floors as figures, two by two so a long band ("200,000+")
|
|
87
|
+
keeps its cell, a hairline above each (the ledger grammar) */
|
|
88
|
+
.gFigs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px 28px;margin:0;}
|
|
89
|
+
.gFigs>div{display:grid;gap:6px;align-content:start;padding:14px 0 4px;border-top:1px solid var(--rule-soft);min-width:0;}
|
|
90
|
+
.gFigs dt{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
91
|
+
.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;}
|
|
92
|
+
.gFigs dd.under{font-size:clamp(20px,2vw,26px);letter-spacing:-.02em;color:var(--ink-soft);}
|
|
93
|
+
/* the roster: one hairline row per member */
|
|
94
|
+
.gRoster{list-style:none;margin:0;padding:0;}
|
|
95
|
+
.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);}
|
|
96
|
+
.gRoster li:last-child{border-bottom:1px solid var(--rule-soft);}
|
|
97
|
+
.gRoster .gAv{width:36px;height:36px;border-radius:50%;object-fit:cover;background:var(--bg-card);}
|
|
98
|
+
.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;}
|
|
99
|
+
.gRoster .gWho{display:grid;gap:2px;min-width:0;}
|
|
100
|
+
.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;}
|
|
101
|
+
.gRoster .gHandle{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
102
|
+
.gRoster .gKind{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-soft);}
|
|
103
|
+
@media (max-width:900px){
|
|
104
|
+
.aSec{grid-template-columns:1fr;gap:12px;}
|
|
105
|
+
.aSec>h2,.aSec>.aSide,.aSec>.aBody{grid-column:1;grid-row:auto;}
|
|
106
|
+
.aSec>.aSide{max-width:none;}
|
|
107
|
+
}
|
|
108
|
+
@media (max-width:560px){
|
|
109
|
+
.gFigs{gap:0 18px;}
|
|
110
|
+
.aBand [role="radiogroup"]{grid-template-columns:1fr;}
|
|
111
|
+
.aOpt{padding:14px 0 16px;border-left:0;border-top:1px solid var(--rule-soft);}
|
|
112
|
+
.aOpt:first-child{border-top:0;}
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
115
|
+
</head>
|
|
116
|
+
<body>
|
|
117
|
+
<!-- THE BAR and its two bands are the hub's own (projects.html, task 1004455):
|
|
118
|
+
the account page's markup with no current place, so /projects → /g/<slug>
|
|
119
|
+
keeps the chrome. assets/hub-bar.js runs it. The sign-in doors name no return
|
|
120
|
+
in the markup — one shell serves every slug — so the page script points them
|
|
121
|
+
back at this guild once it has read the slug (and without script, the sign-in
|
|
122
|
+
route falls back to the Referer, which is this page). -->
|
|
123
|
+
<a class="skip vh" href="#guild">Skip to content</a>
|
|
124
|
+
<header class="shell bar">
|
|
125
|
+
<div class="barLeft">
|
|
126
|
+
<a class="barMark" id="barMark" href="/">
|
|
127
|
+
<svg viewBox="0 0 26 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.4"
|
|
128
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
129
|
+
<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>
|
|
130
|
+
<path d="M1.8 5.6v6.6a6.6 2.9 0 0 0 13.2 0V5.6"/>
|
|
131
|
+
<ellipse cx="19.6" cy="8.4" rx="4.7" ry="2.1"/>
|
|
132
|
+
<path d="M14.9 8.4v4.6a4.7 2.1 0 0 0 9.4 0V8.4"/>
|
|
133
|
+
</svg>
|
|
134
|
+
<span>{{productName}}</span>
|
|
135
|
+
</a>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="barRight">
|
|
138
|
+
<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>
|
|
139
|
+
<!-- the identity chip IS the profile trigger; assets/hub-bar.js fills it from GET /me.
|
|
140
|
+
Signed out it stays hidden and the sign-in door takes its slot. -->
|
|
141
|
+
<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>
|
|
142
|
+
<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?repos=1">Sign in</a>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
147
|
+
<div class="bandIn">
|
|
148
|
+
<ul class="bandRow">
|
|
149
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map">Sky</a><span class="bandC">the sky of every project</span></li>
|
|
150
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
151
|
+
<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>
|
|
152
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
153
|
+
</ul>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
157
|
+
<div class="bandIn bandProfile">
|
|
158
|
+
<p class="bandWho" id="bandWho"><img id="bandWhoImg" alt="" hidden><span class="whoName" id="bandWhoLogin"></span></p>
|
|
159
|
+
<ul class="bandRow">
|
|
160
|
+
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
161
|
+
<li class="bandCell"><a class="bandT" href="/account">Account</a></li>
|
|
162
|
+
<!-- the honest hover target; the delegated click upgrades it to the POST -->
|
|
163
|
+
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
164
|
+
</ul>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<main class="gld" id="guild">
|
|
169
|
+
<p class="loading" id="loading">Reading this guild…</p>
|
|
170
|
+
<div class="alert" id="pageAlert" role="alert"></div>
|
|
171
|
+
|
|
172
|
+
<!-- NO GUILD HERE: an unknown slug, a malformed one, and an unlisted guild seen
|
|
173
|
+
by anyone but its members get this one answer (ADR 0336 D4). Signed out,
|
|
174
|
+
it offers sign-in, because a member reaches an unlisted guild that way. -->
|
|
175
|
+
<section class="gMiss" id="missing" hidden aria-labelledby="missing-h">
|
|
176
|
+
<h1 id="missing-h">no guild here</h1>
|
|
177
|
+
<p class="lede" id="missingWhy"></p>
|
|
178
|
+
<a class="btn" id="missingSignin" hidden href="/api/bongos/auth/web/start?repos=1">
|
|
179
|
+
<!-- keep the spaces inside this path's numbers: same shape, and they are load-bearing (task 1004401) -->
|
|
180
|
+
<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>
|
|
181
|
+
Sign in with GitHub
|
|
182
|
+
</a>
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
<!-- THE GUILD: ships hidden, revealed once either read answers -->
|
|
186
|
+
<div id="guildBody" hidden>
|
|
187
|
+
<p class="gKicker" id="gKicker">Guild</p>
|
|
188
|
+
<h1 id="gName"></h1>
|
|
189
|
+
<p class="lede" id="gDesc" hidden></p>
|
|
190
|
+
<p class="gMeta" id="gMeta"></p>
|
|
191
|
+
|
|
192
|
+
<!-- YOUR PLACE — members only, from the own-scope read. Two switches, each the
|
|
193
|
+
member's own (ADR 0336 D4, ADR 0337 D4.4): shown on the public page, and
|
|
194
|
+
counted in the totals. Only what changed is sent. -->
|
|
195
|
+
<section class="aSec" id="gYou" hidden aria-labelledby="gYou-h">
|
|
196
|
+
<h2 id="gYou-h">your place</h2>
|
|
197
|
+
<p class="aSide" id="gYouSide"></p>
|
|
198
|
+
<form class="aBody" id="youForm" novalidate>
|
|
199
|
+
<!-- the prompt after an unlisted → public flip: a member who joined an
|
|
200
|
+
unlisted guild is not shown on its public page until they say yes -->
|
|
201
|
+
<div class="gAsk" id="showAsk" hidden>
|
|
202
|
+
<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>
|
|
203
|
+
<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>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="aBand" id="shownBand">
|
|
206
|
+
<span class="aLabel" id="shownLabel">On this guild’s public page</span>
|
|
207
|
+
<div role="radiogroup" aria-labelledby="shownLabel">
|
|
208
|
+
<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>
|
|
209
|
+
<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>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<p class="aWhat" id="shownUnlisted" hidden></p>
|
|
213
|
+
<div class="aBand" id="countBand">
|
|
214
|
+
<span class="aLabel" id="countLabel">In this guild’s totals</span>
|
|
215
|
+
<div role="radiogroup" aria-labelledby="countLabel">
|
|
216
|
+
<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>
|
|
217
|
+
<label class="aOpt"><input type="radio" name="counted" value="off"><span><b>Leave them out</b>The totals add up everyone else.</span></label>
|
|
218
|
+
</div>
|
|
219
|
+
<p class="aWhat" id="countWhat"></p>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="actions"><button type="submit" class="btn" id="youSave">save</button></div>
|
|
222
|
+
<p class="projMsg" id="youMsg" role="status" aria-live="polite"></p>
|
|
223
|
+
</form>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
<!-- APPLY TO A PROJECT — the guild's owner only (task 1002303, R27; ADR 0365
|
|
227
|
+
D2): POST /guilds/:slug/engagements { origin }. It applies for nobody —
|
|
228
|
+
each member sees it in their invitations on the projects page and applies
|
|
229
|
+
as themselves, the owner too. A project that is unknown, not on the map or
|
|
230
|
+
not public gets one answer, so the page never says which. -->
|
|
231
|
+
<section class="aSec" id="gApply" hidden aria-labelledby="gApply-h">
|
|
232
|
+
<h2 id="gApply-h">apply to a project</h2>
|
|
233
|
+
<p class="aSide">Put this guild forward for a project. It applies for nobody: each member sees it in their invitations and applies as themselves, you included.</p>
|
|
234
|
+
<form class="aBody" id="applyForm" novalidate>
|
|
235
|
+
<label class="aField"><span class="aLabel">Project address</span>
|
|
236
|
+
<input type="url" id="applyOrigin" inputmode="url" autocomplete="off" spellcheck="false" placeholder="https://project.example.com"></label>
|
|
237
|
+
<div class="actions"><button type="submit" class="btn" id="applySend">apply as this guild</button></div>
|
|
238
|
+
<p class="projMsg" id="applyMsg" role="status" aria-live="polite"></p>
|
|
239
|
+
</form>
|
|
240
|
+
</section>
|
|
241
|
+
|
|
242
|
+
<!-- THE RECORD — R26's rounded weekly totals (ADR 0337 D4.4), from the public
|
|
243
|
+
read only: band floors ("100+"), never an exact sum, and no count of who
|
|
244
|
+
is in them. An unlisted guild publishes none. -->
|
|
245
|
+
<section class="aSec" id="gRecord" aria-labelledby="gRecord-h">
|
|
246
|
+
<h2 id="gRecord-h">the record</h2>
|
|
247
|
+
<p class="aSide">What its members have built together, across every project. Rounded down to a band, and counted once a week.</p>
|
|
248
|
+
<div class="aBody">
|
|
249
|
+
<dl class="gFigs" id="gFigs">
|
|
250
|
+
<div><dt>Works shipped</dt><dd id="tShipped">—</dd></div>
|
|
251
|
+
<div><dt>Credits</dt><dd id="tCredits">—</dd></div>
|
|
252
|
+
<div><dt>Karma</dt><dd id="tKarma">—</dd></div>
|
|
253
|
+
<div><dt>Projects</dt><dd id="tProjects">—</dd></div>
|
|
254
|
+
</dl>
|
|
255
|
+
<p class="aWhat" id="gRecordNote"></p>
|
|
256
|
+
</div>
|
|
257
|
+
</section>
|
|
258
|
+
|
|
259
|
+
<!-- MEMBERS — the public roster (ADR 0336 D4: handle, display name, avatar,
|
|
260
|
+
kind; never a GitHub id or login), or, for a member, the members' roster,
|
|
261
|
+
which lists everyone in the guild. -->
|
|
262
|
+
<section class="aSec" id="gMembers" aria-labelledby="gMembers-h">
|
|
263
|
+
<h2 id="gMembers-h">members</h2>
|
|
264
|
+
<p class="aSide" id="gMembersSide"></p>
|
|
265
|
+
<div class="aBody">
|
|
266
|
+
<ul class="gRoster" id="gRoster"></ul>
|
|
267
|
+
<p class="aWhat" id="gRosterEmpty" hidden>Nobody is listed on this page yet.</p>
|
|
268
|
+
</div>
|
|
269
|
+
</section>
|
|
270
|
+
</div>
|
|
271
|
+
</main>
|
|
272
|
+
|
|
273
|
+
<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>
|
|
274
|
+
|
|
275
|
+
<script src="/assets/hub-bar.js"></script>
|
|
276
|
+
<script>
|
|
277
|
+
(function () {
|
|
278
|
+
'use strict';
|
|
279
|
+
var API = '/api/bongos';
|
|
280
|
+
function $(id) { return document.getElementById(id); }
|
|
281
|
+
// The bar's chip, bands and sign out are assets/hub-bar.js (task 1004489), loaded
|
|
282
|
+
// just above; bar.ready settles with GET /me's answer, null when nobody is
|
|
283
|
+
// signed in. Without the asset the page still shows the public guild.
|
|
284
|
+
var bar = window.HubBar || { paint: function () {}, ready: Promise.resolve(null) };
|
|
285
|
+
|
|
286
|
+
// The slug rule is guilds.js's (normalizeHandle's charset, a two-character
|
|
287
|
+
// floor, at most 39) — boundary-forced duplication (ADR 0091 §2): a page cannot
|
|
288
|
+
// import a module. A path that cannot be a slug is "no guild here" without a
|
|
289
|
+
// request. tests/hub_guild_page.mjs holds this to the module's rule.
|
|
290
|
+
var SLUG_RE = /^(?=.{2,39}$)[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/;
|
|
291
|
+
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
292
|
+
var BASE_TITLE = document.title;
|
|
293
|
+
|
|
294
|
+
// What the server last said: the public read and the own-scope read. Every
|
|
295
|
+
// save re-paints from the server's answer, never from what was clicked.
|
|
296
|
+
var slug = null;
|
|
297
|
+
var pub = null;
|
|
298
|
+
var mine = null;
|
|
299
|
+
|
|
300
|
+
function request(method, path, body) {
|
|
301
|
+
var init = { method: method, credentials: 'same-origin', headers: { Accept: 'application/json' } };
|
|
302
|
+
if (body !== undefined) { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
303
|
+
return fetch(API + path, init).then(function (res) {
|
|
304
|
+
return res.json().catch(function () { return null; }).then(function (data) {
|
|
305
|
+
return { status: res.status, ok: res.ok, data: data };
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function serverWords(res, fallback) {
|
|
310
|
+
var m = res && res.data && res.data.error && res.data.error.message;
|
|
311
|
+
if (typeof m !== 'string' || !m) return fallback;
|
|
312
|
+
return m.charAt(0).toUpperCase() + m.slice(1) + (/[.!?]$/.test(m) ? '' : '.');
|
|
313
|
+
}
|
|
314
|
+
function say(el, text, kind) {
|
|
315
|
+
el.className = 'projMsg' + (kind ? ' ' + kind : '');
|
|
316
|
+
el.textContent = text;
|
|
317
|
+
}
|
|
318
|
+
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many); }
|
|
319
|
+
function monthYear(iso) {
|
|
320
|
+
var d = new Date(iso);
|
|
321
|
+
return isNaN(d.getTime()) ? '' : MONTHS[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
|
|
322
|
+
}
|
|
323
|
+
function dayMonthYear(iso) {
|
|
324
|
+
var d = new Date(iso);
|
|
325
|
+
return isNaN(d.getTime()) ? '' : d.getUTCDate() + ' ' + MONTHS[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
|
|
326
|
+
}
|
|
327
|
+
// A total is published as its band floor (guild-totals.js bandFloor): 0 means
|
|
328
|
+
// below the first band, which a page says as "under 10".
|
|
329
|
+
function band(n) {
|
|
330
|
+
var v = Number(n);
|
|
331
|
+
if (!isFinite(v) || v <= 0) return { text: 'under 10', under: true };
|
|
332
|
+
return { text: Math.floor(v).toLocaleString('en-US') + '+', under: false };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---- the slug, and the sign-in doors that return to it ----
|
|
336
|
+
function slugFromPath(pathname) {
|
|
337
|
+
var m = /^\/g\/([^/]+)\/?$/.exec(pathname || '');
|
|
338
|
+
if (!m) return null;
|
|
339
|
+
var s;
|
|
340
|
+
try { s = decodeURIComponent(m[1]).toLowerCase(); } catch (e) { return null; }
|
|
341
|
+
return SLUG_RE.test(s) ? s : null;
|
|
342
|
+
}
|
|
343
|
+
function pointSigninHere() {
|
|
344
|
+
// the slug is SLUG_RE-clean ([a-z0-9-]), so it needs no encoding: the shape the
|
|
345
|
+
// other hub doors send (return=/account&repos=1)
|
|
346
|
+
var href = API + '/auth/web/start?return=/g/' + slug + '&repos=1';
|
|
347
|
+
$('signinTop').setAttribute('href', href);
|
|
348
|
+
$('missingSignin').setAttribute('href', href);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- the page's states: reading, no guild here, the guild ----
|
|
352
|
+
function showAlert(text) {
|
|
353
|
+
$('loading').hidden = true;
|
|
354
|
+
var a = $('pageAlert');
|
|
355
|
+
a.textContent = text;
|
|
356
|
+
a.className = 'alert on';
|
|
357
|
+
}
|
|
358
|
+
function showMissing(signedIn) {
|
|
359
|
+
$('loading').hidden = true;
|
|
360
|
+
$('guildBody').hidden = true;
|
|
361
|
+
$('missingWhy').textContent = signedIn
|
|
362
|
+
? 'There is no guild at this address that you can see. A guild that is unlisted is shown only to its members.'
|
|
363
|
+
: 'There is no public guild at this address. If it is an unlisted guild you belong to, sign in to see it.';
|
|
364
|
+
$('missingSignin').hidden = !!signedIn;
|
|
365
|
+
$('missing').hidden = false;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function load() {
|
|
369
|
+
slug = slugFromPath(location.pathname);
|
|
370
|
+
if (!slug) { return bar.ready.then(function (me) { showMissing(!!me); }); }
|
|
371
|
+
pointSigninHere();
|
|
372
|
+
return Promise.all([
|
|
373
|
+
request('GET', '/guilds/' + slug),
|
|
374
|
+
// the own-scope read only for someone signed in: a stranger's page makes one call
|
|
375
|
+
bar.ready.then(function (me) { return me ? request('GET', '/guilds/' + slug + '/members') : null; }),
|
|
376
|
+
]).then(function (both) {
|
|
377
|
+
var p = both[0], m = both[1];
|
|
378
|
+
var signedIn = !!m && m.status !== 401;
|
|
379
|
+
pub = p.ok && p.data && p.data.guild ? p.data : null;
|
|
380
|
+
mine = m && m.ok && m.data && m.data.guild ? m.data : null;
|
|
381
|
+
if (!pub && !mine) {
|
|
382
|
+
if (p.status === 404 && (!m || m.status === 404 || m.status === 401)) { showMissing(signedIn); return; }
|
|
383
|
+
showAlert('Couldn’t read this guild right now — refresh in a moment.');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
paint();
|
|
387
|
+
$('loading').hidden = true;
|
|
388
|
+
$('missing').hidden = true;
|
|
389
|
+
$('guildBody').hidden = false;
|
|
390
|
+
}, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ---- paint: every region from the server's answers ----
|
|
394
|
+
function paint() { paintHead(); paintYou(); paintApply(); paintRecord(); paintRoster(); }
|
|
395
|
+
|
|
396
|
+
function guild() { return (mine && mine.guild) || pub.guild; }
|
|
397
|
+
|
|
398
|
+
function paintHead() {
|
|
399
|
+
var g = guild();
|
|
400
|
+
document.title = g.name + ' — ' + BASE_TITLE.replace(/ — Guild$/, '');
|
|
401
|
+
$('gName').textContent = g.name;
|
|
402
|
+
var desc = g.description ? String(g.description).trim() : '';
|
|
403
|
+
$('gDesc').textContent = desc;
|
|
404
|
+
$('gDesc').hidden = !desc;
|
|
405
|
+
// a member is told which kind of guild this is; the public sees only public ones
|
|
406
|
+
$('gKicker').textContent = mine ? 'Guild · ' + (mine.guild.visibility === 'public' ? 'public' : 'unlisted') : 'Guild';
|
|
407
|
+
var parts = [];
|
|
408
|
+
if (mine) {
|
|
409
|
+
parts.push(plural(mine.count, 'member', 'members'));
|
|
410
|
+
if (pub) parts.push(pub.size + ' on the public page');
|
|
411
|
+
} else {
|
|
412
|
+
parts.push(plural(pub.size, 'member', 'members'));
|
|
413
|
+
}
|
|
414
|
+
var since = pub && pub.guild.created_at ? monthYear(pub.guild.created_at) : '';
|
|
415
|
+
if (since) parts.push('since ' + since);
|
|
416
|
+
$('gMeta').textContent = parts.join(' · ');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ---- your place: the member's own two switches ----
|
|
420
|
+
var ASK_KEY = 'cb-guild-ask-';
|
|
421
|
+
function askDismissed() {
|
|
422
|
+
try { return window.localStorage.getItem(ASK_KEY + slug) === 'no'; } catch (e) { return false; }
|
|
423
|
+
}
|
|
424
|
+
function dismissAsk() {
|
|
425
|
+
try { window.localStorage.setItem(ASK_KEY + slug, 'no'); } catch (e) { /* a per-viewer convenience only */ }
|
|
426
|
+
}
|
|
427
|
+
function pick(name, value) {
|
|
428
|
+
document.querySelectorAll('input[name="' + name + '"]').forEach(function (r) { r.checked = r.value === value; });
|
|
429
|
+
}
|
|
430
|
+
function picked(name) {
|
|
431
|
+
var r = document.querySelector('input[name="' + name + '"]:checked');
|
|
432
|
+
return r ? r.value : null;
|
|
433
|
+
}
|
|
434
|
+
function paintYou() {
|
|
435
|
+
if (!mine) { $('gYou').hidden = true; return; }
|
|
436
|
+
var you = mine.you;
|
|
437
|
+
var isPublic = mine.guild.visibility === 'public';
|
|
438
|
+
$('gYou').hidden = false;
|
|
439
|
+
$('gYouSide').textContent = (you.membership_kind === 'owner' ? 'You own this guild.' : 'You’re a member of this guild.') +
|
|
440
|
+
' Only you see this, and only you can change it.';
|
|
441
|
+
// Asked, never assumed: while the prompt stands it IS the control, so the pair
|
|
442
|
+
// for the same switch waits until it is answered.
|
|
443
|
+
var asking = isPublic && !you.shown_publicly && !askDismissed();
|
|
444
|
+
$('showAsk').hidden = !asking;
|
|
445
|
+
$('shownBand').hidden = !isPublic || asking;
|
|
446
|
+
$('shownUnlisted').hidden = isPublic;
|
|
447
|
+
// A visibility change flips only the OWNER's own flag (ADR 0336 D4 §3), so the
|
|
448
|
+
// owner is listed the moment they make it public; everyone else is asked.
|
|
449
|
+
$('shownUnlisted').textContent = 'This guild is unlisted, so it has no public page. ' + (you.membership_kind === 'owner'
|
|
450
|
+
? 'If you make it public, you’ll be listed on it as its owner, and each member will be asked before they are.'
|
|
451
|
+
: 'If it is made public, you’ll be asked here before you’re listed on it.');
|
|
452
|
+
pick('shown', you.shown_publicly ? 'on' : 'off');
|
|
453
|
+
pick('counted', you.counted_in_totals ? 'on' : 'off');
|
|
454
|
+
paintCountWhat();
|
|
455
|
+
}
|
|
456
|
+
// The notice IS the consent (ADR 0337 D4.4): said for the choice as it stands
|
|
457
|
+
// on the page, before anything is saved.
|
|
458
|
+
function paintCountWhat() {
|
|
459
|
+
$('countWhat').textContent = picked('counted') === 'off'
|
|
460
|
+
? 'Your numbers are left out of this guild’s totals, from its next weekly count.'
|
|
461
|
+
: 'Your numbers count toward this guild’s rounded totals — even while your record is hidden or your account is private. ' +
|
|
462
|
+
'They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. ' +
|
|
463
|
+
'Leave them out of any guild you’d rather not share them with.';
|
|
464
|
+
}
|
|
465
|
+
document.querySelectorAll('input[name="counted"]').forEach(function (r) { r.addEventListener('change', paintCountWhat); });
|
|
466
|
+
|
|
467
|
+
// Re-read both after a save: the public roster and size move with the switch.
|
|
468
|
+
function reread() {
|
|
469
|
+
return Promise.all([
|
|
470
|
+
request('GET', '/guilds/' + slug),
|
|
471
|
+
request('GET', '/guilds/' + slug + '/members'),
|
|
472
|
+
]).then(function (both) {
|
|
473
|
+
if (both[0].ok && both[0].data && both[0].data.guild) pub = both[0].data;
|
|
474
|
+
else if (both[0].status === 404) pub = null;
|
|
475
|
+
if (both[1].ok && both[1].data && both[1].data.guild) mine = both[1].data;
|
|
476
|
+
paint();
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function saveSwitches(body, done, btns) {
|
|
480
|
+
var msg = $('youMsg');
|
|
481
|
+
btns.forEach(function (b) { b.disabled = true; });
|
|
482
|
+
say(msg, 'Saving…');
|
|
483
|
+
return request('PATCH', '/guilds/' + slug + '/membership', body).then(function (res) {
|
|
484
|
+
btns.forEach(function (b) { b.disabled = false; });
|
|
485
|
+
if (res.status === 401) { say(msg, 'Your session expired — sign in again to save.', 'err'); bar.paint(null); return; }
|
|
486
|
+
if (!res.ok) { say(msg, serverWords(res, 'Couldn’t save that. Try again.'), 'err'); return; }
|
|
487
|
+
mine.you.shown_publicly = !!res.data.shown_publicly;
|
|
488
|
+
mine.you.counted_in_totals = !!res.data.counted_in_totals;
|
|
489
|
+
paint();
|
|
490
|
+
say(msg, done, 'ok');
|
|
491
|
+
return reread();
|
|
492
|
+
}, function () { btns.forEach(function (b) { b.disabled = false; }); say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
493
|
+
}
|
|
494
|
+
$('youForm').addEventListener('submit', function (ev) {
|
|
495
|
+
ev.preventDefault();
|
|
496
|
+
var you = mine.you;
|
|
497
|
+
var body = {};
|
|
498
|
+
var said = [];
|
|
499
|
+
var shown = picked('shown');
|
|
500
|
+
if (mine.guild.visibility === 'public' && shown && (shown === 'on') !== !!you.shown_publicly) {
|
|
501
|
+
body.shown_publicly = shown === 'on';
|
|
502
|
+
said.push(shown === 'on' ? 'you’re listed on its public page' : 'you’re off its public page');
|
|
503
|
+
// a member who answered here has answered the prompt too
|
|
504
|
+
dismissAsk();
|
|
505
|
+
}
|
|
506
|
+
var counted = picked('counted');
|
|
507
|
+
if (counted && (counted === 'on') !== !!you.counted_in_totals) {
|
|
508
|
+
body.counted_in_totals = counted === 'on';
|
|
509
|
+
said.push(counted === 'on' ? 'your numbers count toward its totals' : 'your numbers are left out of its totals');
|
|
510
|
+
}
|
|
511
|
+
if (!Object.keys(body).length) { say($('youMsg'), 'Nothing has changed.'); return; }
|
|
512
|
+
saveSwitches(body, 'Saved — ' + said.join(', and ') + '.', [$('youSave')]);
|
|
513
|
+
});
|
|
514
|
+
$('askYes').addEventListener('click', function () {
|
|
515
|
+
dismissAsk();
|
|
516
|
+
saveSwitches({ shown_publicly: true }, 'Saved — you’re listed on its public page.', [$('askYes'), $('askNo'), $('youSave')]);
|
|
517
|
+
});
|
|
518
|
+
$('askNo').addEventListener('click', function () {
|
|
519
|
+
dismissAsk();
|
|
520
|
+
$('showAsk').hidden = true;
|
|
521
|
+
$('shownBand').hidden = false;
|
|
522
|
+
say($('youMsg'), 'You’re staying off its public page. You can change that here at any time.');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// ---- apply to a project: the owner files, and it applies for nobody ----
|
|
526
|
+
// Shown from the own-scope read only, to the owner; the route refuses anyone
|
|
527
|
+
// else on its own (a member is told the act is the owner's, a stranger 404s).
|
|
528
|
+
function paintApply() {
|
|
529
|
+
$('gApply').hidden = !(mine && mine.you.membership_kind === 'owner');
|
|
530
|
+
}
|
|
531
|
+
$('applyForm').addEventListener('submit', function (ev) {
|
|
532
|
+
ev.preventDefault();
|
|
533
|
+
var msg = $('applyMsg');
|
|
534
|
+
var field = $('applyOrigin');
|
|
535
|
+
var origin = String(field.value || '').trim();
|
|
536
|
+
if (!origin) { say(msg, 'Enter the project’s address.', 'err'); return; }
|
|
537
|
+
// the hub's join field's rule: a bare host is assumed https://, the form the
|
|
538
|
+
// server's exact origin lookup matches
|
|
539
|
+
if (!/^https?:\/\//i.test(origin)) origin = 'https://' + origin;
|
|
540
|
+
var btn = $('applySend');
|
|
541
|
+
btn.disabled = true;
|
|
542
|
+
say(msg, 'Sending…');
|
|
543
|
+
return request('POST', '/guilds/' + slug + '/engagements', { origin: origin }).then(function (res) {
|
|
544
|
+
btn.disabled = false;
|
|
545
|
+
if (res.status === 401) { say(msg, 'Your session expired — sign in again to send this.', 'err'); bar.paint(null); return; }
|
|
546
|
+
var e = res.ok && res.data && res.data.engagement;
|
|
547
|
+
if (e) {
|
|
548
|
+
field.value = '';
|
|
549
|
+
var gName = (e.guild && e.guild.name) || guild().name;
|
|
550
|
+
var pName = (e.project && e.project.name) || origin;
|
|
551
|
+
say(msg, 'Done. ' + gName + '’s members will see ' + pName + ' in their invitations, and each one applies as themselves. ' +
|
|
552
|
+
'That includes you — this didn’t apply for you.', 'ok');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// unknown, inactive, off the map or not public are ONE refusal on the
|
|
556
|
+
// server, so they are one line here: it never says a private project exists
|
|
557
|
+
var code = res.data && res.data.error && res.data.error.code;
|
|
558
|
+
if (res.status === 404 && code === 'unknown_project') { say(msg, 'No public project on the map at that address.', 'err'); return; }
|
|
559
|
+
say(msg, serverWords(res, 'Couldn’t send that. Try again.'), 'err');
|
|
560
|
+
}, function () { btn.disabled = false; say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ---- the record ----
|
|
564
|
+
function paintRecord() {
|
|
565
|
+
var t = pub && pub.totals;
|
|
566
|
+
[['tShipped', 'works_shipped'], ['tCredits', 'credits'], ['tKarma', 'karma'], ['tProjects', 'projects']].forEach(function (pair) {
|
|
567
|
+
var dd = $(pair[0]);
|
|
568
|
+
if (!t) { dd.textContent = '—'; dd.className = 'under'; return; }
|
|
569
|
+
var b = band(t[pair[1]]);
|
|
570
|
+
dd.textContent = b.text;
|
|
571
|
+
dd.className = b.under ? 'under' : '';
|
|
572
|
+
});
|
|
573
|
+
$('gFigs').hidden = !pub;
|
|
574
|
+
var note;
|
|
575
|
+
if (!pub) note = 'This guild is unlisted, so its record isn’t published anywhere.';
|
|
576
|
+
else if (!t) note = 'Not counted yet. A guild’s record is counted once a week.';
|
|
577
|
+
else note = 'Counted on ' + dayMonthYear(t.as_of) + '. Projects are the public projects its listed members build on.';
|
|
578
|
+
$('gRecordNote').textContent = note;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ---- the roster ----
|
|
582
|
+
function avatarOf(m) {
|
|
583
|
+
var url = typeof m.avatar_url === 'string' && /^https?:\/\//i.test(m.avatar_url) ? m.avatar_url : null;
|
|
584
|
+
if (url) {
|
|
585
|
+
var img = document.createElement('img');
|
|
586
|
+
img.className = 'gAv';
|
|
587
|
+
img.alt = '';
|
|
588
|
+
img.loading = 'lazy';
|
|
589
|
+
img.src = url;
|
|
590
|
+
return img;
|
|
591
|
+
}
|
|
592
|
+
var span = document.createElement('span');
|
|
593
|
+
span.className = 'gAv';
|
|
594
|
+
span.setAttribute('aria-hidden', 'true');
|
|
595
|
+
span.textContent = Array.from(String(m.display_name || m.handle || '?').trim())[0] || '?';
|
|
596
|
+
return span;
|
|
597
|
+
}
|
|
598
|
+
function rowOf(m) {
|
|
599
|
+
var li = document.createElement('li');
|
|
600
|
+
li.appendChild(avatarOf(m));
|
|
601
|
+
var who = document.createElement('span');
|
|
602
|
+
who.className = 'gWho';
|
|
603
|
+
var name = document.createElement('span');
|
|
604
|
+
name.className = 'gName';
|
|
605
|
+
name.textContent = m.display_name || m.handle;
|
|
606
|
+
var handle = document.createElement('span');
|
|
607
|
+
handle.className = 'gHandle';
|
|
608
|
+
handle.textContent = m.handle;
|
|
609
|
+
who.appendChild(name);
|
|
610
|
+
who.appendChild(handle);
|
|
611
|
+
li.appendChild(who);
|
|
612
|
+
var kind = document.createElement('span');
|
|
613
|
+
kind.className = 'gKind';
|
|
614
|
+
kind.textContent = m.membership_kind === 'owner' ? 'Owner' : '';
|
|
615
|
+
li.appendChild(kind);
|
|
616
|
+
return li;
|
|
617
|
+
}
|
|
618
|
+
function paintRoster() {
|
|
619
|
+
var list = mine ? mine.members : pub.members;
|
|
620
|
+
$('gMembersSide').textContent = mine
|
|
621
|
+
? 'Everyone in the guild. Only its members see this list; the public page lists only the members who chose to be on it.'
|
|
622
|
+
: 'The members who chose to be listed on this page.';
|
|
623
|
+
var ul = $('gRoster');
|
|
624
|
+
ul.textContent = '';
|
|
625
|
+
(list || []).forEach(function (m) { ul.appendChild(rowOf(m)); });
|
|
626
|
+
$('gRosterEmpty').hidden = !!(list && list.length);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
load();
|
|
630
|
+
})();
|
|
631
|
+
</script>
|
|
632
|
+
</body>
|
|
633
|
+
</html>
|