Wall: optional intro video (YouTube, Vimeo or .mp4) from Profile, embedded under the bio
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,7 @@ FACTS:
|
|||||||
- Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery.
|
- Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery.
|
||||||
- Live formats: display banners (per impression), text ads (per impression), full-screen LOGIN ADS (per day: right after a member signs in they land on a sponsor interstitial — they click "Open Ad", the advertiser's page opens in a NEW tab, a countdown runs on the interstitial, and at zero a "Go to dashboard" button appears. Just a CTA link is enough; an optional banner image can be the clickable creative. No framing requirement since it opens in its own tab), WATCH-TO-EARN VIDEO ADS (advertiser uploads an MP4/WebM or gives a direct https .mp4/.webm link and picks a required watch length — 10s/30s/60s — which sets the per-view price; viewers watch in an escape-proof player under Earn credits > Watch videos, the watch time is enforced on the server clock, and they earn credits per completed watch; you never see your own videos), and solo ads. Banner ads also require a size (standard IAB sizes like 728x90, 300x250). Coming: featured rotation with disclosed rotation size, verified-visit packs.
|
- Live formats: display banners (per impression), text ads (per impression), full-screen LOGIN ADS (per day: right after a member signs in they land on a sponsor interstitial — they click "Open Ad", the advertiser's page opens in a NEW tab, a countdown runs on the interstitial, and at zero a "Go to dashboard" button appears. Just a CTA link is enough; an optional banner image can be the clickable creative. No framing requirement since it opens in its own tab), WATCH-TO-EARN VIDEO ADS (advertiser uploads an MP4/WebM or gives a direct https .mp4/.webm link and picks a required watch length — 10s/30s/60s — which sets the per-view price; viewers watch in an escape-proof player under Earn credits > Watch videos, the watch time is enforced on the server clock, and they earn credits per completed watch; you never see your own videos), and solo ads. Banner ads also require a size (standard IAB sizes like 728x90, 300x250). Coming: featured rotation with disclosed rotation size, verified-visit packs.
|
||||||
- WALLETS + BUYING POL (Training > Wallets and buying POL, /wallets, members only): preferred MetaMask (recommended; extra accounts for Qualified Start), Phantom, SafePal, Coinbase Wallet; Trust works but blocks buys spending most of its POL (keep ~2x). MoonPay flow: connect wallet, Buy packages > "Buy POL with a card" opens MoonPay with POL on Polygon + the member's address prefilled; card/Apple Pay/Google Pay; first-time ID check; minimum order ~$30; buy package cost + 2-3 POL for fees; POL arrives in minutes; then buy. Exchanges: withdraw POL on the Polygon network. Never MATIC on Ethereum, never share the recovery phrase.
|
- WALLETS + BUYING POL (Training > Wallets and buying POL, /wallets, members only): preferred MetaMask (recommended; extra accounts for Qualified Start), Phantom, SafePal, Coinbase Wallet; Trust works but blocks buys spending most of its POL (keep ~2x). MoonPay flow: connect wallet, Buy packages > "Buy POL with a card" opens MoonPay with POL on Polygon + the member's address prefilled; card/Apple Pay/Google Pay; first-time ID check; minimum order ~$30; buy package cost + 2-3 POL for fees; POL arrives in minutes; then buy. Exchanges: withdraw POL on the Polygon network. Never MATIC on Ethereum, never share the recovery phrase.
|
||||||
|
- INTRO VIDEO ON THE WALL: Profile > social links has an "Intro video" field (YouTube, Vimeo or direct .mp4 link). It embeds on the member's public wall page (/wall/<username>) right under their bio, above the three-level line and the join button.
|
||||||
- HOLDING TANK (Members > My line > Holding tank card): free members who joined with no sponsor wait there; a member who has switched on payouts AND bought their own $20+ package can Adopt one (first come, max 2 open adoptions, 7-day window; if the person never links a wallet or buys, they fall back into the tank; a person can be adopted twice at most). Adopting sets the sponsor, opens a chat and emails the member; their first purchase then binds to the adopter on-chain. Members can also "Release to tank" one of their own free referrals (pay it forward). Admin sees the tank under Members.
|
- HOLDING TANK (Members > My line > Holding tank card): free members who joined with no sponsor wait there; a member who has switched on payouts AND bought their own $20+ package can Adopt one (first come, max 2 open adoptions, 7-day window; if the person never links a wallet or buys, they fall back into the tank; a person can be adopted twice at most). Adopting sets the sponsor, opens a chat and emails the member; their first purchase then binds to the adopter on-chain. Members can also "Release to tank" one of their own free referrals (pay it forward). Admin sees the tank under Members.
|
||||||
- DORMANT-LEAD RESCUE: a FREE referral (no wallet, no purchase) with no message from their sponsor for 10 days triggers a warning email + dashboard flag to the sponsor ("unreached, tank in N days"); at 14 days (warning at least 4 days old) the lead moves to the holding tank and the sponsor is told. Sponsor resets the clock with a chat, a Nudge, or the "Contacted them" button (for phone/text contact). Leads whose sponsor link resolves to nobody go to the tank after a day. Nothing on-chain moves; anyone bound by a purchase never moves.
|
- DORMANT-LEAD RESCUE: a FREE referral (no wallet, no purchase) with no message from their sponsor for 10 days triggers a warning email + dashboard flag to the sponsor ("unreached, tank in N days"); at 14 days (warning at least 4 days old) the lead moves to the holding tank and the sponsor is told. Sponsor resets the clock with a chat, a Nudge, or the "Contacted them" button (for phone/text contact). Leads whose sponsor link resolves to nobody go to the tank after a day. Nothing on-chain moves; anyone bound by a purchase never moves.
|
||||||
- PIF (pay it forward) button: on a free direct or an adopted member who has linked a wallet, the sponsor taps PIF, enters an amount (suggested: the $20 package plus fees), and their OWN wallet app opens with the member's address prefilled; the POL goes wallet to wallet. The site never touches the funds; it only logs the transaction and tells the recipient with a Polygonscan link. The gift is theirs; nothing forces a purchase.
|
- PIF (pay it forward) button: on a free direct or an adopted member who has linked a wallet, the sponsor taps PIF, enters an amount (suggested: the $20 package plus fees), and their OWN wallet app opens with the member's address prefilled; the POL goes wallet to wallet. The site never touches the funds; it only logs the transaction and tells the recipient with a Polygonscan link. The gift is theirs; nothing forces a purchase.
|
||||||
|
|||||||
+1
-1
@@ -1924,7 +1924,7 @@
|
|||||||
IAP.status('Wall positions saved.', 'ok');
|
IAP.status('Wall positions saved.', 'ok');
|
||||||
await loadLineBanner();
|
await loadLineBanner();
|
||||||
}));
|
}));
|
||||||
const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
|
const SOCIALS = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website', 'video']; // video = intro video on the wall, not a social link
|
||||||
function fillProfileDetails(a) {
|
function fillProfileDetails(a) {
|
||||||
if (!a) return;
|
if (!a) return;
|
||||||
if (a.bio) $('pfBio').value = a.bio;
|
if (a.bio) $('pfBio').value = a.bio;
|
||||||
|
|||||||
@@ -425,6 +425,8 @@ textarea{resize:vertical;font:inherit}
|
|||||||
.btn.disabled{opacity:.45;pointer-events:none;filter:grayscale(.3)}
|
.btn.disabled{opacity:.45;pointer-events:none;filter:grayscale(.3)}
|
||||||
.bio-meta{flex:1;min-width:220px}
|
.bio-meta{flex:1;min-width:220px}
|
||||||
.bio-socials{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
.bio-socials{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
|
||||||
|
.bio-video{margin:14px auto 8px;max-width:760px}
|
||||||
|
.bio-video iframe,.bio-video video{width:100%;aspect-ratio:16/9;border:1px solid var(--line-strong);border-radius:16px;background:#000;display:block}
|
||||||
.bio-socials a{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:var(--mint);border:1px solid var(--line-strong);
|
.bio-socials a{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:var(--mint);border:1px solid var(--line-strong);
|
||||||
border-radius:999px;padding:4px 12px 4px 9px;text-decoration:none}
|
border-radius:999px;padding:4px 12px 4px 9px;text-decoration:none}
|
||||||
.bio-socials a svg{flex:0 0 auto}
|
.bio-socials a svg{flex:0 0 auto}
|
||||||
|
|||||||
@@ -41,6 +41,18 @@
|
|||||||
'<a href="' + String(w.socials[k]).replace(/"/g, '%22') + '" target="_blank" rel="noopener nofollow me">' + (ICON[k] || '') + '<span>' + SOC[k] + '</span></a>');
|
'<a href="' + String(w.socials[k]).replace(/"/g, '%22') + '" target="_blank" rel="noopener nofollow me">' + (ICON[k] || '') + '<span>' + SOC[k] + '</span></a>');
|
||||||
sc.innerHTML = links.join('');
|
sc.innerHTML = links.join('');
|
||||||
}
|
}
|
||||||
|
// intro video under the bio: YouTube / Vimeo embed, or a direct file
|
||||||
|
const bv = IAP.$('bioVideo');
|
||||||
|
const vurl = w.socials && typeof w.socials === 'object' ? String(w.socials.video || '') : '';
|
||||||
|
if (bv && vurl) {
|
||||||
|
let yt = /(?:youtube\.com\/(?:watch\?(?:.*&)?v=|shorts\/|embed\/)|youtu\.be\/)([A-Za-z0-9_-]{6,})/.exec(vurl);
|
||||||
|
let vm = /vimeo\.com\/(?:video\/)?(\d+)/.exec(vurl);
|
||||||
|
let inner = '';
|
||||||
|
if (yt) inner = '<iframe src="https://www.youtube-nocookie.com/embed/' + yt[1] + '?rel=0" title="Intro video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>';
|
||||||
|
else if (vm) inner = '<iframe src="https://player.vimeo.com/video/' + vm[1] + '" title="Intro video" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen loading="lazy"></iframe>';
|
||||||
|
else if (/\.(mp4|webm)(\?|$)/i.test(vurl)) inner = '<video src="' + vurl.replace(/"/g, '%22') + '" controls playsinline preload="metadata"></video>';
|
||||||
|
if (inner) { bv.innerHTML = '<p class="eyebrow" style="margin:0 0 8px">A word from ' + String(w.name).replace(/[&<>]/g, '') + '</p>' + inner; bv.hidden = false; }
|
||||||
|
}
|
||||||
IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line';
|
IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line';
|
||||||
// per-wall viewed-position memory (survives revisits; anonymous-friendly)
|
// per-wall viewed-position memory (survives revisits; anonymous-friendly)
|
||||||
const VKEY = 'iap-wall-' + name;
|
const VKEY = 'iap-wall-' + name;
|
||||||
|
|||||||
+4
-2
@@ -5,7 +5,7 @@
|
|||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260911c">
|
<link rel="stylesheet" href="/assets/site.css?v=20260911v">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -749,6 +749,8 @@
|
|||||||
<input id="soc-linkedin" placeholder="LinkedIn URL" style="width:100%">
|
<input id="soc-linkedin" placeholder="LinkedIn URL" style="width:100%">
|
||||||
<input id="soc-website" placeholder="Website URL" style="width:100%">
|
<input id="soc-website" placeholder="Website URL" style="width:100%">
|
||||||
</div>
|
</div>
|
||||||
|
<p class="small muted" style="margin:10px 0 4px">Intro video on your public page (optional): a YouTube, Vimeo or direct .mp4 link. It plays right under your bio, before the line and the join button.</p>
|
||||||
|
<p><input id="soc-video" placeholder="https://www.youtube.com/watch?v=…" style="width:100%"></p>
|
||||||
<p style="margin-top:12px"><button class="btn" id="pfDetailsSave">Save profile</button>
|
<p style="margin-top:12px"><button class="btn" id="pfDetailsSave">Save profile</button>
|
||||||
<a class="btn small sec" id="pfViewBio" target="_blank" rel="noopener" hidden>View my page</a></p>
|
<a class="btn small sec" id="pfViewBio" target="_blank" rel="noopener" hidden>View my page</a></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -900,7 +902,7 @@
|
|||||||
<script src="/assets/common.js?v=20260911b"></script>
|
<script src="/assets/common.js?v=20260911b"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260911g"></script>
|
<script src="/assets/my.js?v=20260911v"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+3
-2
@@ -6,7 +6,7 @@
|
|||||||
<!--OG-->
|
<!--OG-->
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260910h">
|
<link rel="stylesheet" href="/assets/site.css?v=20260911v">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="nav"></div>
|
<div id="nav"></div>
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bio-qr"><img id="bioQr" alt="Join QR code" width="150" height="150"><div class="small muted" style="text-align:center">scan to join</div></div>
|
<div class="bio-qr"><img id="bioQr" alt="Join QR code" width="150" height="150"><div class="small muted" style="text-align:center">scan to join</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bio-video" id="bioVideo" hidden></div>
|
||||||
<div class="sectionhead" style="text-align:left;margin-top:10px">
|
<div class="sectionhead" style="text-align:left;margin-top:10px">
|
||||||
<h3 style="margin:0">The line, three levels deep</h3>
|
<h3 style="margin:0">The line, three levels deep</h3>
|
||||||
<p>Three positions, three levels — the exact levels the contract pays. Every banner here belongs
|
<p>Three positions, three levels — the exact levels the contract pays. Every banner here belongs
|
||||||
@@ -47,6 +48,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<script src="/assets/common.js?v=20260910c"></script>
|
<script src="/assets/common.js?v=20260910c"></script>
|
||||||
<script src="/assets/wall.js?v=20260910b"></script>
|
<script src="/assets/wall.js?v=20260911v"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1409,12 +1409,15 @@ const server = http.createServer(async (req, res) => {
|
|||||||
// socials: {platform: url}; keep only known platforms with valid https urls
|
// socials: {platform: url}; keep only known platforms with valid https urls
|
||||||
let socials;
|
let socials;
|
||||||
if (b.socials !== undefined) {
|
if (b.socials !== undefined) {
|
||||||
const PLAT = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website'];
|
const PLAT = ['facebook', 'twitter', 'youtube', 'instagram', 'tiktok', 'telegram', 'linkedin', 'website', 'video'];
|
||||||
const clean = {};
|
const clean = {};
|
||||||
for (const p of PLAT) {
|
for (const p of PLAT) {
|
||||||
const v = String((b.socials && b.socials[p]) || '').trim();
|
const v = String((b.socials && b.socials[p]) || '').trim();
|
||||||
if (v && /^https:\/\/[^\s]+$/i.test(v) && v.length <= 200) clean[p] = v;
|
if (v && /^https:\/\/[^\s]+$/i.test(v) && v.length <= 300) clean[p] = v;
|
||||||
}
|
}
|
||||||
|
// intro video on the public wall: a YouTube, Vimeo or direct .mp4/.webm link only
|
||||||
|
if (clean.video && !/^https:\/\/((www\.|m\.)?youtube\.com\/(watch\?|shorts\/|embed\/)|youtu\.be\/|(www\.)?vimeo\.com\/\d+|player\.vimeo\.com\/video\/\d+|[^\s]+\.(mp4|webm)(\?|$))/i.test(clean.video))
|
||||||
|
return json(res, 400, { error: 'The intro video needs to be a YouTube link, a Vimeo link, or a direct .mp4 link.' });
|
||||||
socials = Object.keys(clean).length ? JSON.stringify(clean) : null;
|
socials = Object.keys(clean).length ? JSON.stringify(clean) : null;
|
||||||
}
|
}
|
||||||
const r = await accounts.setProfile(s.email, avatar, bio, socials);
|
const r = await accounts.setProfile(s.email, avatar, bio, socials);
|
||||||
|
|||||||
Reference in New Issue
Block a user