mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 13:46:41 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
|
|
@ -0,0 +1,251 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { hashManager } from '../service/HashManager';
|
||||
import {
|
||||
getVariantContent,
|
||||
getTrustBadges,
|
||||
getFreeText,
|
||||
type VariantContent,
|
||||
} from '../config/variants';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PageData, ActionData } from '../../../routes/$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
form: ActionData;
|
||||
onSubmit?: () => void;
|
||||
}
|
||||
|
||||
let { data, form, onSubmit }: Props = $props();
|
||||
|
||||
let variant = $state<string>('control');
|
||||
let content = $state<VariantContent>(getVariantContent('control'));
|
||||
let trustBadges = $state(getTrustBadges());
|
||||
let freeText = $state(getFreeText());
|
||||
let showDebug = $state(false);
|
||||
let isLoading = $state(true);
|
||||
|
||||
onMount(() => {
|
||||
// Get variant assignment
|
||||
variant = hashManager.getVariant();
|
||||
content = getVariantContent(variant);
|
||||
trustBadges = getTrustBadges();
|
||||
freeText = getFreeText();
|
||||
showDebug = hashManager.isDebugMode();
|
||||
isLoading = false;
|
||||
|
||||
// Track page view with variant
|
||||
if (typeof window !== 'undefined' && (window as any).umami) {
|
||||
(window as any).umami.track(`page_view_${variant}`);
|
||||
}
|
||||
|
||||
// Log for debugging
|
||||
if (showDebug) {
|
||||
console.log('A/B Test Variant:', variant, content);
|
||||
console.log('Current Locale:', get(locale));
|
||||
}
|
||||
});
|
||||
|
||||
// React to locale changes - use derived state
|
||||
$effect(() => {
|
||||
// This will re-run when locale changes
|
||||
const currentLocale = get(locale);
|
||||
|
||||
// Update content based on current locale
|
||||
content = getVariantContent(variant);
|
||||
trustBadges = getTrustBadges();
|
||||
freeText = getFreeText();
|
||||
|
||||
if (showDebug) {
|
||||
console.log('Locale changed to:', currentLocale);
|
||||
}
|
||||
});
|
||||
|
||||
function handleCtaClick() {
|
||||
// Track CTA click
|
||||
if (typeof window !== 'undefined' && (window as any).umami) {
|
||||
(window as any).umami.track(`cta_click_${variant}`);
|
||||
}
|
||||
onSubmit?.();
|
||||
|
||||
// Smooth scroll to form
|
||||
const form = document.getElementById('url-form');
|
||||
if (form) {
|
||||
form.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showDebug}
|
||||
<div class="fixed right-4 top-20 z-50 rounded-lg bg-black/80 p-4 text-white shadow-lg">
|
||||
<div class="font-mono text-xs">
|
||||
<div class="font-bold text-green-400">A/B Test Debug</div>
|
||||
<div>Variant: <span class="text-yellow-400">{variant}</span></div>
|
||||
<div>Name: {content.name}</div>
|
||||
<div>Locale: <span class="text-blue-400">{get(locale)}</span></div>
|
||||
<div class="mt-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
hashManager.reset();
|
||||
window.location.reload();
|
||||
}}
|
||||
class="rounded bg-red-600 px-2 py-1 text-xs hover:bg-red-700"
|
||||
>
|
||||
Reset & Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section
|
||||
class="relative overflow-hidden bg-gradient-to-br from-purple-50 via-white to-blue-50 dark:from-gray-900 dark:via-gray-900 dark:to-purple-900"
|
||||
>
|
||||
<!-- Background decoration -->
|
||||
<div
|
||||
class="absolute -right-40 -top-40 h-80 w-80 rounded-full bg-purple-300 opacity-20 blur-3xl"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-blue-300 opacity-20 blur-3xl"
|
||||
></div>
|
||||
|
||||
<div class="relative mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
|
||||
{#if !isLoading}
|
||||
<div class="text-center">
|
||||
<!-- Headline -->
|
||||
<h1
|
||||
class="text-4xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-5xl md:text-6xl"
|
||||
>
|
||||
{#if variant === 'b2' && content.headline.includes(',')}
|
||||
<!-- Special formatting for logos variant -->
|
||||
<span class="block">{content.headline.split(',')[0]},</span>
|
||||
<span class="block text-3xl sm:text-4xl md:text-5xl"
|
||||
>{content.headline.split(',').slice(1).join(',')}</span
|
||||
>
|
||||
{:else}
|
||||
{content.headline}
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<!-- Subheadline -->
|
||||
<p class="mx-auto mt-6 max-w-2xl text-lg text-gray-600 dark:text-gray-300 sm:text-xl">
|
||||
{content.subheadline}
|
||||
</p>
|
||||
|
||||
<!-- Social Proof (if present) -->
|
||||
{#if content.socialProof}
|
||||
<div class="mt-8">
|
||||
{#if content.socialProof.type === 'numbers'}
|
||||
<div
|
||||
class="flex flex-wrap justify-center gap-4 text-sm font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{#each content.socialProof.content.split('•') as stat}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="text-green-500">✓</span>
|
||||
{stat.trim()}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if content.socialProof.type === 'logos'}
|
||||
<div class="mt-4 flex flex-wrap justify-center gap-6 opacity-60 grayscale">
|
||||
{#each content.socialProof.content.split('•') as logo}
|
||||
<span class="text-lg font-semibold text-gray-700 dark:text-gray-300">
|
||||
{logo.trim()}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if content.socialProof.type === 'testimonial'}
|
||||
<div class="mt-4 text-yellow-500">
|
||||
{content.socialProof.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Features List (if present) -->
|
||||
{#if content.features && content.features.length > 0}
|
||||
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
||||
{#each content.features.slice(0, 3) as feature}
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-full bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm dark:bg-gray-800/80 dark:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{feature}
|
||||
</div>
|
||||
{/each}
|
||||
{#if content.features.length > 3}
|
||||
{#each content.features.slice(3) as feature}
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-full bg-white/80 px-4 py-2 text-sm font-medium text-gray-700 shadow-sm dark:bg-gray-800/80 dark:text-gray-300"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
{feature}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- CTA Button -->
|
||||
<div class="mx-auto mt-10 max-w-xl">
|
||||
<a
|
||||
href="#url-form"
|
||||
onclick={handleCtaClick}
|
||||
class="inline-block whitespace-nowrap rounded-lg px-8 py-4 font-semibold text-white shadow-lg transition-all hover:scale-105 hover:shadow-xl {content.ctaStyle ||
|
||||
'bg-theme-primary hover:bg-theme-primary-hover'}"
|
||||
>
|
||||
{content.ctaText}
|
||||
</a>
|
||||
|
||||
{#if !data.user}
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{freeText}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Trust Badges -->
|
||||
<div
|
||||
class="mt-12 flex flex-wrap justify-center gap-6 text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{#each trustBadges as badge}
|
||||
<span class="flex items-center gap-1">
|
||||
{badge.icon}
|
||||
{badge.text}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Loading state -->
|
||||
<div class="flex min-h-[400px] items-center justify-center">
|
||||
<div class="text-gray-500">Loading...</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/**
|
||||
* A/B Testing Variant Configurations
|
||||
* Defines content and styling for each variant using multilingual messages
|
||||
*/
|
||||
|
||||
import * as m from '$paraglide/messages';
|
||||
|
||||
export interface VariantContent {
|
||||
id: string;
|
||||
name: string;
|
||||
headline: string;
|
||||
subheadline: string;
|
||||
ctaText: string;
|
||||
ctaStyle?: string;
|
||||
features?: string[];
|
||||
socialProof?: {
|
||||
type: 'numbers' | 'logos' | 'testimonial';
|
||||
content: string;
|
||||
};
|
||||
layout?: 'standard' | 'split' | 'centered';
|
||||
}
|
||||
|
||||
// Get variant content with multilingual support
|
||||
export function getVariantContent(variantId: string): VariantContent {
|
||||
switch (variantId) {
|
||||
case 'control':
|
||||
return {
|
||||
id: 'control',
|
||||
name: 'Control (Baseline)',
|
||||
headline: m.hero_control_headline(),
|
||||
subheadline: m.hero_control_subheadline(),
|
||||
ctaText: m.hero_control_cta(),
|
||||
ctaStyle: 'bg-theme-primary hover:bg-theme-primary-hover',
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
// Variant A - Value Focused
|
||||
case 'a1':
|
||||
return {
|
||||
id: 'a1',
|
||||
name: 'Value Generic',
|
||||
headline: m.hero_a1_headline(),
|
||||
subheadline: m.hero_a1_subheadline(),
|
||||
ctaText: m.hero_a1_cta(),
|
||||
ctaStyle: 'bg-blue-600 hover:bg-blue-700',
|
||||
features: [m.hero_a1_feature_1(), m.hero_a1_feature_2(), m.hero_a1_feature_3()],
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
case 'a2':
|
||||
return {
|
||||
id: 'a2',
|
||||
name: 'Value Specific',
|
||||
headline: 'Save 3 Hours Per Week on Link Management',
|
||||
subheadline: 'Join teams who reduced their link management tasks by 75%',
|
||||
ctaText: 'Calculate Your Savings',
|
||||
ctaStyle:
|
||||
'bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700',
|
||||
features: ['3 hours saved weekly', '75% faster workflows', 'ROI in 2 weeks'],
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
case 'a3':
|
||||
return {
|
||||
id: 'a3',
|
||||
name: 'Value Transform',
|
||||
headline: 'Your Links, 10x More Powerful',
|
||||
subheadline: 'Transform every URL into a conversion machine with analytics and automation',
|
||||
ctaText: 'Unlock Link Power →',
|
||||
ctaStyle: 'bg-black hover:bg-gray-800',
|
||||
features: ['10x more clicks', 'Conversion tracking', 'Smart redirects'],
|
||||
layout: 'centered',
|
||||
};
|
||||
|
||||
// Variant B - Social Proof
|
||||
case 'b1':
|
||||
return {
|
||||
id: 'b1',
|
||||
name: 'Social Numbers',
|
||||
headline: m.hero_b1_headline(),
|
||||
subheadline: m.hero_b1_subheadline(),
|
||||
ctaText: m.hero_b1_cta(),
|
||||
ctaStyle: 'bg-purple-600 hover:bg-purple-700',
|
||||
socialProof: {
|
||||
type: 'numbers',
|
||||
content: m.hero_b1_social(),
|
||||
},
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
case 'b2':
|
||||
return {
|
||||
id: 'b2',
|
||||
name: 'Social Logos',
|
||||
headline: 'Trusted by Google, Meta, and Microsoft Teams',
|
||||
subheadline: 'Enterprise-grade URL management for companies of all sizes',
|
||||
ctaText: 'See Why They Chose Us',
|
||||
ctaStyle:
|
||||
'bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700',
|
||||
socialProof: {
|
||||
type: 'logos',
|
||||
content: 'Google • Meta • Microsoft • Spotify • Netflix',
|
||||
},
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
case 'b3':
|
||||
return {
|
||||
id: 'b3',
|
||||
name: 'Social Testimonial',
|
||||
headline: 'Rated #1 URL Shortener by Marketing Teams',
|
||||
subheadline: '"uLoad saved us 5 hours per week and increased our CTR by 40%"',
|
||||
ctaText: 'Read Success Stories',
|
||||
ctaStyle: 'bg-green-600 hover:bg-green-700',
|
||||
socialProof: {
|
||||
type: 'testimonial',
|
||||
content: '⭐⭐⭐⭐⭐ 4.9/5 from 1,000+ reviews',
|
||||
},
|
||||
layout: 'centered',
|
||||
};
|
||||
|
||||
// Variant C - Feature Focused
|
||||
case 'c1':
|
||||
return {
|
||||
id: 'c1',
|
||||
name: 'Features All-in-One',
|
||||
headline: m.hero_c1_headline(),
|
||||
subheadline: m.hero_c1_subheadline(),
|
||||
ctaText: m.hero_c1_cta(),
|
||||
ctaStyle: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
features: [
|
||||
m.hero_c1_feature_1(),
|
||||
m.hero_c1_feature_2(),
|
||||
m.hero_c1_feature_3(),
|
||||
m.hero_c1_feature_4(),
|
||||
m.hero_c1_feature_5(),
|
||||
m.hero_c1_feature_6(),
|
||||
],
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
case 'c2':
|
||||
return {
|
||||
id: 'c2',
|
||||
name: 'Features QR Focus',
|
||||
headline: 'QR Codes That Actually Convert',
|
||||
subheadline: 'Create dynamic QR codes with real-time analytics and custom branding',
|
||||
ctaText: 'Create Your First QR Code',
|
||||
ctaStyle: 'bg-orange-600 hover:bg-orange-700',
|
||||
features: ['Dynamic QR codes', 'Custom designs', 'Scan analytics', 'Bulk generation'],
|
||||
layout: 'split',
|
||||
};
|
||||
|
||||
case 'c3':
|
||||
return {
|
||||
id: 'c3',
|
||||
name: 'Features Integration',
|
||||
headline: 'Works With Your Favorite Tools',
|
||||
subheadline: 'Seamless integration with Zapier, Slack, WordPress & 100+ platforms',
|
||||
ctaText: 'Connect Your Tools',
|
||||
ctaStyle: 'bg-teal-600 hover:bg-teal-700',
|
||||
features: [
|
||||
'Zapier automation',
|
||||
'Slack notifications',
|
||||
'WordPress plugin',
|
||||
'API & Webhooks',
|
||||
],
|
||||
layout: 'standard',
|
||||
};
|
||||
|
||||
// Default to control
|
||||
default:
|
||||
return {
|
||||
id: 'control',
|
||||
name: 'Control (Baseline)',
|
||||
headline: m.hero_control_headline(),
|
||||
subheadline: m.hero_control_subheadline(),
|
||||
ctaText: m.hero_control_cta(),
|
||||
ctaStyle: 'bg-theme-primary hover:bg-theme-primary-hover',
|
||||
layout: 'standard',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get all active variant IDs
|
||||
export function getActiveVariantIds(): string[] {
|
||||
return ['control', 'a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3'];
|
||||
}
|
||||
|
||||
// Check if variant exists
|
||||
export function isValidVariant(variantId: string): boolean {
|
||||
return getActiveVariantIds().includes(variantId);
|
||||
}
|
||||
|
||||
// Get trust badges with translations
|
||||
export function getTrustBadges(): Array<{ icon: string; text: string }> {
|
||||
return [
|
||||
{ icon: '🔒', text: m.hero_trust_badge_1() },
|
||||
{ icon: '🇪🇺', text: m.hero_trust_badge_2() },
|
||||
{ icon: '⚡', text: m.hero_trust_badge_3() },
|
||||
{ icon: '🚀', text: m.hero_trust_badge_4() },
|
||||
];
|
||||
}
|
||||
|
||||
// Get free text
|
||||
export function getFreeText(): string {
|
||||
return m.hero_free_text();
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* Hash-based A/B Testing Manager
|
||||
* Manages variant assignment and persistence via URL hash
|
||||
*/
|
||||
export class HashManager {
|
||||
// Valid variants with versions
|
||||
private readonly validVariants = ['a1', 'a2', 'a3', 'b1', 'b2', 'b3', 'c1', 'c2', 'c3'];
|
||||
|
||||
// Current traffic distribution (percentages must sum to 100)
|
||||
private readonly distribution: Record<string, number> = {
|
||||
control: 40, // Baseline
|
||||
a1: 20, // Value-focused variant
|
||||
b1: 20, // Social proof variant
|
||||
c1: 20, // Feature-focused variant
|
||||
};
|
||||
|
||||
// Storage key for backup
|
||||
private readonly storageKey = 'uload_ab_variant';
|
||||
|
||||
// Debug mode flag
|
||||
private debugMode = false;
|
||||
|
||||
constructor() {
|
||||
// Check for debug mode
|
||||
if (typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
this.debugMode = params.get('debug') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current variant for the user
|
||||
* Priority: URL hash > localStorage > new assignment
|
||||
*/
|
||||
getVariant(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'control';
|
||||
}
|
||||
|
||||
// Check for forced variant (testing)
|
||||
const forced = this.getForcedVariant();
|
||||
if (forced !== null) {
|
||||
this.log(`Forced variant: ${forced}`);
|
||||
return forced;
|
||||
}
|
||||
|
||||
// Check existing hash
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash && this.isValidVariant(hash)) {
|
||||
this.log(`Using hash variant: ${hash}`);
|
||||
this.storeVariant(hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Check localStorage backup
|
||||
const stored = this.getStoredVariant();
|
||||
if (stored && this.isValidVariant(stored)) {
|
||||
this.log(`Using stored variant: ${stored}`);
|
||||
this.setHash(stored);
|
||||
return stored;
|
||||
}
|
||||
|
||||
// Assign new variant
|
||||
const newVariant = this.assignRandomVariant();
|
||||
this.log(`Assigned new variant: ${newVariant}`);
|
||||
this.setHash(newVariant);
|
||||
this.storeVariant(newVariant);
|
||||
return newVariant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a variant is valid
|
||||
*/
|
||||
private isValidVariant(variant: string): boolean {
|
||||
return variant === 'control' || this.validVariants.includes(variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a random variant based on distribution weights
|
||||
*/
|
||||
private assignRandomVariant(): string {
|
||||
const random = Math.random() * 100;
|
||||
let cumulative = 0;
|
||||
|
||||
for (const [variant, weight] of Object.entries(this.distribution)) {
|
||||
cumulative += weight;
|
||||
if (random <= cumulative) {
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to control
|
||||
return 'control';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the URL hash
|
||||
*/
|
||||
private setHash(variant: string): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Don't set hash for control to keep URL clean
|
||||
if (variant === 'control') {
|
||||
// Remove hash if it exists
|
||||
if (window.location.hash) {
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
}
|
||||
} else {
|
||||
window.location.hash = variant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store variant in localStorage
|
||||
*/
|
||||
private storeVariant(variant: string): void {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, variant);
|
||||
// Also store timestamp for analytics
|
||||
localStorage.setItem(`${this.storageKey}_timestamp`, new Date().toISOString());
|
||||
} catch (e) {
|
||||
console.warn('Could not store variant in localStorage:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored variant from localStorage
|
||||
*/
|
||||
private getStoredVariant(): string | null {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
return localStorage.getItem(this.storageKey);
|
||||
} catch (e) {
|
||||
console.warn('Could not read variant from localStorage:', e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get forced variant from URL params (for testing)
|
||||
*/
|
||||
private getForcedVariant(): string | null {
|
||||
if (typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const forced = params.get('force') || params.get('variant');
|
||||
|
||||
if (forced && this.isValidVariant(forced)) {
|
||||
return forced;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset variant assignment (for testing)
|
||||
*/
|
||||
reset(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
// Clear hash
|
||||
if (window.location.hash) {
|
||||
history.replaceState(null, '', window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
// Clear storage
|
||||
if (window.localStorage) {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
localStorage.removeItem(`${this.storageKey}_timestamp`);
|
||||
}
|
||||
|
||||
this.log('Variant assignment reset');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active variants (for debugging)
|
||||
*/
|
||||
getActiveVariants(): string[] {
|
||||
return ['control', ...Object.keys(this.distribution).filter((v) => v !== 'control')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current distribution (for debugging)
|
||||
*/
|
||||
getDistribution(): Record<string, number> {
|
||||
return { ...this.distribution };
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug messages
|
||||
*/
|
||||
private log(message: string): void {
|
||||
if (this.debugMode) {
|
||||
console.log(`[A/B Testing] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should show debug info
|
||||
*/
|
||||
isDebugMode(): boolean {
|
||||
return this.debugMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const hashManager = new HashManager();
|
||||
Loading…
Add table
Add a link
Reference in a new issue