Brings the working codebase (Next.js app, auth system, Stripe billing, Docker/deploy config, tests, docs) into version control on top of the placeholder initial commit, and adds account self-deletion (Danger Zone in Settings, password + typed-email confirmation, cascading DB cleanup, Stripe cancellation) per GDPR right-to-erasure. Excludes local build caches, node_modules, and internal agent scratch files; .gitignore hardened to keep those out going forward. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
2.0 KiB
JavaScript
84 lines
2.0 KiB
JavaScript
const EN_KEYWORDS = [
|
|
'receipt scanner',
|
|
'receipt to excel',
|
|
'receipt scanner to excel',
|
|
'expense tracker',
|
|
'receipt keeper',
|
|
'invoice scanner',
|
|
'tax receipts',
|
|
'ocr receipt scanner',
|
|
'smart receipts',
|
|
'mileage and receipts',
|
|
'receipt organizer',
|
|
'bookkeeping scanner',
|
|
'scan receipts for taxes',
|
|
'extract receipt to csv',
|
|
'business expense tracker'
|
|
];
|
|
|
|
const DE_KEYWORDS = [
|
|
'beleg scanner',
|
|
'belege digitalisieren',
|
|
'rechnung scanner',
|
|
'kassenbon scanner',
|
|
'quittung scanner',
|
|
'spesen app',
|
|
'buchhaltung scanner',
|
|
'datev scanner',
|
|
'receipt to excel',
|
|
'haushaltsbuch',
|
|
'ausgaben tracker',
|
|
'steuer belege',
|
|
'belegmanager'
|
|
];
|
|
|
|
const COUNTRIES = {
|
|
US: 'us',
|
|
GB: 'gb',
|
|
CA: 'ca',
|
|
AU: 'au',
|
|
DE: 'de'
|
|
};
|
|
|
|
async function fetchSearch(term, country) {
|
|
try {
|
|
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&country=${country}&entity=software&limit=5`;
|
|
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
if (!res.ok) return [];
|
|
const data = await res.json();
|
|
return data.results || [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
const summary = {};
|
|
|
|
for (const [cName, cCode] of Object.entries(COUNTRIES)) {
|
|
summary[cName] = {};
|
|
const kws = cName === 'DE' ? DE_KEYWORDS : EN_KEYWORDS;
|
|
|
|
// run in chunks of 5
|
|
for (let i = 0; i < kws.length; i += 5) {
|
|
const chunk = kws.slice(i, i + 5);
|
|
await Promise.all(chunk.map(async (kw) => {
|
|
const apps = await fetchSearch(kw, cCode);
|
|
const top5Reviews = apps.reduce((sum, a) => sum + (a.userRatingCount || 0), 0);
|
|
const avgReviews = Math.round(top5Reviews / (apps.length || 1));
|
|
const top1 = apps[0] || {};
|
|
|
|
summary[cName][kw] = {
|
|
count: apps.length,
|
|
top1: top1.trackName ? `${top1.trackName.slice(0, 30)} (${top1.userRatingCount || 0} rev)` : 'None',
|
|
avgReviews
|
|
};
|
|
}));
|
|
}
|
|
}
|
|
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
}
|
|
|
|
run();
|