Video once-per-day dedup + featured gold border & spacing
- Video watch-to-earn: a member can no longer re-earn from the same video the same day. New video_seen table (dual-mode); serveVideo excludes today's already-watched videos; videowatch double-checks before crediting. - Featured card: equal top/bottom margin (no longer touches the stat cards) and a gold border on the card + the rotating link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,14 @@ async function bootstrap() {
|
||||
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
|
||||
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1');
|
||||
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
|
||||
await q(`CREATE TABLE IF NOT EXISTS video_seen (
|
||||
email VARCHAR(190) NOT NULL,
|
||||
campaign_id INT NOT NULL,
|
||||
day CHAR(10) NOT NULL,
|
||||
ts BIGINT NOT NULL,
|
||||
UNIQUE KEY uq_vseen (email, campaign_id, day),
|
||||
INDEX (email, day)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
await q(`CREATE TABLE IF NOT EXISTS ad_reports (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
campaign_id INT NOT NULL,
|
||||
|
||||
@@ -274,12 +274,12 @@ textarea{resize:vertical;font:inherit}
|
||||
.feat-day .fd-occ{font-family:var(--mono);font-size:11px;color:var(--muted)}
|
||||
.feat-day.open2 .fd-occ{color:var(--mint)}
|
||||
/* ── featured rotation strip ── */
|
||||
.feat-top{max-width:760px;margin:0 auto 16px}
|
||||
.feat-top{max-width:760px;margin:16px auto;border:1px solid rgba(255,177,56,.5)}
|
||||
.feat-strip{display:flex;justify-content:center;margin-top:10px}
|
||||
.feat-strip a{max-width:728px;width:100%}
|
||||
.feat-strip a{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;
|
||||
background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);text-decoration:none;font-weight:600}
|
||||
.feat-strip a:hover{border-color:var(--mint)}
|
||||
.feat-strip a{max-width:728px;width:100%;display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;
|
||||
background:var(--panel);border:1px solid #ffb238;color:var(--ink);text-decoration:none;font-weight:600}
|
||||
.feat-strip a:hover{border-color:#ffd15c;box-shadow:0 0 0 1px rgba(255,177,56,.4)}
|
||||
.feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap}
|
||||
.feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap}
|
||||
/* ── achievement badges ── */
|
||||
.badges{display:flex;gap:18px;flex-wrap:wrap;margin-top:12px}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<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="/assets/site.css?v=20260907n">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260907o">
|
||||
</head>
|
||||
<body class="bo-body">
|
||||
|
||||
|
||||
@@ -934,9 +934,12 @@ const server = http.createServer(async (req, res) => {
|
||||
if (age < t.secs * 1000 - 600) return json(res, 400, { error: 'Watch the full video first.' });
|
||||
if (age > t.secs * 1000 + 10 * 60 * 1000) { videoTokens.delete(s.email); return json(res, 400, { error: 'That watch went stale. Load a fresh video.' }); }
|
||||
videoTokens.delete(s.email); // single use
|
||||
if (await ads.hasWatchedVideoToday(s.email, t.id)) // once-per-day-per-video: no double earning
|
||||
return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), already: true });
|
||||
const tier = await ads.chargeVideoView(t.id); // charge advertiser; null if it ran dry
|
||||
if (!tier) return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), gone: true });
|
||||
await ads.addEarned(s.email, tier.reward);
|
||||
await ads.markVideoSeen(s.email, t.id);
|
||||
const status = await ads.recordVideoWatch(s.email);
|
||||
return json(res, 200, { ok: true, credited: tier.reward, status });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user