src / donations.ts
src / donations.ts
/**
* Donation & Supporter System for web-search
*
* Manages:
* - Supporter recognition
* - Donation prompts (non-intrusive)
* - Credits display
*/
import * as fs from "fs";
import { join } from "path";
// ---------------------------------------------------------------------------
// Supporter Data
// ---------------------------------------------------------------------------
interface SupporterData {
founding: string[];
pizza: string[];
lunch: string[];
coffee: string[];
stats: {
totalDonations: number;
totalSupporters: number;
lastUpdated: string;
};
}
// Default supporters file (shipped with plugin)
let supportersData: SupporterData = {
founding: [],
pizza: [],
lunch: [],
coffee: [],
stats: {
totalDonations: 0,
totalSupporters: 0,
lastUpdated: new Date().toISOString().split("T")[0],
},
};
// Try to load supporters data
try {
const dataPath = join(__dirname, "supporters.json");
if (fs.existsSync(dataPath)) {
supportersData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
}
} catch {
// Use defaults
}
// ---------------------------------------------------------------------------
// Donation Tiers
// ---------------------------------------------------------------------------
export type DonationTier = "founding" | "pizza" | "lunch" | "coffee";
const TIER_EMOJI: Record<DonationTier, string> = {
founding: "🏆",
pizza: "🍕",
lunch: "🍔",
coffee: "☕",
};
const TIER_NAMES: Record<DonationTier, string> = {
founding: "Founding Member",
pizza: "Pizza Tier",
lunch: "Lunch Tier",
coffee: "Coffee Tier",
};
const TIER_AMOUNTS: Record<DonationTier, string> = {
founding: "$50+",
pizza: "$25",
lunch: "$10",
coffee: "$3",
};
// ---------------------------------------------------------------------------
// Supporter Functions
// ---------------------------------------------------------------------------
export function isSupporter(name: string): boolean {
const allSupporters = [
...supportersData.founding,
...supportersData.pizza,
...supportersData.lunch,
...supportersData.coffee,
];
return allSupporters.some(
(s) => s.toLowerCase() === name.toLowerCase()
);
}
export function getSupporterTier(name: string): DonationTier | null {
if (supportersData.founding.some((s) => s.toLowerCase() === name.toLowerCase())) return "founding";
if (supportersData.pizza.some((s) => s.toLowerCase() === name.toLowerCase())) return "pizza";
if (supportersData.lunch.some((s) => s.toLowerCase() === name.toLowerCase())) return "lunch";
if (supportersData.coffee.some((s) => s.toLowerCase() === name.toLowerCase())) return "coffee";
return null;
}
export function getSupporterMessage(name: string): string | null {
const tier = getSupporterTier(name);
if (!tier) return null;
const emoji = TIER_EMOJI[tier];
return `${emoji} Thanks for supporting web-search, ${name}! You're a ${TIER_NAMES[tier]}.`;
}
export function getStats() {
return {
total: supportersData.stats.totalSupporters,
founding: supportersData.founding.length,
pizza: supportersData.pizza.length,
lunch: supportersData.lunch.length,
coffee: supportersData.coffee.length,
};
}
// ---------------------------------------------------------------------------
// Donation Prompt (Non-intrusive)
// ---------------------------------------------------------------------------
// Track when we last showed the prompt
let lastPromptTime = 0;
const PROMPT_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours
export function shouldShowDonationPrompt(): boolean {
const now = Date.now();
if (now - lastPromptTime < PROMPT_COOLDOWN) {
return false;
}
lastPromptTime = now;
return true;
}
export function getDonationPrompt(): string {
const stats = getStats();
const supporterCount = stats.total;
// Different messages based on supporter count
if (supporterCount === 0) {
return `
💡 web-search is free and open source. If you find it useful, consider supporting development:
https://ko-fi.com/thriloke96
Every coffee helps keep this project alive! ☕`;
}
return `
💡 web-search is free, thanks to ${supporterCount} supporters!
Want to join them? https://ko-fi.com/thriloke96
🏆 Founding Members get priority support + feature requests.`;
}
// ---------------------------------------------------------------------------
// Credits Display
// ---------------------------------------------------------------------------
export function getCreditsText(): string {
const lines: string[] = [];
if (supportersData.founding.length > 0) {
lines.push(`🏆 Founding Members: ${supportersData.founding.join(", ")}`);
}
if (supportersData.pizza.length > 0) {
lines.push(`🍕 Pizza Tier: ${supportersData.pizza.join(", ")}`);
}
if (supportersData.lunch.length > 0) {
lines.push(`🍔 Lunch Tier: ${supportersData.lunch.join(", ")}`);
}
if (supportersData.coffee.length > 0) {
const coffeeList = supportersData.coffee.slice(0, 5).join(", ");
const remaining = supportersData.coffee.length - 5;
lines.push(`☕ Coffee Tier: ${coffeeList}${remaining > 0 ? ` +${remaining} others` : ""}`);
}
if (lines.length === 0) {
return "web-search is free and open source. No supporters yet — be the first!";
}
return "Made possible by supporters:\n" + lines.join("\n");
}
// ---------------------------------------------------------------------------
// Add Supporter (for admin use)
// ---------------------------------------------------------------------------
export function addSupporter(name: string, tier: DonationTier): void {
if (!supportersData[tier].includes(name)) {
supportersData[tier].push(name);
supportersData.stats.totalSupporters++;
supportersData.stats.lastUpdated = new Date().toISOString().split("T")[0];
// Save to file
try {
const dataPath = join(__dirname, "supporters.json");
fs.writeFileSync(dataPath, JSON.stringify(supportersData, null, 2));
} catch {
// File write failed, but data is updated in memory
}
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
export const donationManager = {
isSupporter,
getSupporterTier,
getSupporterMessage,
getStats,
shouldShowDonationPrompt,
getDonationPrompt,
getCreditsText,
addSupporter,
TIER_EMOJI,
TIER_NAMES,
TIER_AMOUNTS,
};
/**
* Donation & Supporter System for web-search
*
* Manages:
* - Supporter recognition
* - Donation prompts (non-intrusive)
* - Credits display
*/
import * as fs from "fs";
import { join } from "path";
// ---------------------------------------------------------------------------
// Supporter Data
// ---------------------------------------------------------------------------
interface SupporterData {
founding: string[];
pizza: string[];
lunch: string[];
coffee: string[];
stats: {
totalDonations: number;
totalSupporters: number;
lastUpdated: string;
};
}
// Default supporters file (shipped with plugin)
let supportersData: SupporterData = {
founding: [],
pizza: [],
lunch: [],
coffee: [],
stats: {
totalDonations: 0,
totalSupporters: 0,
lastUpdated: new Date().toISOString().split("T")[0],
},
};
// Try to load supporters data
try {
const dataPath = join(__dirname, "supporters.json");
if (fs.existsSync(dataPath)) {
supportersData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
}
} catch {
// Use defaults
}
// ---------------------------------------------------------------------------
// Donation Tiers
// ---------------------------------------------------------------------------
export type DonationTier = "founding" | "pizza" | "lunch" | "coffee";
const TIER_EMOJI: Record<DonationTier, string> = {
founding: "🏆",
pizza: "🍕",
lunch: "🍔",
coffee: "☕",
};
const TIER_NAMES: Record<DonationTier, string> = {
founding: "Founding Member",
pizza: "Pizza Tier",
lunch: "Lunch Tier",
coffee: "Coffee Tier",
};
const TIER_AMOUNTS: Record<DonationTier, string> = {
founding: "$50+",
pizza: "$25",
lunch: "$10",
coffee: "$3",
};
// ---------------------------------------------------------------------------
// Supporter Functions
// ---------------------------------------------------------------------------
export function isSupporter(name: string): boolean {
const allSupporters = [
...supportersData.founding,
...supportersData.pizza,
...supportersData.lunch,
...supportersData.coffee,
];
return allSupporters.some(
(s) => s.toLowerCase() === name.toLowerCase()
);
}
export function getSupporterTier(name: string): DonationTier | null {
if (supportersData.founding.some((s) => s.toLowerCase() === name.toLowerCase())) return "founding";
if (supportersData.pizza.some((s) => s.toLowerCase() === name.toLowerCase())) return "pizza";
if (supportersData.lunch.some((s) => s.toLowerCase() === name.toLowerCase())) return "lunch";
if (supportersData.coffee.some((s) => s.toLowerCase() === name.toLowerCase())) return "coffee";
return null;
}
export function getSupporterMessage(name: string): string | null {
const tier = getSupporterTier(name);
if (!tier) return null;
const emoji = TIER_EMOJI[tier];
return `${emoji} Thanks for supporting web-search, ${name}! You're a ${TIER_NAMES[tier]}.`;
}
export function getStats() {
return {
total: supportersData.stats.totalSupporters,
founding: supportersData.founding.length,
pizza: supportersData.pizza.length,
lunch: supportersData.lunch.length,
coffee: supportersData.coffee.length,
};
}
// ---------------------------------------------------------------------------
// Donation Prompt (Non-intrusive)
// ---------------------------------------------------------------------------
// Track when we last showed the prompt
let lastPromptTime = 0;
const PROMPT_COOLDOWN = 24 * 60 * 60 * 1000; // 24 hours
export function shouldShowDonationPrompt(): boolean {
const now = Date.now();
if (now - lastPromptTime < PROMPT_COOLDOWN) {
return false;
}
lastPromptTime = now;
return true;
}
export function getDonationPrompt(): string {
const stats = getStats();
const supporterCount = stats.total;
// Different messages based on supporter count
if (supporterCount === 0) {
return `
💡 web-search is free and open source. If you find it useful, consider supporting development:
https://ko-fi.com/thriloke96
Every coffee helps keep this project alive! ☕`;
}
return `
💡 web-search is free, thanks to ${supporterCount} supporters!
Want to join them? https://ko-fi.com/thriloke96
🏆 Founding Members get priority support + feature requests.`;
}
// ---------------------------------------------------------------------------
// Credits Display
// ---------------------------------------------------------------------------
export function getCreditsText(): string {
const lines: string[] = [];
if (supportersData.founding.length > 0) {
lines.push(`🏆 Founding Members: ${supportersData.founding.join(", ")}`);
}
if (supportersData.pizza.length > 0) {
lines.push(`🍕 Pizza Tier: ${supportersData.pizza.join(", ")}`);
}
if (supportersData.lunch.length > 0) {
lines.push(`🍔 Lunch Tier: ${supportersData.lunch.join(", ")}`);
}
if (supportersData.coffee.length > 0) {
const coffeeList = supportersData.coffee.slice(0, 5).join(", ");
const remaining = supportersData.coffee.length - 5;
lines.push(`☕ Coffee Tier: ${coffeeList}${remaining > 0 ? ` +${remaining} others` : ""}`);
}
if (lines.length === 0) {
return "web-search is free and open source. No supporters yet — be the first!";
}
return "Made possible by supporters:\n" + lines.join("\n");
}
// ---------------------------------------------------------------------------
// Add Supporter (for admin use)
// ---------------------------------------------------------------------------
export function addSupporter(name: string, tier: DonationTier): void {
if (!supportersData[tier].includes(name)) {
supportersData[tier].push(name);
supportersData.stats.totalSupporters++;
supportersData.stats.lastUpdated = new Date().toISOString().split("T")[0];
// Save to file
try {
const dataPath = join(__dirname, "supporters.json");
fs.writeFileSync(dataPath, JSON.stringify(supportersData, null, 2));
} catch {
// File write failed, but data is updated in memory
}
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
export const donationManager = {
isSupporter,
getSupporterTier,
getSupporterMessage,
getStats,
shouldShowDonationPrompt,
getDonationPrompt,
getCreditsText,
addSupporter,
TIER_EMOJI,
TIER_NAMES,
TIER_AMOUNTS,
};