122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
export const DEFAULT_SOCIAL_MILESTONE_THRESHOLDS = [1000, 10000] as const;
|
|
|
|
export type SocialMilestoneKind = `unique_scans_${number}`;
|
|
|
|
/**
|
|
* Staging can set SOCIAL_MILESTONE_THRESHOLDS=1 (or e.g. 1,2) so the complete
|
|
* flow is testable without fabricating thousands of scans. Production keeps
|
|
* the conservative defaults unless its environment explicitly changes them.
|
|
*/
|
|
export function getSocialMilestoneThresholds(): number[] {
|
|
const configured = process.env.SOCIAL_MILESTONE_THRESHOLDS;
|
|
if (!configured) return [...DEFAULT_SOCIAL_MILESTONE_THRESHOLDS];
|
|
|
|
const thresholds = Array.from(new Set(
|
|
configured.split(',')
|
|
.map(value => Number(value.trim()))
|
|
.filter(value => Number.isInteger(value) && value > 0 && value <= 1_000_000)
|
|
)).sort((a, b) => a - b);
|
|
|
|
return thresholds.length ? thresholds : [...DEFAULT_SOCIAL_MILESTONE_THRESHOLDS];
|
|
}
|
|
|
|
const useCaseLabels: Record<string, string> = {
|
|
menu_pdf: 'menu QR code',
|
|
marketing_campaign: 'campaign QR code',
|
|
vcard: 'digital business-card QR code',
|
|
event: 'event QR code',
|
|
feedback: 'feedback QR code',
|
|
};
|
|
|
|
export function milestoneKind(threshold: number): SocialMilestoneKind {
|
|
return `unique_scans_${threshold}` as SocialMilestoneKind;
|
|
}
|
|
|
|
export function milestoneThreshold(kind: string): number | null {
|
|
const result = /^unique_scans_(\d+)$/.exec(kind);
|
|
return result ? Number(result[1]) : null;
|
|
}
|
|
|
|
export type SocialLocale = 'en' | 'de';
|
|
|
|
export type SocialMilestoneCard = {
|
|
version: 'milestone-card-v2';
|
|
language: SocialLocale;
|
|
qrTitle: string;
|
|
label: string;
|
|
title: string;
|
|
totalScans: number;
|
|
totalUniqueScans: number;
|
|
milestoneThreshold: number;
|
|
reachedAt: string;
|
|
trend: {
|
|
points: Array<{ at: string; total: number }>;
|
|
startLabel: string;
|
|
endLabel: string;
|
|
target: number;
|
|
ceiling: number;
|
|
} | null;
|
|
};
|
|
|
|
export function socialLocale(value?: string | null): SocialLocale {
|
|
return value === 'de' ? 'de' : 'en';
|
|
}
|
|
|
|
export function usageLabel(primaryUseCase: string | null, locale: SocialLocale = 'en'): string {
|
|
if (locale === 'de') {
|
|
const german: Record<string, string> = { menu_pdf: 'Speisekarten-QR-Code', marketing_campaign: 'Kampagnen-QR-Code', vcard: 'Visitenkarten-QR-Code', event: 'Event-QR-Code', feedback: 'Feedback-QR-Code' };
|
|
return (primaryUseCase && german[primaryUseCase]) || 'QR-Code';
|
|
}
|
|
return (primaryUseCase && useCaseLabels[primaryUseCase]) || 'QR code';
|
|
}
|
|
|
|
export function buildMilestonePost(primaryUseCase: string | null, threshold: number, xHandle?: string | null, locale: SocialLocale = 'en'): string {
|
|
const count = threshold.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US');
|
|
const base = locale === 'de'
|
|
? `Ein ${usageLabel(primaryUseCase, locale)} hat gerade ${count} eindeutige Scans erreicht. 🎉`
|
|
: `A ${usageLabel(primaryUseCase, locale)} just reached ${count} unique scans. 🎉`;
|
|
return xHandle ? `${base} By @${xHandle.replace(/^@/, '')}.` : base;
|
|
}
|
|
|
|
export function buildMilestoneCard(primaryUseCase: string | null, threshold: number, locale: SocialLocale) {
|
|
return { version: 'milestone-card-v1', language: locale, threshold, label: usageLabel(primaryUseCase, locale), title: locale === 'de' ? 'Erfolgsmeilenstein' : 'Success milestone' };
|
|
}
|
|
|
|
export function buildMilestonePostForQr(primaryUseCase: string | null, totalUniqueScans: number, xHandle: string | null | undefined, locale: SocialLocale, qrTitle: string): string {
|
|
const count = totalUniqueScans.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US');
|
|
const subject = qrTitle.trim() || usageLabel(primaryUseCase, locale);
|
|
const base = locale === 'de'
|
|
? `„${subject}“ hat ${count} eindeutige Scans erreicht.`
|
|
: `“${subject}” reached ${count} unique scans.`;
|
|
return xHandle ? `${base} By @${xHandle.replace(/^@/, '')}.` : base;
|
|
}
|
|
|
|
export function buildMilestoneCardSnapshot(input: {
|
|
primaryUseCase: string | null;
|
|
qrTitle: string;
|
|
totalScans: number;
|
|
totalUniqueScans: number;
|
|
milestoneThreshold: number;
|
|
reachedAt: Date;
|
|
trend: SocialMilestoneCard['trend'];
|
|
locale: SocialLocale;
|
|
}): SocialMilestoneCard {
|
|
return {
|
|
version: 'milestone-card-v2',
|
|
language: input.locale,
|
|
qrTitle: input.qrTitle,
|
|
label: usageLabel(input.primaryUseCase, input.locale),
|
|
title: input.locale === 'de' ? 'Erfolgsmeilenstein' : 'Success milestone',
|
|
totalScans: input.totalScans,
|
|
totalUniqueScans: input.totalUniqueScans,
|
|
milestoneThreshold: input.milestoneThreshold,
|
|
reachedAt: input.reachedAt.toISOString(),
|
|
trend: input.trend,
|
|
};
|
|
}
|
|
|
|
export function normalizeXHandle(value: string): string | null {
|
|
const handle = value.trim().replace(/^@/, '');
|
|
return /^[A-Za-z0-9_]{1,15}$/.test(handle) ? handle : null;
|
|
}
|