Member identity, promo tools, back-office color, seeded inventory
Real people, not numbers: usernames (unique, profile pane to set them), shown across the ledger, activity, rosters, and the sidebar chip; vanity invite links (/join/<username>) with late chain binding intact. Promo tools pane ships Branded Voice share posts and an email swipe personalized with each member's link. The earn viewer excludes a member's own campaigns, so nobody earns from their own spend. Back office gains the cyan/violet/amber accent family over the green base. Three house campaigns seeded so every surface shows live inventory. Assets v=20260905i. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+60
-2
@@ -33,7 +33,10 @@ function newCode(taken) {
|
||||
return c;
|
||||
}
|
||||
const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null,
|
||||
username: a.username || null, memberId: a.memberId || 0,
|
||||
address: a.address || null, created: a.created } : null;
|
||||
const USER_RE = /^[a-zA-Z0-9_]{3,20}$/;
|
||||
const normUser = u => String(u || '').trim().toLowerCase();
|
||||
|
||||
// ---- JSON fallback ----
|
||||
const J = {
|
||||
@@ -82,6 +85,29 @@ const J = {
|
||||
async byEmail(e) { return pub(this.db.byEmail[e]); },
|
||||
async byAddress(a) { const e = this.db.byAddress[a]; return e ? pub(this.db.byEmail[e]) : null; },
|
||||
async byCode(c) { const e = this.db.byCode[c]; return e ? pub(this.db.byEmail[e]) : null; },
|
||||
async byUsername(u) {
|
||||
for (const a of Object.values(this.db.byEmail)) if (a.username === u) return pub(a);
|
||||
return null;
|
||||
},
|
||||
async setUsername(e, u) {
|
||||
const acct = this.db.byEmail[e];
|
||||
if (!acct) return { error: 'No such account.' };
|
||||
for (const a of Object.values(this.db.byEmail)) if (a.username === u && a.email !== e)
|
||||
return { error: 'That username is taken. Try another.' };
|
||||
acct.username = u;
|
||||
this.save();
|
||||
return { ok: true, account: pub(acct) };
|
||||
},
|
||||
async setMemberId(e, id) {
|
||||
const acct = this.db.byEmail[e];
|
||||
if (acct && acct.memberId !== id) { acct.memberId = id; this.save(); }
|
||||
},
|
||||
async namesForMembers(ids) {
|
||||
const out = {};
|
||||
for (const a of Object.values(this.db.byEmail))
|
||||
if (a.username && a.memberId && ids.includes(a.memberId)) out[a.memberId] = a.username;
|
||||
return out;
|
||||
},
|
||||
async listByReferrer(refs) {
|
||||
const set = new Set(refs.filter(Boolean).map(String));
|
||||
return Object.values(this.db.byEmail)
|
||||
@@ -104,7 +130,8 @@ const J = {
|
||||
};
|
||||
|
||||
// ---- MySQL mode ----
|
||||
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, address: r.address, created: Number(r.created) }) : null;
|
||||
const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code,
|
||||
username: r.username, memberId: r.member_id || 0, address: r.address, created: Number(r.created) }) : null;
|
||||
const D = {
|
||||
async signup(e, password, ref) {
|
||||
const code = newCode();
|
||||
@@ -140,6 +167,24 @@ const D = {
|
||||
async byEmail(e) { const r = await db.q('SELECT * FROM accounts WHERE email=?', [e]); return rowPub(r[0]); },
|
||||
async byAddress(a) { const r = await db.q('SELECT * FROM accounts WHERE address=?', [a]); return rowPub(r[0]); },
|
||||
async byCode(c) { const r = await db.q('SELECT * FROM accounts WHERE code=?', [c]); return rowPub(r[0]); },
|
||||
async byUsername(u) { const r = await db.q('SELECT * FROM accounts WHERE username=?', [u]); return rowPub(r[0]); },
|
||||
async setUsername(e, u) {
|
||||
try { await db.q('UPDATE accounts SET username=? WHERE email=?', [u, e]); }
|
||||
catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') return { error: 'That username is taken. Try another.' };
|
||||
throw err;
|
||||
}
|
||||
return { ok: true, account: await this.byEmail(e) };
|
||||
},
|
||||
async setMemberId(e, id) { await db.q('UPDATE accounts SET member_id=? WHERE email=? AND (member_id IS NULL OR member_id<>?)', [id, e, id]); },
|
||||
async namesForMembers(ids) {
|
||||
if (!ids.length) return {};
|
||||
const rows = await db.q('SELECT member_id, username FROM accounts WHERE username IS NOT NULL AND member_id IN ('
|
||||
+ ids.map(() => '?').join(',') + ')', ids);
|
||||
const out = {};
|
||||
for (const r of rows) out[r.member_id] = r.username;
|
||||
return out;
|
||||
},
|
||||
async listByReferrer(refs) {
|
||||
const clean = refs.filter(Boolean).map(String);
|
||||
if (!clean.length) return [];
|
||||
@@ -180,6 +225,18 @@ async function ensure(email, sponsorRef) {
|
||||
async function byEmail(email) { return impl().byEmail(normEmail(email)); }
|
||||
async function byAddress(address) { return impl().byAddress(normAddr(address)); }
|
||||
async function byCode(code) { return impl().byCode(String(code || '').toLowerCase()); }
|
||||
async function byUsername(u) {
|
||||
const n = normUser(u);
|
||||
return USER_RE.test(n) ? impl().byUsername(n) : null;
|
||||
}
|
||||
async function setUsername(email, username) {
|
||||
const n = normUser(username);
|
||||
if (!USER_RE.test(n)) return { error: 'Usernames are 3 to 20 letters, numbers, or underscores.' };
|
||||
if (/^\d+$/.test(n)) return { error: 'Usernames need at least one letter.' }; // keep /join/<number> unambiguous
|
||||
return impl().setUsername(normEmail(email), n);
|
||||
}
|
||||
async function setMemberId(email, id) { return impl().setMemberId(normEmail(email), Number(id) || 0); }
|
||||
async function namesForMembers(ids) { return impl().namesForMembers([...new Set(ids)].filter(n => n > 0)); }
|
||||
async function listByReferrer(refs) { return impl().listByReferrer(refs || []); }
|
||||
async function linkWallet(email, address) {
|
||||
const a = normAddr(address);
|
||||
@@ -188,4 +245,5 @@ async function linkWallet(email, address) {
|
||||
}
|
||||
async function count() { return impl().count(); }
|
||||
|
||||
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, listByReferrer, linkWallet, count };
|
||||
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
|
||||
setUsername, setMemberId, namesForMembers, listByReferrer, linkWallet, count };
|
||||
|
||||
Reference in New Issue
Block a user