mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 01:26:42 +02:00
feat(referral): integrate referral system frontend
- Add referral code input to RegisterPage with live validation - Create ReferralWidget for dashboard with stats, code sharing, and tier display - Extend authService.signUp to accept optional referralCode parameter - Add validateReferralCode function to authStore - Create referrals.ts API service for frontend - Add 'referral' widget type to dashboard configuration - Fix drizzle.config.ts to include 'referrals' schema filter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
37039048f4
commit
b148a72e36
9 changed files with 492 additions and 10 deletions
|
|
@ -17,6 +17,12 @@
|
|||
backToLogin: string;
|
||||
showPassword: string;
|
||||
hidePassword: string;
|
||||
// Referral
|
||||
referralCodePlaceholder: string;
|
||||
referralCodeValid: string;
|
||||
referralCodeInvalid: string;
|
||||
referralCodeValidating: string;
|
||||
referralBonusCredits: string;
|
||||
// Error messages
|
||||
emailRequired: string;
|
||||
passwordRequired: string;
|
||||
|
|
@ -29,6 +35,14 @@
|
|||
accountCreated: string;
|
||||
}
|
||||
|
||||
/** Referral code validation result */
|
||||
interface ReferralValidation {
|
||||
valid: boolean;
|
||||
referrerName?: string;
|
||||
bonusCredits?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Default English translations */
|
||||
const defaultTranslations: RegisterTranslations = {
|
||||
title: 'Create Account',
|
||||
|
|
@ -42,6 +56,13 @@
|
|||
backToLogin: 'Back to Login',
|
||||
showPassword: 'Show password',
|
||||
hidePassword: 'Hide password',
|
||||
// Referral
|
||||
referralCodePlaceholder: 'Referral Code (optional)',
|
||||
referralCodeValid: 'Valid code!',
|
||||
referralCodeInvalid: 'Invalid code',
|
||||
referralCodeValidating: 'Checking...',
|
||||
referralBonusCredits: 'bonus credits',
|
||||
// Error messages
|
||||
emailRequired: 'Email is required',
|
||||
passwordRequired: 'Password is required',
|
||||
confirmPasswordRequired: 'Please confirm your password',
|
||||
|
|
@ -60,8 +81,8 @@
|
|||
logo: Component<{ size?: number; color?: string }>;
|
||||
/** Primary color (hex) */
|
||||
primaryColor: string;
|
||||
/** Sign up function */
|
||||
onSignUp: (email: string, password: string) => Promise<AuthResult>;
|
||||
/** Sign up function (with optional referral code) */
|
||||
onSignUp: (email: string, password: string, referralCode?: string) => Promise<AuthResult>;
|
||||
/** Navigation function */
|
||||
goto: (path: string) => void;
|
||||
/** Success redirect path */
|
||||
|
|
@ -78,6 +99,12 @@
|
|||
headerControls?: Snippet;
|
||||
/** Translations for i18n support */
|
||||
translations?: Partial<RegisterTranslations>;
|
||||
/** Referral code validation function */
|
||||
onValidateReferralCode?: (code: string) => Promise<ReferralValidation>;
|
||||
/** Initial referral code (e.g., from URL) */
|
||||
initialReferralCode?: string;
|
||||
/** Base signup credits (shown to user) */
|
||||
baseSignupCredits?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -93,6 +120,9 @@
|
|||
appSlider,
|
||||
headerControls,
|
||||
translations = {},
|
||||
onValidateReferralCode,
|
||||
initialReferralCode = '',
|
||||
baseSignupCredits = 25,
|
||||
}: Props = $props();
|
||||
|
||||
// Merge provided translations with defaults
|
||||
|
|
@ -108,6 +138,17 @@
|
|||
let showPassword = $state(false);
|
||||
let showConfirmPassword = $state(false);
|
||||
|
||||
// Referral state
|
||||
let referralCode = $state(initialReferralCode);
|
||||
let referralValidation = $state<ReferralValidation | null>(null);
|
||||
let validatingReferral = $state(false);
|
||||
let referralDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Calculate total credits (base + bonus)
|
||||
let totalCredits = $derived(
|
||||
baseSignupCredits + (referralValidation?.valid ? referralValidation.bonusCredits || 0 : 0)
|
||||
);
|
||||
|
||||
// Theme state - can be toggled manually, defaults to system preference
|
||||
let userThemePreference = $state<'light' | 'dark' | null>(null);
|
||||
let systemIsDark = $state(
|
||||
|
|
@ -139,6 +180,44 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Referral code validation
|
||||
async function validateReferralCode(code: string) {
|
||||
if (!code || code.length < 3 || !onValidateReferralCode) {
|
||||
referralValidation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
validatingReferral = true;
|
||||
try {
|
||||
const result = await onValidateReferralCode(code);
|
||||
referralValidation = result;
|
||||
} catch {
|
||||
referralValidation = { valid: false, error: 'Validation failed' };
|
||||
} finally {
|
||||
validatingReferral = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleReferralCodeChange(value: string) {
|
||||
referralCode = value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||
|
||||
// Clear previous timer
|
||||
if (referralDebounceTimer) {
|
||||
clearTimeout(referralDebounceTimer);
|
||||
}
|
||||
|
||||
// Reset validation if empty
|
||||
if (!referralCode) {
|
||||
referralValidation = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce validation
|
||||
referralDebounceTimer = setTimeout(() => {
|
||||
validateReferralCode(referralCode);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Password validation
|
||||
let passwordRequirements = $derived.by(() => {
|
||||
if (!password) {
|
||||
|
|
@ -212,7 +291,9 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const result = await onSignUp(email, password);
|
||||
// Pass referral code if valid
|
||||
const validReferralCode = referralValidation?.valid ? referralCode : undefined;
|
||||
const result = await onSignUp(email, password, validReferralCode);
|
||||
|
||||
loading = false;
|
||||
|
||||
|
|
@ -415,6 +496,58 @@
|
|||
{t.passwordRequirements}
|
||||
</p>
|
||||
|
||||
<!-- Referral Code Input -->
|
||||
{#if onValidateReferralCode}
|
||||
<div class="mb-4">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={referralCode}
|
||||
oninput={(e) => handleReferralCodeChange((e.target as HTMLInputElement).value)}
|
||||
placeholder={t.referralCodePlaceholder}
|
||||
maxlength={12}
|
||||
class="h-14 w-full rounded-xl border px-4 pr-24 text-lg transition-colors focus:outline-none focus:ring-2 uppercase tracking-wider"
|
||||
style="background-color: {isDark
|
||||
? 'rgba(0, 0, 0, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.8)'}; border-color: {referralValidation?.valid
|
||||
? '#22c55e'
|
||||
: referralValidation && !referralValidation.valid
|
||||
? '#ef4444'
|
||||
: isDark
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
|
||||
? '#ffffff'
|
||||
: '#000000'}; --tw-ring-color: {primaryColor};"
|
||||
/>
|
||||
<!-- Validation indicator -->
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-4">
|
||||
{#if validatingReferral}
|
||||
<span
|
||||
class="text-sm"
|
||||
style="color: {isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)'};"
|
||||
>
|
||||
{t.referralCodeValidating}
|
||||
</span>
|
||||
{:else if referralValidation?.valid}
|
||||
<span class="text-sm text-green-500">✓ {t.referralCodeValid}</span>
|
||||
{:else if referralValidation && !referralValidation.valid}
|
||||
<span class="text-sm text-red-500">✗ {t.referralCodeInvalid}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bonus info -->
|
||||
{#if referralValidation?.valid && referralValidation.bonusCredits}
|
||||
<p class="mt-2 text-sm text-green-500">
|
||||
+{referralValidation.bonusCredits}
|
||||
{t.referralBonusCredits}
|
||||
{#if referralValidation.referrerName}
|
||||
(von {referralValidation.referrerName})
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue