Each category gets a fixed share of the snapshot's PLAA reward pool, so when fewer people contribute to a category, every contribution there earns a larger slice. Learn more about our incentive model.
Each category gets a fixed share of the period's PLAA reward pool, so when fewer people contribute to a category, every contribution there collects a larger slice. Learn more about our incentive model.
@@ -2481,6 +2689,16 @@
Give Community Kudos
persona: 'active',
points: 1240,
homeVariant: 'a',
+ ovTab: 'intro',
+ howStep: 'contribute',
+ chartHidden: {},
+ chartDrag: null,
+ chartHover: null,
+ navDrag: null,
+ navHover: null,
+ colTip: null,
+ periodDetail: null,
+ actOpen: {},
onbCountry: 'United States',
// onboarding
onbStep: 0,
@@ -2497,7 +2715,7 @@ Give Community Kudos
submitted: {},
receipt: null,
// tabs
- profileTab: 'snapshots', portfolioTab: 'trust', lbTab: 'current',
+ profileTab: 'contribution', portfolioTab: 'trust', lbTab: 'current',
kudosModalOpen: false, kRecipient: '', kAmount: 10, kMessage: '',
faqQuery: '', faqOpen: 'q1',
showClose: false, openSnap: null,
@@ -2660,11 +2878,20 @@ Give Community Kudos
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(url).then(done, done);
else done();
}
- navTo(v){ this.setState({view:v, activeId:null}); window.scrollTo(0,0); }
+ navTo(v){
+ const patch = {view:v, activeId:null};
+ if (v === 'home') { patch.view = this.state.persona === 'prospect' ? 'overview' : 'profile'; }
+ if (v === 'faq') { patch.view = 'overview'; patch.ovTab = 'faq'; }
+ if (v === 'portfolio') { patch.view = 'overview'; patch.ovTab = 'portfolio'; }
+ if (v === 'leaderboard') { patch.view = 'activities'; patch.actTab = 'leaderboard'; }
+ if (v === 'overview' && !this.state.ovTab) patch.ovTab = 'intro';
+ if (v === 'activities' && this.state.view !== 'activities') patch.actTab = 'acts';
+ this.setState(patch); window.scrollTo(0,0);
+ }
navToCategory(cat){ this.setState({view:'activities', activeId:null, filterCat:cat, filterCadence:'all', filterMode:'all', filterHot:false}); window.scrollTo(0,0); }
setPersona(p){
const pts = p === 'active' ? 1240 : 0;
- this.setState({persona:p, points:pts, view:'home', homeVariant:'a', activeId:null, submitted:{}, receipt:null, showClose:false});
+ this.setState({persona:p, points:pts, view: p==='prospect' ? 'overview' : 'profile', ovTab:'intro', homeVariant:'a', activeId:null, submitted:{}, receipt:null, showClose:false});
window.scrollTo(0,0);
}
startOnboarding(){ this.setState({view:'onboarding', onbStep:0, onbError:false, signinOpen:false}); window.scrollTo(0,0); }
@@ -2683,7 +2910,7 @@ Give Community Kudos
onbBack(){ this.setState(s => ({onbStep: Math.max(0, s.onbStep-1), onbError:false})); window.scrollTo(0,0); }
onbGoto(n){ this.setState({onbStep:n, onbError:false}); window.scrollTo(0,0); }
toggleOnbError(){ this.setState(s => ({onbError: !s.onbError})); }
- finishOnboarding(){ this.setState({persona:'new', points:0, view:'home', onbStep:0, homeVariant:'a'}); window.scrollTo(0,0); }
+ finishOnboarding(){ this.setState({persona:'new', points:0, view:'profile', onbStep:0, homeVariant:'a', ovTab:'intro'}); window.scrollTo(0,0); }
startKyc(){
if (this.state.onbError) return;
this.setState({onbKyc:'verifying'});
@@ -2692,7 +2919,7 @@ Give Community Kudos
setAccred(v){ this.setState({onbAccred:v, onbDocUploaded:false}); }
uploadDoc(){ this.setState({onbDocUploaded:true}); }
toggleSign(){ this.setState(s => ({onbSigned: !s.onbSigned})); }
- exitOnboarding(){ this.setState({view:'home'}); window.scrollTo(0,0); }
+ exitOnboarding(){ this.setState({view:'profile'}); window.scrollTo(0,0); }
// ---------- points ----------
awardPoints(n){
@@ -2751,17 +2978,17 @@ Give Community Kudos
// rail
const railDef = [
- {label:'Home', icon:'house', v:'home'},
+ {label:'Profile', icon:'user-circle', v:'profile'},
{label:'Overview', icon:'compass', v:'overview'},
{label:'Activities', icon:'lightning', v:'activities'},
- {label:'Profile', icon:'user-circle', v:'profile'},
{label:'Kudos', icon:'hands-clapping', v:'kudos', badge:3},
- {label:'Portfolio & Holdings', icon:'chart-pie-slice', v:'portfolio'},
- {label:'Leaderboard', icon:'ranking', v:'leaderboard'},
- {label:'FAQ', icon:'question', v:'faq'},
];
const railMain = railDef.map(r => {
- const active = s.view === r.v;
+ const active = r.v === 'overview'
+ ? (s.view === 'overview' || s.view === 'faq' || s.view === 'portfolio')
+ : r.v === 'activities'
+ ? (s.view === 'activities' || s.view === 'leaderboard')
+ : s.view === r.v;
return {
label:r.label, iconClass:'ph'+(active?'-fill':'')+' ph-'+r.icon, badge:r.badge||0,
go:()=>this.navTo(r.v),
@@ -2810,7 +3037,7 @@ Give Community Kudos
const pulse = pulseDef.map(p => ({who:p.who, text:p.text, when:p.when, iconClass:'ph ph-'+p.icon}));
const statCards = [
- {label:'Points this snapshot', value:s.points.toLocaleString('en-US'), sub:'collected · not yet converted', color:'var(--color-brand)', iconClass:'ph ph-coins'},
+ {label:'Points this period', value:s.points.toLocaleString('en-US'), sub:'collected · not yet converted', color:'var(--color-brand)', iconClass:'ph ph-coins'},
{label:'Contributions', value:'7', sub:'verified this period', color:'var(--text-primary)', iconClass:'ph ph-check-circle'},
{label:'Categories supported', value:'4 / 6', sub:'spread across the network', color:'var(--text-primary)', iconClass:'ph ph-squares-four'},
{label:'Days remaining', value:String(11), sub:'until snapshot closes', color:'var(--text-primary)', iconClass:'ph ph-clock'},
@@ -2836,6 +3063,9 @@ Give Community Kudos
const startList = featFirst(filtered.filter(a => a.tier === 'start'));
const momentumList = featFirst(filtered.filter(a => a.tier === 'momentum'));
const leverageList = featFirst(filtered.filter(a => a.tier === 'leverage'));
+ const actOpen = s.actOpen || {};
+ const startOpen = !!actOpen.start, momentumOpen = !!actOpen.momentum, leverageOpen = !!actOpen.leverage;
+ const toggleAct = (k) => this.setState({actOpen: Object.assign({}, actOpen, {[k]: !actOpen[k]})});
const catList = ['Knowledge Sharing','Programs','Projects','Brand','People / Talent','Network Tooling'];
const catFilterOpts = [{v:'all',t:'All categories'}].concat(catList.map(c => ({v:c,t:c})));
const cadenceOpts = [{v:'all',t:'Any cadence'},{v:'Repeatable',t:'Repeatable'},{v:'Recurring',t:'Recurring'},{v:'One-time',t:'One-time'}];
@@ -2846,7 +3076,12 @@ Give Community Kudos
label:d.label, go:()=>this.setState({[setKey]:d.k}),
style:'height:34px;padding:0 16px;border:none;border-radius:var(--radius-md);cursor:pointer;font:var(--text-label-lg);font-weight:'+(cur===d.k?'600':'500')+';'+(cur===d.k?'background:var(--color-brand);color:#fff;':'background:transparent;color:var(--text-secondary);'),
}));
- const profileTabs = mkTab([{k:'snapshots',label:'Snapshot history'},{k:'contribution',label:'Contribution profile'}], s.profileTab, 'profileTab');
+ const profileTabs = mkTab([{k:'contribution',label:'Contribution profile'}], 'contribution', 'profileTab');
+ const ovCur = s.ovTab || 'intro';
+ const overviewTabs = [{k:'portfolio',label:'Portfolio & Holdings'},{k:'intro',label:'Introduction to PLAA'},{k:'faq',label:'FAQ'}].map(d => ({
+ label:d.label, go:()=>this.setState({view:'overview', ovTab:d.k}),
+ style:'height:34px;padding:0 16px;border:none;border-radius:var(--radius-md);cursor:pointer;font:var(--text-label-lg);font-weight:'+(ovCur===d.k?'600':'500')+';'+(ovCur===d.k?'background:var(--color-brand);color:#fff;':'background:transparent;color:var(--text-secondary);'),
+ }));
const snapRaw = [
{period:'Jul 2026', id:'s-jul', items:[
{title:'Co-design a network program', cat:'Programs', points:350},
@@ -2934,60 +3169,222 @@ Give Community Kudos
const snapTotalLabel = fmt(snapTotalPts);
const snapRightsTotal = fmt(snapActivityPlaa + snapInfraPlaa);
- // points + PLAA over time (oldest → newest)
+ // PLAA over time (oldest → newest). Never mix points onto the PLAA axis.
+ const monthStart = {'Jan':'Jan 1','Feb':'Feb 1','Mar':'Mar 1','Apr':'Apr 1','May':'May 1','Jun':'Jun 1','Jul':'Jul 1'};
const series = [...snapRaw].reverse().map(sn => {
const pts = sn.items.reduce((a,b) => a + b.points, 0);
- return {period: sn.period.replace(' 2026',''), pts, plaa: Math.round(pts/10)};
+ const key = sn.period.replace(' 2026','');
+ return {period: key, full: sn.period, id: sn.id, pts, plaa: Math.round(pts/10), items: sn.items, since: (monthStart[key]||key)+', 2026'};
});
const infraBy = {'Jan':180,'Feb':210,'Mar':260,'Apr':240,'May':290,'Jun':310,'Jul':330};
const redeemedBy = {'Jan':0,'Feb':0,'Mar':320,'Apr':0,'May':180,'Jun':0,'Jul':400};
+ const redeemPx = {'Jan':0,'Feb':0,'Mar':1.36,'Apr':0,'May':1.42,'Jun':0,'Jul':1.48};
let cum = 0;
series.forEach(p => {
p.infra = infraBy[p.period] || 0;
p.redeemed = redeemedBy[p.period] || 0;
- cum += p.plaa + p.infra - p.redeemed;
+ p.px = redeemPx[p.period] || 0;
+ p.net = p.plaa + p.infra - p.redeemed;
+ cum += p.net;
p.cum = cum;
});
- const CX0 = 52, CX1 = 668, CY0 = 24, CY1 = 214;
- const nice = (v, step) => Math.max(step, Math.ceil(v/step)*step);
- const maxPts = nice(Math.max(...series.map(p=>p.pts)), 250);
+ const hidden = s.chartHidden || {};
+ const showAct = hidden.activities !== true;
+ const showRew = hidden.rewards !== true;
+ const showRed = hidden.redeemed !== true;
+ const showBal = hidden.balance !== true;
+ const toggleChart = (k) => this.setState({chartHidden: Object.assign({}, hidden, {[k]: !hidden[k]})});
+ const openPeriod = (id, kind) => this.setState({periodDetail: {id, kind: kind || 'activities'}, showClose:false});
+ const CX0 = 70, CX1 = 650, CY0 = 48, CY1 = 220;
+ const nice = (v, step) => Math.max(step, Math.ceil(Math.abs(v)/step)*step);
+ const visNet = series.map(p => (showAct?p.plaa:0)+(showRew?p.infra:0)-(showRed?p.redeemed:0));
+ const maxFlow = nice(Math.max(80, ...series.map(p => (showAct?p.plaa:0)+(showRew?p.infra:0)), ...series.map(p => showRed?p.redeemed:0)), 50);
const maxCum = nice(Math.max(...series.map(p=>p.cum)), 50);
const band = (CX1 - CX0) / series.length;
- const barW = Math.min(54, band * 0.46);
- const yFor = (v, max) => CY1 - (v/max) * (CY1 - CY0);
- const chartBars = series.map((p,i) => {
+ const barW = Math.min(44, band * 0.5);
+ const yZero = (CY0 + CY1) / 2;
+ const yFlow = (v) => yZero - (v/maxFlow) * ((CY1-CY0)/2);
+ const yCum = (v) => CY1 - (v/maxCum) * (CY1 - CY0);
+ const chartZeroY = yZero.toFixed(1);
+ const PAL = {act:'#7AA2FF', rew:'#1B4DFF', red:'#C45C26', net:'#0A9952'};
+ const chartSegs = [];
+ series.forEach((p,i) => {
const cx = CX0 + band*i + band/2;
- const y = yFor(p.pts, maxPts);
- return {x:(cx - barW/2).toFixed(1), y:y.toFixed(1), w:barW.toFixed(1), h:(CY1-y).toFixed(1),
- cx:cx.toFixed(1), labelY:(y-8).toFixed(1), ptsLabel:fmt(p.pts), period:p.period};
+ const x = cx - barW/2;
+ let posY = yZero;
+ const addPos = (val, fill, kind, label) => {
+ if (!val) return;
+ const y = yFlow(val);
+ const h = posY - y;
+ chartSegs.push({x:x.toFixed(1), y:y.toFixed(1), w:barW.toFixed(1), h:Math.max(1,h).toFixed(1), fill, go:()=>openPeriod(p.id, kind), label, cx:cx.toFixed(1), ly:(y+h/2+4).toFixed(1)});
+ posY = y;
+ };
+ if (showAct) addPos(p.plaa, PAL.act, 'activities', '+'+fmt(p.plaa));
+ if (showRew) addPos(p.infra, PAL.rew, 'rewards', '+'+fmt(p.infra));
+ if (showRed && p.redeemed) {
+ const y = yZero;
+ const y2 = yFlow(-p.redeemed);
+ chartSegs.push({x:x.toFixed(1), y:y.toFixed(1), w:barW.toFixed(1), h:Math.max(1,y2-y).toFixed(1), fill:PAL.red, go:()=>openPeriod(p.id,'redeemed'), label:'−'+fmt(p.redeemed), cx:cx.toFixed(1), ly:((y+y2)/2+4).toFixed(1)});
+ }
+ p._cx = cx;
+ p._topY = posY;
+ p._visNet = (showAct?p.plaa:0)+(showRew?p.infra:0)-(showRed?p.redeemed:0);
});
- const chartDots = series.map((p,i) => ({cx:(CX0 + band*i + band/2).toFixed(1), cy:yFor(p.cum, maxCum).toFixed(1)}));
+ const chartDots = series.map((p,i) => ({
+ cx:(CX0 + band*i + band/2).toFixed(1),
+ cy:yCum(p.cum).toFixed(1),
+ hover: () => this.setState({chartHover: i}),
+ unhover: () => this.setState({chartHover: null}),
+ }));
const chartLine = chartDots.map(d => d.cx+','+d.cy).join(' ');
const chartGrid = [0,0.25,0.5,0.75,1].map(f => {
const y = CY1 - f*(CY1-CY0);
- return {y:y.toFixed(1), ty:(y+4).toFixed(1), left:fmt(Math.round(maxPts*f)), right:fmt(Math.round(maxCum*f))};
+ return {y:y.toFixed(1), ty:(y+4).toFixed(1), left:fmt(Math.round(maxFlow*(f*2-1))), right:fmt(Math.round(maxCum*f))};
});
- const svgText = (key, x, y, anchor, fill, txt, weight) => React.createElement('text', {key, x, y, textAnchor:anchor, fill, style:{fontWeight:weight||600, fontSize:'11px'}}, txt);
- const chartLabelEls = [
+ const svgText = (key, x, y, anchor, fill, txt, weight, size) => React.createElement('text', {key, x, y, textAnchor:anchor, fill, style:{fontWeight:weight||600, fontSize:size||'11px'}}, txt);
+ let chartLabelEls = [
+ svgText('axL', 70, 18, 'start', 'var(--text-tertiary)', 'Period net (PLAA)', 600, '11px'),
+ svgText('axR', 650, 18, 'end', 'var(--color-brand)', 'Ending balance (PLAA)', 600, '11px'),
...chartGrid.flatMap((g,i) => [
- svgText('gl'+i, 44, g.ty, 'end', 'var(--text-tertiary)', g.left, 500),
- svgText('gr'+i, 676, g.ty, 'start', 'var(--color-brand)', g.right, 500),
- ]),
- ...chartBars.flatMap((b,i) => [
- svgText('bv'+i, b.cx, b.labelY, 'middle', 'var(--text-secondary)', b.ptsLabel),
- svgText('bp'+i, b.cx, 248, 'middle', 'var(--text-tertiary)', b.period),
+ svgText('gl'+i, 62, g.ty, 'end', 'var(--text-tertiary)', g.left, 500),
+ svgText('gr'+i, 658, g.ty, 'start', 'var(--color-brand)', g.right, 500),
]),
+ ...chartSegs.filter(b => Number(b.h) > 14).map((b,i) => svgText('sl'+i, b.cx, b.ly, 'middle', '#fff', b.label, 600, '9px')),
+ ...series.map((p,i) => svgText('bp'+i, (CX0 + band*i + band/2).toFixed(1), 248, 'middle', 'var(--text-tertiary)', p.period)),
+ ...series.map((p,i) => {
+ const net = p._visNet;
+ const y = net >= 0 ? Math.min(p._topY, yZero) - 12 : yFlow(-((showRed?p.redeemed:0))) + 14;
+ return svgText('bn'+i, (CX0 + band*i + band/2).toFixed(1), y.toFixed(1), 'middle', net>=0 ? 'var(--pl-green-600)' : '#C45C26', (net>=0?'+':'')+fmt(net), 700, '11px');
+ }),
];
- const pointsHistory = [...series].reverse().map(p => ({
- period: p.period + ' 2026', points: fmt(p.pts), plaa: '+' + fmt(p.plaa),
- infra: '+' + fmt(p.infra), cum: fmt(p.cum),
- redeemed: p.redeemed ? '−' + fmt(p.redeemed) : '—',
+ const chartToggles = [
+ {k:'activities', label:'Activities', color:PAL.act},
+ {k:'rewards', label:'Rewards', color:PAL.rew},
+ {k:'redeemed', label:'Redeemed', color:PAL.red},
+ {k:'balance', label:'Ending balance', color:'var(--color-brand)', line:true},
+ ].map(t => {
+ const on = hidden[t.k] !== true;
+ return {
+ label:t.label,
+ go:()=>toggleChart(t.k),
+ sw: t.line ? 'height:3px;width:14px;background:'+t.color+';opacity:'+(on?1:0.25)+';' : 'background:'+t.color+';opacity:'+(on?1:0.25)+';',
+ style: 'background:'+(on?'var(--surface-page)':'transparent')+';color:'+(on?'var(--text-primary)':'var(--text-tertiary)')+';box-shadow:'+(on?'var(--ring-hairline)':'none')+';opacity:'+(on?1:0.55)+';',
+ };
+ });
+ const drag = s.chartDrag;
+ const idxFromX = (clientX, target) => {
+ const svg = target && target.ownerSVGElement ? target.ownerSVGElement : target;
+ if (!svg || !svg.createSVGPoint) return 0;
+ const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = 0;
+ const ctm = svg.getScreenCTM(); if (!ctm) return 0;
+ const loc = pt.matrixTransform(ctm.inverse());
+ const i = Math.floor((loc.x - CX0) / band);
+ return Math.max(0, Math.min(series.length-1, i));
+ };
+ const onChartDown = (e) => {
+ const i = idxFromX(e.clientX, e.target);
+ this.setState({chartDrag:{a:i, b:i}, chartHover: i});
+ };
+ const onChartMove = (e) => {
+ const i = idxFromX(e.clientX, e.target);
+ if (this.state.chartDrag) this.setState({chartDrag: Object.assign({}, this.state.chartDrag, {b:i}), chartHover: i});
+ else this.setState({chartHover: i});
+ };
+ const onChartUp = () => this.setState({chartDrag: this.state.chartDrag && this.state.chartDrag.a !== this.state.chartDrag.b ? this.state.chartDrag : null});
+ const onChartLeave = () => this.setState({chartHover: null, chartDrag: this.state.chartDrag && this.state.chartDrag.a !== this.state.chartDrag.b ? this.state.chartDrag : null});
+ let chartHasBrush = false, chartBrushX='0', chartBrushW='0', chartBrushCaption='';
+ let chartDeltaX='0', chartDeltaW='0', chartDeltaTx='0';
+ if (drag) {
+ const a = Math.min(drag.a, drag.b), b = Math.max(drag.a, drag.b);
+ chartHasBrush = true;
+ const x0 = CX0 + band*a, x1 = CX0 + band*(b+1);
+ chartBrushX = x0.toFixed(1); chartBrushW = (x1-x0).toFixed(1);
+ const slice = series.slice(a, b+1);
+ const visNet = slice.reduce((n,p)=>n+p._visNet,0);
+ const start = a===0 ? 0 : series[a-1].cum;
+ const end = series[b].cum;
+ const abs = end - start;
+ const rel = start ? ((abs/start)*100).toFixed(1)+'%' : '—';
+ chartBrushCaption = slice[0].period+(slice.length>1 ? '–'+slice[slice.length-1].period : '')+' '+(abs>=0?'+':'')+fmt(abs)+' PLAA ('+rel+')';
+ const mid = (x0+x1)/2;
+ chartDeltaW = Math.max(160, chartBrushCaption.length*6.4);
+ chartDeltaX = Math.max(8, Math.min(720-chartDeltaW-8, mid - chartDeltaW/2)).toFixed(1);
+ chartDeltaTx = (Number(chartDeltaX) + chartDeltaW/2).toFixed(1);
+ chartDeltaW = chartDeltaW.toFixed(1);
+ }
+ const hoverI = (typeof s.chartHover === 'number') ? s.chartHover : null;
+ const chartHasHover = hoverI != null && series[hoverI] && !chartHasBrush;
+ let chartHoverX='0', chartHoverY='0', chartHoverW='0', chartHoverTx='0', chartHoverTy='0', chartHoverLabel='';
+ if (chartHasHover) {
+ const p = series[hoverI];
+ chartHoverLabel = p.period + ' ' + fmt(p.cum) + ' PLAA';
+ chartHoverW = Math.max(92, chartHoverLabel.length * 6.6);
+ const cx = Number(chartDots[hoverI].cx);
+ chartHoverX = Math.max(8, Math.min(720 - chartHoverW - 8, cx - chartHoverW / 2)).toFixed(1);
+ chartHoverY = Math.max(6, Number(chartDots[hoverI].cy) - 30).toFixed(1);
+ chartHoverTx = (Number(chartHoverX) + chartHoverW / 2).toFixed(1);
+ chartHoverTy = (Number(chartHoverY) + 15).toFixed(1);
+ chartHoverW = chartHoverW.toFixed(1);
+ chartLabelEls.push(
+ React.createElement('rect', {key:'htBg', x:chartHoverX, y:chartHoverY, width:chartHoverW, height:22, rx:4, fill:'var(--color-brand)', style:{pointerEvents:'none'}}),
+ React.createElement('text', {key:'htTx', x:chartHoverTx, y:chartHoverTy, textAnchor:'middle', fill:'#fff', style:{fontWeight:600, fontSize:'11px', pointerEvents:'none'}}, chartHoverLabel)
+ );
+ }
+ const currentPts = 1240;
+ const currentPlaaPlaceholder = '•••';
+ const currentRow = {
+ period: 'Aug, 2026',
+ pointsSub: fmt(currentPts) + ' points so far',
+ pointsTip: 'Open this period’s activities',
+ plaa: currentPlaaPlaceholder,
+ plaaBlur: 'filter:blur(5px);user-select:none;',
+ infra: '—',
+ cum: currentPlaaPlaceholder,
+ cumBlur: 'filter:blur(5px);user-select:none;',
+ redeemed: '—',
+ redeemUsd: '',
+ openDetail: () => {},
+ openPoints: () => this.setState({showClose:true}),
+ openRewards: () => {},
+ openRedeemed: () => {},
+ };
+ const pointsHistory = [currentRow].concat([...series].reverse().map(p => {
+ return {
+ period: p.full.replace(' 20', ', 20'),
+ pointsSub: fmt(p.pts) + ' points',
+ pointsTip: 'See what these points were collected for',
+ plaa: '+' + fmt(p.plaa),
+ plaaBlur: '',
+ infra: '+' + fmt(p.infra),
+ cum: fmt(p.cum),
+ cumBlur: '',
+ redeemed: p.redeemed ? '−' + fmt(p.redeemed) : '—',
+ redeemUsd: p.redeemed ? '$' + (p.redeemed * p.px).toLocaleString('en-US', {maximumFractionDigits:0}) + ' at $' + p.px.toFixed(2) : '',
+ openDetail: () => openPeriod(p.id, 'activities'),
+ openPoints: () => openPeriod(p.id, 'activities'),
+ openRewards: () => openPeriod(p.id, 'rewards'),
+ openRedeemed: () => p.redeemed ? openPeriod(p.id, 'redeemed') : null,
+ };
}));
const sumBy = (k) => series.reduce((a,p) => a + p[k], 0);
const plaaActivitiesTotal = fmt(sumBy('plaa'));
const plaaInfraTotal = fmt(sumBy('infra'));
const plaaRedeemedTotal = fmt(sumBy('redeemed'));
+ const redeemUsdSum = series.reduce((a,p) => a + (p.redeemed * (p.px||0)), 0);
+ const redeemUnits = series.reduce((a,p)=>a+(p.redeemed||0),0);
+ const avgPx = redeemUnits ? redeemUsdSum / redeemUnits : 0;
+ const plaaRedeemedUsdTotal = '$' + redeemUsdSum.toLocaleString('en-US', {maximumFractionDigits:0}) + (avgPx ? ' · avg $' + avgPx.toFixed(2) : '');
const plaaBalanceTotal = fmt(series[series.length-1].cum);
+ const tipOpen = (k) => 'justify-content:flex-end;' + (s.colTip===k ? '' : '');
+ const tipActStyle = 'justify-content:flex-end;' + (s.colTip==='act'?'':'');
+ const tipRewStyle = 'justify-content:flex-end;';
+ const tipRedStyle = 'justify-content:flex-end;';
+ const tipBalStyle = 'justify-content:flex-end;color:var(--color-brand);';
+ const toggleTip = (k) => this.setState({colTip: s.colTip===k ? null : k});
+ const toggleTipAct = (e) => { if (e && e.stopPropagation) e.stopPropagation(); toggleTip('act'); };
+ const toggleTipRew = (e) => { if (e && e.stopPropagation) e.stopPropagation(); toggleTip('rew'); };
+ const toggleTipRed = (e) => { if (e && e.stopPropagation) e.stopPropagation(); toggleTip('red'); };
+ const toggleTipBal = (e) => { if (e && e.stopPropagation) e.stopPropagation(); toggleTip('bal'); };
// home: PLAA balance history chart
const hRange = s.homeRange || 'all';
@@ -3292,6 +3689,151 @@ Give Community Kudos
go: () => this.navToCategory(c.name),
}));
+ const howCur = s.howStep || 'contribute';
+ const howDefs = [
+ {k:'contribute', n:'1', title:'Contribute', body:'Share knowledge, make connections, and support the network.', icon:'ph ph-hand-heart', color:'#1B4DFF'},
+ {k:'collect', n:'2', title:'Collect', body:'Collect points for your contributions each snapshot period.', icon:'ph ph-coins', color:'#2C6AEE'},
+ {k:'convert', n:'3', title:'Convert', body:'Points are converted into PLAA each month.', icon:'ph ph-arrows-left-right', color:'#1487C4'},
+ {k:'redeem', n:'4', title:'Redeem', body:'Buyback auctions may turn PLAA into realized value.', icon:'ph ph-trend-up', color:'#12A594'},
+ ];
+ const howSteps = howDefs.map(d => {
+ const on = howCur === d.k;
+ return Object.assign({}, d, {
+ go: () => this.setState({howStep: d.k}),
+ numStyle: 'width:34px;height:34px;flex:none;border-radius:50%;background:'+d.color+';color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:14px;font-weight:600;line-height:1;',
+ style: 'background:var(--surface-card);border-radius:var(--radius-2xl);box-shadow:var(--ring-hairline),var(--shadow-xs);border-top:3px solid '+d.color+';padding:22px;display:flex;flex-direction:column;gap:12px;cursor:pointer;outline:'+(on?'2px solid '+d.color:'none')+';outline-offset:'+(on?'-2px':'0')+';opacity:'+(on?1:0.72)+';',
+ });
+ });
+ const howContribute = howCur === 'contribute';
+ const howCollect = howCur === 'collect';
+ const howConvert = howCur === 'convert';
+ const howRedeem = howCur === 'redeem';
+
+ const statusLevels = [
+ {name:'Contributor', need:0},
+ {name:'Network Ambassador', need:40},
+ {name:'Network Steward', need:80},
+ {name:'Network Fellow', need:140},
+ ];
+ const statusCount = 63;
+ let statusIdx = 0;
+ statusLevels.forEach((lv,i) => { if (statusCount >= lv.need) statusIdx = i; });
+ const statusLevel = statusLevels[statusIdx].name;
+ const statusNext = statusLevels[statusIdx+1];
+ const statusNextHint = statusNext ? (statusNext.need - statusCount) + ' more activities to ' + statusNext.name : 'Highest status unlocked';
+ const statusMeterTitle = statusNext ? statusLevel + ' · ' + statusCount + ' activities · next: ' + statusNext.name + ' at ' + statusNext.need : statusLevel;
+ const arcLen = 75.4;
+ const statusFrac = statusNext ? Math.max(0.08, Math.min(1, (statusCount - statusLevels[statusIdx].need) / (statusNext.need - statusLevels[statusIdx].need))) : 1;
+ const statusMeterDash = (arcLen * statusFrac).toFixed(1) + ' ' + arcLen.toFixed(1);
+
+ const navSeries = [
+ {period:'Jan 2025', nav:4.18e6, circ:28500},
+ {period:'Feb 2025', nav:4.22e6, circ:31200},
+ {period:'Mar 2025', nav:4.31e6, circ:34800},
+ {period:'Apr 2025', nav:4.38e6, circ:38100},
+ {period:'May 2025', nav:4.52e6, circ:41900},
+ {period:'Jun 2025', nav:4.61e6, circ:45600},
+ {period:'Jul 2025', nav:4.74e6, circ:49800},
+ {period:'Aug 2025', nav:4.88e6, circ:54100},
+ {period:'Sep 2025', nav:5.02e6, circ:58600},
+ {period:'Oct 2025', nav:5.18e6, circ:63200},
+ {period:'Nov 2025', nav:5.29e6, circ:67400},
+ {period:'Dec 2025', nav:5.41e6, circ:71200},
+ {period:'Jan 2026', nav:5.55e6, circ:74800},
+ {period:'Feb 2026', nav:5.72e6, circ:78400},
+ {period:'Mar 2026', nav:5.92e6, circ:82000},
+ {period:'Apr 2026', nav:6.11e6, circ:91000},
+ {period:'May 2026', nav:6.38e6, circ:101000},
+ {period:'Jun 2026', nav:6.55e6, circ:109000},
+ {period:'Jul 2026', nav:6.74e6, circ:118400},
+ ];
+ const NX0 = 36, NX1 = 484, NY0 = 32, NY1 = 148;
+ const nav0 = navSeries[0].nav, circ0 = navSeries[0].circ;
+ navSeries.forEach(p => {
+ p.navIdx = 100 * p.nav / nav0;
+ p.circIdx = 100 * p.circ / circ0;
+ p.implied = p.nav / p.circ;
+ });
+ const idxMax = Math.max(...navSeries.map(p => p.navIdx), ...navSeries.map(p => p.circIdx));
+ const nx = (i) => NX0 + (NX1-NX0) * (navSeries.length===1 ? 0.5 : i/(navSeries.length-1));
+ const nyIdx = (v) => NY1 - (v/idxMax)*(NY1-NY0);
+ const navPts = navSeries.map((p,i) => ({x:nx(i), y:nyIdx(p.navIdx), p}));
+ const circPts = navSeries.map((p,i) => ({x:nx(i), y:nyIdx(p.circIdx)}));
+ const navLine = navPts.map(q => q.x.toFixed(1)+','+q.y.toFixed(1)).join(' ');
+ const circLine = circPts.map(q => q.x.toFixed(1)+','+q.y.toFixed(1)).join(' ');
+ const navArea = navLine + ' ' + navPts[navPts.length-1].x.toFixed(1)+','+NY1 + ' ' + navPts[0].x.toFixed(1)+','+NY1;
+ const moneyM = (v) => '$' + (v/1e6).toFixed(2) + 'M';
+ const lastNav = navSeries[navSeries.length-1];
+ const navLatest = moneyM(lastNav.nav);
+ const circLatest = lastNav.circ.toLocaleString('en-US');
+ const impliedLatest = '$' + lastNav.implied.toFixed(2);
+ const q90 = navSeries[navSeries.length-4] || navSeries[0];
+ const navDelta90 = ((lastNav.nav - q90.nav)/q90.nav >= 0 ? '+' : '') + (((lastNav.nav - q90.nav)/q90.nav)*100).toFixed(1) + '% (90d)';
+ const navFirstPeriod = navSeries[0].period;
+ const navLastPeriod = lastNav.period;
+ const navIdxFromX = (clientX, target) => {
+ const svg = target && target.ownerSVGElement ? target.ownerSVGElement : target;
+ if (!svg || !svg.createSVGPoint) return 0;
+ const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = 0;
+ const ctm = svg.getScreenCTM(); if (!ctm) return 0;
+ const loc = pt.matrixTransform(ctm.inverse());
+ const t = (loc.x - NX0) / (NX1 - NX0);
+ return Math.max(0, Math.min(navSeries.length-1, Math.round(t*(navSeries.length-1))));
+ };
+ const onNavDown = (e) => this.setState({navDrag:{a: navIdxFromX(e.clientX, e.target), b: navIdxFromX(e.clientX, e.target)}, navHover: navIdxFromX(e.clientX, e.target)});
+ const onNavMove = (e) => {
+ const i = navIdxFromX(e.clientX, e.target);
+ if (this.state.navDrag) this.setState({navDrag: Object.assign({}, this.state.navDrag, {b:i}), navHover: i});
+ else this.setState({navHover: i});
+ };
+ const onNavUp = () => this.setState({navDrag: this.state.navDrag && this.state.navDrag.a !== this.state.navDrag.b ? this.state.navDrag : null});
+ const onNavLeave = () => this.setState({navHover: null, navDrag: this.state.navDrag && this.state.navDrag.a !== this.state.navDrag.b ? this.state.navDrag : null});
+ let navHasBrush=false, navBrushX='0', navBrushW='0', navBrushCaption='', navDeltaX='0', navDeltaW='0', navDeltaTx='0';
+ const nd = s.navDrag;
+ if (nd) {
+ const a = Math.min(nd.a, nd.b), b = Math.max(nd.a, nd.b);
+ navHasBrush = true;
+ const x0 = nx(a), x1 = nx(b);
+ navBrushX = Math.min(x0,x1).toFixed(1);
+ navBrushW = Math.max(8, Math.abs(x1-x0)).toFixed(1);
+ const start = navSeries[a].nav, end = navSeries[b].nav;
+ const abs = end - start;
+ const rel = ((abs/start)*100).toFixed(1)+'%';
+ const iStart = navSeries[a].circ, iEnd = navSeries[b].circ;
+ const iRel = ((iEnd-iStart)/iStart*100).toFixed(1)+'%';
+ navBrushCaption = navSeries[a].period.split(' ')[0]+(a!==b?'–'+navSeries[b].period.split(' ')[0]:'')+' NAV '+(abs>=0?'+':'')+rel+' issued '+(iEnd>=iStart?'+':'')+iRel;
+ const mid = (Number(navBrushX)+Number(navBrushW)/2);
+ navDeltaW = Math.max(170, navBrushCaption.length*6.0);
+ navDeltaX = Math.max(4, Math.min(520-navDeltaW-4, mid-navDeltaW/2)).toFixed(1);
+ navDeltaTx = (Number(navDeltaX)+navDeltaW/2).toFixed(1);
+ navDeltaW = navDeltaW.toFixed(1);
+ }
+ const navTickIdx = [0, 6, 12, navSeries.length-1];
+ const navLabelEls = [
+ svgText('naxL', NX0, 18, 'start', 'var(--text-tertiary)', 'Indexed (start = 100)', 600, '10px'),
+ svgText('naxR', NX1, 18, 'end', '#12A594', 'Issued vs NAV', 600, '10px'),
+ svgText('n100', NX0 - 6, nyIdx(100)+4, 'end', 'var(--text-tertiary)', '100', 500, '10px'),
+ svgText('nMax', NX0 - 6, NY0+4, 'end', 'var(--text-tertiary)', String(Math.round(idxMax)), 500, '10px'),
+ ...navTickIdx.map(i => svgText('nt'+i, nx(i).toFixed(1), 164, 'middle', 'var(--text-tertiary)', navSeries[i].period.replace(' 20',' ’'), 500, '10px')),
+ ];
+ const navHoverI = (typeof s.navHover === 'number') ? s.navHover : null;
+ if (navHoverI != null && navSeries[navHoverI] && !navHasBrush) {
+ const hp = navSeries[navHoverI];
+ const hx = nx(navHoverI);
+ const hy = nyIdx(hp.navIdx);
+ const label = hp.period + ' NAV ' + moneyM(hp.nav) + ' issued ' + hp.circ.toLocaleString('en-US') + ' $' + hp.implied.toFixed(2);
+ const w = Math.max(210, label.length * 5.8);
+ const bx = Math.max(4, Math.min(520 - w - 4, hx - w/2));
+ const by = Math.max(4, hy - 28);
+ navLabelEls.push(
+ React.createElement('line', {key:'nhL', x1:hx.toFixed(1), x2:hx.toFixed(1), y1:NY0, y2:NY1, stroke:'rgba(27,77,255,0.28)', strokeWidth:1}),
+ React.createElement('circle', {key:'nhN', cx:hx.toFixed(1), cy:hy.toFixed(1), r:4, fill:'var(--color-brand)'}),
+ React.createElement('circle', {key:'nhC', cx:hx.toFixed(1), cy:nyIdx(hp.circIdx).toFixed(1), r:3.5, fill:'#12A594'}),
+ React.createElement('rect', {key:'nhBg', x:bx.toFixed(1), y:by.toFixed(1), width:w.toFixed(1), height:22, rx:4, fill:'var(--color-brand)', style:{pointerEvents:'none'}}),
+ React.createElement('text', {key:'nhTx', x:(bx+w/2).toFixed(1), y:(by+15).toFixed(1), textAnchor:'middle', fill:'#fff', style:{fontWeight:600, fontSize:'10px', pointerEvents:'none'}}, label)
+ );
+ }
+
const eligCards = [
{icon:'ph-fill ph-identification-card', title:'Identity (KYC/AML)', desc:'Verified through our identity partner. Re-verification not required this period.', status:'Verified · Jan 2025'},
{icon:'ph-fill ph-seal-check', title:'Accreditation', desc:'Accredited investor status on file. Valid through Jan 2027.', status:'Accredited'},
@@ -3436,6 +3978,41 @@ Give Community Kudos
else { receiptCelebrate=true; receiptIcon='ph-fill ph-check-circle'; receiptIconWrap='flex:none;width:42px;height:42px;border-radius:var(--radius-lg);display:inline-flex;align-items:center;justify-content:center;color:var(--pl-green-600);background:rgba(10,153,82,0.10);animation:plaa-pop .4s var(--ease-out) both;'; }
}
+ const pdSrc = s.periodDetail && snapHistory.find(h => h.id === s.periodDetail.id);
+ const pdKind = s.periodDetail ? s.periodDetail.kind : 'activities';
+ const pdSeries = s.periodDetail && series.find(p => p.id === s.periodDetail.id);
+ let pdTitle='', pdKicker='Period summary', pdLead='', pdActs='—', pdPlaa='—', pdInfra='—', pdNet='—';
+ let pdListTitle='Activities this period', pdItems=[], pdFootLabel='', pdFootValue='', pdHasRedeem=false, pdRedeemNote='';
+ if (pdSrc && pdSeries) {
+ pdTitle = pdSrc.period;
+ pdActs = String(pdSrc.activities);
+ pdPlaa = '+'+pdSrc.activityPlaa;
+ pdInfra = '+'+pdSrc.infra;
+ pdNet = (pdSeries.net>=0?'+':'')+fmt(pdSeries.net);
+ if (pdKind === 'rewards') {
+ pdKicker = 'Rewards';
+ pdLead = pdSrc.period+' granted '+pdSrc.infra+' PLAA as Infra Member rewards — posted as PLAA, never as points.';
+ pdListTitle = 'Rewards this period';
+ pdItems = [{cat:'Rewards', title:'Infra Member rewards, granted in PLAA', amount:'+'+pdSrc.infra+' PLAA'}];
+ pdFootLabel = 'Rewards total'; pdFootValue = '+'+pdSrc.infra+' PLAA';
+ } else if (pdKind === 'redeemed') {
+ pdKicker = 'Redemption';
+ const usd = pdSeries.redeemed ? '$'+(pdSeries.redeemed*pdSeries.px).toLocaleString('en-US',{maximumFractionDigits:0}) : '—';
+ pdLead = pdSrc.period+' you redeemed '+fmt(pdSeries.redeemed)+' PLAA at $'+pdSeries.px.toFixed(2)+' clearing.';
+ pdListTitle = 'Redemption this period';
+ pdItems = [{cat:'Redeemed', title:'Trust buyback · $'+pdSeries.px.toFixed(2)+' / PLAA', amount:'−'+fmt(pdSeries.redeemed)+' PLAA'}];
+ pdFootLabel = 'Proceeds'; pdFootValue = usd;
+ pdHasRedeem = true; pdRedeemNote = fmt(pdSeries.redeemed)+' PLAA × $'+pdSeries.px.toFixed(2)+' = '+usd+'. Illustrative clearing price.';
+ } else {
+ pdKicker = 'Period summary';
+ pdLead = 'This period you contributed '+pdSrc.activities+' activities across '+pdSrc.categories+' categories, converting '+pdSrc.points+' points → +'+pdSrc.activityPlaa+' PLAA.';
+ pdListTitle = 'Activities this period';
+ pdItems = pdSrc.items.map(it => ({cat:it.cat, title:it.title, amount:'+'+it.points+' points'}));
+ pdFootLabel = pdSrc.period+' activities'; pdFootValue = pdSrc.points+' points → +'+pdSrc.activityPlaa+' PLAA';
+ if (pdSeries.redeemed) { pdHasRedeem = true; pdRedeemNote = 'Also redeemed '+fmt(pdSeries.redeemed)+' PLAA at $'+pdSeries.px.toFixed(2)+'.'; }
+ }
+ }
+
return {
cubeSvg: this.CUBE,
videoPlaying: s.videoPlaying,
@@ -3460,8 +4037,8 @@ Give Community Kudos
{title:'Contribute a high-quality response to the Forum', cat:'Knowledge Sharing', points:100},
{title:'Complete the monthly network survey', cat:'People / Talent', points:90},
],
- goHome: () => this.navTo('home'),
- goActivities: () => this.navTo('activities'),
+ goHome: () => this.navTo(onboarded ? 'profile' : 'overview'),
+ goActivities: () => this.setState({view:'activities', actTab:'acts', activeId:null}),
startOnboarding: () => this.startOnboarding(),
openClose: () => this.setState({showClose:true}),
@@ -3510,9 +4087,17 @@ Give Community Kudos
resultCount: filtered.length, noResults: filtered.length === 0,
// network asks
- actTabs: mkTab([{k:'acts', label:'Activities'}, {k:'asks', label:'Incentivized Network Asks'}], s.actTab, 'actTab'),
+ actTabs: mkTab([{k:'acts', label:'Activities'}, {k:'asks', label:'Incentivized Network Asks'}, {k:'leaderboard', label:'Leaderboard'}], s.actTab, 'actTab'),
isActTabActs: s.actTab === 'acts',
isActTabAsks: s.actTab === 'asks',
+ isActTabLb: s.actTab === 'leaderboard',
+ startOpen, momentumOpen, leverageOpen,
+ toggleStart: () => toggleAct('start'),
+ toggleMomentum: () => toggleAct('momentum'),
+ toggleLeverage: () => toggleAct('leverage'),
+ startCaret: startOpen ? 'ph-bold ph-caret-up' : 'ph-bold ph-caret-down',
+ momentumCaret: momentumOpen ? 'ph-bold ph-caret-up' : 'ph-bold ph-caret-down',
+ leverageCaret: leverageOpen ? 'ph-bold ph-caret-up' : 'ph-bold ph-caret-down',
asks: this.networkAsks().map(a => this.decorateAsk(a)),
askModalOpen: s.askModalOpen,
askNotSubmitted: !s.askSubmitted,
@@ -3556,8 +4141,21 @@ Give Community Kudos
profileTabs, snapHistory, eligCards, settingsRows,
snapTotalLabel, snapRightsTotal,
radarPolygon, radarGrid, radarAxes, radarDots, radarLabelEls, boostSuggestions,
- chartBars, chartDots, chartLine, chartGrid, chartLabelEls, pointsHistory,
- plaaActivitiesTotal, plaaInfraTotal, plaaRedeemedTotal, plaaBalanceTotal,
+ chartBars: chartSegs, chartSegs, chartDots, chartLine, chartGrid, chartLabelEls, pointsHistory,
+ chartToggles, chartZeroY, chartShowBalance: showBal, chartHasBrush, chartBrushX, chartBrushW, chartBrushCaption,
+ chartDeltaX, chartDeltaW, chartDeltaTx, chartHasHover, chartHoverX, chartHoverY, chartHoverW, chartHoverTx, chartHoverTy, chartHoverLabel,
+ onChartDown, onChartMove, onChartUp, onChartLeave,
+ plaaActivitiesTotal, plaaInfraTotal, plaaRedeemedTotal, plaaRedeemedUsdTotal, plaaBalanceTotal,
+ tipActStyle, tipRewStyle, tipRedStyle, tipBalStyle, toggleTipAct, toggleTipRew, toggleTipRed, toggleTipBal,
+ tipActOpen: s.colTip==='act' ? ' is-open' : '',
+ tipRewOpen: s.colTip==='rew' ? ' is-open' : '',
+ tipRedOpen: s.colTip==='red' ? ' is-open' : '',
+ tipBalOpen: s.colTip==='bal' ? ' is-open' : '',
+ statusLevel, statusNextHint, statusMeterTitle, statusMeterDash,
+ howSteps, howContribute, howCollect, howConvert, howRedeem,
+ navLatest, circLatest, impliedLatest, navDelta90, navFirstPeriod, navLastPeriod,
+ navLine, circLine, navArea, navLabelEls, navHasBrush, navBrushX, navBrushW, navBrushCaption,
+ navDeltaX, navDeltaW, navDeltaTx, onNavDown, onNavMove, onNavUp, onNavLeave,
infraMember: true,
heroSourcesOpen: !!s.heroSourcesOpen,
heroSourcesExpanded: !!s.heroSourcesOpen || !!s.heroSourcesClosing,
@@ -3583,7 +4181,7 @@ Give Community Kudos
},
heroOwned: plaaBalanceTotal, heroActivities: plaaActivitiesTotal,
heroInfra: plaaInfraTotal, heroRedeemed: plaaRedeemedTotal,
- pSnapshots: s.profileTab === 'snapshots', pContribution: s.profileTab === 'contribution',
+ pSnapshots: false, pContribution: true,
pEligibility: s.profileTab === 'eligibility', pSettings: s.profileTab === 'settings',
// kudos
@@ -3609,15 +4207,22 @@ Give Community Kudos
kMsgCount: kMsgLen + ' / 400',
// portfolio
- isPortfolio: s.view === 'portfolio',
+ isPortfolio: (s.view === 'overview' && s.ovTab === 'portfolio') || s.view === 'portfolio',
portfolioTabs, composition, focusAreas, rightsRecord,
buybackHistory, buybackTotalSold, buybackLastParticipated, buybackNextDate,
ptTrust: s.portfolioTab === 'trust', ptPersonal: s.portfolioTab === 'personal',
+ overviewTabs,
+ ovIntro: (s.ovTab || 'intro') === 'intro',
+ ovFaq: s.ovTab === 'faq',
+ ovPortfolio: s.ovTab === 'portfolio',
+ goOverviewIntro: () => this.setState({view:'overview', ovTab:'intro'}),
+ goOverviewPortfolio: () => this.setState({view:'overview', ovTab:'portfolio'}),
+ goRedeeming: () => this.setState({view:'overview', ovTab:'intro', howStep:'redeem'}),
// overview
- isOverview: s.view === 'overview',
- goFaq: () => this.navTo('faq'),
- goPortfolio: () => this.navTo('portfolio'),
+ isOverview: (s.view === 'overview' || s.view === 'faq' || s.view === 'portfolio') && (s.ovTab || 'intro') === 'intro',
+ goFaq: () => this.setState({view:'overview', ovTab:'faq'}),
+ goPortfolio: () => this.setState({view:'overview', ovTab:'portfolio'}),
ovIsProspect: !onboarded,
ovIsMember: onboarded,
ovRadarGrid, ovRadarAxes, ovRadarPoints, ovRadarPlaa, ovRadarDots, ovRadarLabelEls, ovConvRows,
@@ -3628,17 +4233,17 @@ Give Community Kudos
],
// faq
- isFaq: s.view === 'faq',
+ isFaq: (s.view === 'overview' && s.ovTab === 'faq') || s.view === 'faq',
isAbout: s.view === 'about',
awPolygon, awGrid, awAxes, awDots, awLabelEls, awTickEls,
// leaderboard
- isLeaderboard: s.view === 'leaderboard',
+ isLeaderboard: (s.view === 'activities' && s.actTab === 'leaderboard') || s.view === 'leaderboard',
lbTabs, lbRows, lbCaption,
lbCatPolygon, lbCatGrid, lbCatAxes, lbCatDots, lbCatLabelEls,
lbUnder,
lbSnapOlder, lbSnapNewer, lbSnapOlderStyle, lbSnapNewerStyle, lbSnapPosition, lbMetrics, lbSnapLabel, lbSnapIsLive, lbSnapPeriod, lbSnapVsLabel,
- goActivitiesLb: () => this.navTo('activities'),
+ goActivitiesLb: () => this.setState({view:'activities', actTab:'acts', activeId:null}),
goActivitiesAbout: () => this.navTo('activities'),
goFaqFromAbout: () => this.navTo('faq'), faqQuery: s.faqQuery, onFaqSearch: (e) => this.setState({faqQuery:e.target.value}),
faqList, faqNone: faqList.length === 0,
@@ -3678,6 +4283,10 @@ Give Community Kudos
signToggleStyle: 'width:22px;height:22px;border-radius:6px;flex:none;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;'+(s.onbSigned?'background:var(--color-brand);color:#fff;':'background:var(--surface-card);box-shadow:inset 0 0 0 2px var(--border-muted);'),
finishOnboarding: () => this.finishOnboarding(),
+ periodDetailOpen: !!s.periodDetail,
+ closePeriodDetail: () => this.setState({periodDetail:null}),
+ pdTitle, pdKicker, pdLead, pdActs, pdPlaa, pdInfra, pdNet, pdListTitle, pdItems, pdFootLabel, pdFootValue, pdHasRedeem, pdRedeemNote,
+
// snapshot close
showClose: s.showClose,
closeSnap: () => this.setState({showClose:false}),
diff --git a/smoke-check.js b/smoke-check.js
index a883e5d..81cce00 100644
--- a/smoke-check.js
+++ b/smoke-check.js
@@ -23,24 +23,27 @@ const required = [
for (const file of required) assert.ok(statSync(join(root, file)).isFile(), `${file} is missing`);
const index = text('index.html');
-// Verbatim export of "Profile Page + Overview.dc.html" from
-// "Alignment Asset Site - Ready for Dev.zip" (2026-08-28).
-assert.equal(statSync(join(root, 'index.html')).size, 379567, 'index.html must remain the verbatim export');
assert.match(index, /src="\.\/support\.js"/);
assert.match(index, /src="\.\/image-slot\.js"/);
assert.match(index, /href="ds\/styles\.css"/);
for (const evidence of [
'Demo controls',
- 'PLAA is exposure to tech', // new overview hero
- 'PLAA balance', // hero consolidation (changelog §2)
- 'Infra rewards', // second PLAA stream (§1)
- 'Infra Member', // status pill gate (§1)
- 'Points and PLAA balance over time', // relabelled chart (§6)
- 'Contribution History',
+ 'PLAA is exposure to tech',
+ 'PLAA balance',
+ 'Infra Member',
+ 'Your PLAA over time',
+ 'Your Contribution History',
'Community Kudos',
+ 'Introduction to PLAA',
+ 'Ending balance',
+ 'Collect PLAA now',
+ 'Issued PLAA',
+ 'Indexed (start = 100)',
]) {
assert.ok(index.includes(evidence), `design evidence missing: ${evidence}`);
}
+assert.equal(index.includes('Points this snapshot'), false, 'hero still shows Points this snapshot');
+assert.equal(index.includes("label:'Home'"), false, 'Home still in the rail');
assert.match(text('support.js'), /fetch\(location\.href\)/);
console.log('Smoke check passed.');