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:
Till-JS 2025-11-29 07:03:59 +01:00
parent b97149ac12
commit 61d181fbc2
3148 changed files with 437 additions and 46640 deletions

View file

@ -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>

View file

@ -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();
}

View file

@ -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();