add supporters section to homepage
Shows paying squadrons as pill links (SHORT // LONG → /squadrons/<short>) above the footer, with a 15-min server-side cache backed by entitlements DB + SQUADRONS.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -102,7 +102,8 @@
|
|||||||
"ctaReign": "Ready to R3IGN again?",
|
"ctaReign": "Ready to R3IGN again?",
|
||||||
"ctaMeow": "Meowww",
|
"ctaMeow": "Meowww",
|
||||||
"ctaPurr": "Purrr",
|
"ctaPurr": "Purrr",
|
||||||
"ctaRawr": "Rawr"
|
"ctaRawr": "Rawr",
|
||||||
|
"supportedBy": "Supported by"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Documentation",
|
"title": "Documentation",
|
||||||
|
|||||||
+46
-1
@@ -98,6 +98,7 @@ fs.mkdirSync(REPLAYS_ROOT, { recursive: true });
|
|||||||
const LEGACY_REPLAYS_ROOT = path.join(__dirname, '..', 'replays');
|
const LEGACY_REPLAYS_ROOT = path.join(__dirname, '..', 'replays');
|
||||||
const ENTITLEMENTS_DB_PATH = path.join(STORAGE_ROOT, 'entitlements.db');
|
const ENTITLEMENTS_DB_PATH = path.join(STORAGE_ROOT, 'entitlements.db');
|
||||||
const SQUADRONS_DB_PATH = path.join(STORAGE_ROOT, 'squadrons.db');
|
const SQUADRONS_DB_PATH = path.join(STORAGE_ROOT, 'squadrons.db');
|
||||||
|
const SQUADRONS_JSON_PATH = path.join(STORAGE_ROOT, 'SQUADRONS.json');
|
||||||
const entitlementsDb = new sqlite3.Database(ENTITLEMENTS_DB_PATH);
|
const entitlementsDb = new sqlite3.Database(ENTITLEMENTS_DB_PATH);
|
||||||
const squadronsDb = fs.existsSync(SQUADRONS_DB_PATH)
|
const squadronsDb = fs.existsSync(SQUADRONS_DB_PATH)
|
||||||
? new sqlite3.Database(SQUADRONS_DB_PATH, sqlite3.OPEN_READONLY)
|
? new sqlite3.Database(SQUADRONS_DB_PATH, sqlite3.OPEN_READONLY)
|
||||||
@@ -824,12 +825,56 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Supporters (paying squadrons) ────────────────────────────────────────────
|
||||||
|
let _supportersCache = null;
|
||||||
|
let _supportersCacheAt = 0;
|
||||||
|
const SUPPORTERS_TTL = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
function getSupporters() {
|
||||||
|
if (_supportersCache && Date.now() - _supportersCacheAt < SUPPORTERS_TTL) {
|
||||||
|
return Promise.resolve(_supportersCache);
|
||||||
|
}
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let squads;
|
||||||
|
try {
|
||||||
|
squads = JSON.parse(fs.readFileSync(SQUADRONS_JSON_PATH, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
entitlementsDb.all(
|
||||||
|
`SELECT guild_id FROM guild_entitlements WHERE status='active'
|
||||||
|
UNION SELECT guild_id FROM discord_entitlements
|
||||||
|
UNION SELECT guild_id FROM manual_entitlements WHERE expires_at > strftime('%s','now')`,
|
||||||
|
[],
|
||||||
|
(err, rows) => {
|
||||||
|
if (err) return resolve([]);
|
||||||
|
const paying = new Set(rows.map(r => r.guild_id));
|
||||||
|
const seen = new Map();
|
||||||
|
for (const [gid, sq] of Object.entries(squads)) {
|
||||||
|
if (paying.has(gid) && !seen.has(sq.SQ_ShortHand_Name)) {
|
||||||
|
seen.set(sq.SQ_ShortHand_Name, sq.SQ_LongHandName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = [...seen.entries()]
|
||||||
|
.map(([short, long]) => ({ short, long }))
|
||||||
|
.sort((a, b) => a.long.localeCompare(b.long));
|
||||||
|
_supportersCache = result;
|
||||||
|
_supportersCacheAt = Date.now();
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.get('/', (req, res) => {
|
app.get('/', async (req, res) => {
|
||||||
const siteUrl = (process.env.PRODUCTION_DOMAIN || 'https://sre.pawjob.us').replace(/\/$/, '');
|
const siteUrl = (process.env.PRODUCTION_DOMAIN || 'https://sre.pawjob.us').replace(/\/$/, '');
|
||||||
|
const supporters = await getSupporters().catch(() => []);
|
||||||
|
|
||||||
res.render('index', {
|
res.render('index', {
|
||||||
botName: 'Toothless SQB Bot',
|
botName: 'Toothless SQB Bot',
|
||||||
|
supporters,
|
||||||
metaTitle: "Toothless' SRE Bot",
|
metaTitle: "Toothless' SRE Bot",
|
||||||
metaDescription: 'The Best Squadron Battles Bot.',
|
metaDescription: 'The Best Squadron Battles Bot.',
|
||||||
metaUrl: `${siteUrl}/`,
|
metaUrl: `${siteUrl}/`,
|
||||||
|
|||||||
@@ -366,6 +366,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Supporters -->
|
||||||
|
<% if (typeof supporters !== 'undefined' && supporters.length > 0) { %>
|
||||||
|
<section class="py-10 border-t border-[rgba(144,238,144,0.06)]">
|
||||||
|
<div class="max-w-[1400px] mx-auto px-6 lg:px-8">
|
||||||
|
<p class="text-center text-xs uppercase tracking-widest text-muted opacity-50 mb-5"><%= t('home.supportedBy') %></p>
|
||||||
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
|
<% supporters.forEach(function(sq) { %>
|
||||||
|
<a href="/squadrons/<%= encodeURIComponent(sq.short) %>"
|
||||||
|
class="px-3 py-1 rounded-full text-[11px] font-medium border border-[rgba(144,238,144,0.15)] text-muted hover:text-accent hover:border-[rgba(144,238,144,0.35)] transition-colors">
|
||||||
|
<span class="opacity-60"><%= sq.short %></span> // <%= sq.long %>
|
||||||
|
</a>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user