209 lines
7.8 KiB
TypeScript
209 lines
7.8 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo, useState } from 'react'
|
|
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
|
|
import { createReferralLink } from '../hooks/generateReferralLink'
|
|
import { useToast } from '../../components/toast/toastComponent'
|
|
import { useTranslation } from '../../i18n/useTranslation'
|
|
|
|
interface Props {
|
|
onCreated?: () => void | Promise<void>
|
|
}
|
|
|
|
export default function GenerateReferralLinkWidget({ onCreated }: Props) {
|
|
const { t } = useTranslation()
|
|
const { showToast } = useToast()
|
|
// Defaults: Unlimited + Never expires
|
|
const [maxUses, setMaxUses] = useState<string>('-1')
|
|
const [expiresInDays, setExpiresInDays] = useState<string>('-1')
|
|
// Track which select is locking the other: 'max' | 'exp' | null
|
|
const [lockedBy, setLockedBy] = useState<'max' | 'exp' | null>('max')
|
|
|
|
const [generatedLink, setGeneratedLink] = useState<string>('')
|
|
const [isCopying, setIsCopying] = useState(false)
|
|
const [isGenerating, setIsGenerating] = useState(false)
|
|
|
|
const expiryOptions = useMemo(
|
|
() => [
|
|
{ value: '1', label: t('referralManagement.expiry1Day') },
|
|
{ value: '2', label: t('referralManagement.expiry2Days') },
|
|
{ value: '3', label: t('referralManagement.expiry3Days') },
|
|
{ value: '4', label: t('referralManagement.expiry4Days') },
|
|
{ value: '5', label: t('referralManagement.expiry5Days') },
|
|
{ value: '6', label: t('referralManagement.expiry6Days') },
|
|
{ value: '7', label: t('referralManagement.expiry7Days') },
|
|
{ value: '-1', label: t('referralManagement.expiryNever') },
|
|
],
|
|
[t]
|
|
)
|
|
|
|
const maxUsesOptions = useMemo(
|
|
() => [
|
|
{ value: '1', label: t('referralManagement.maxUses1') },
|
|
{ value: '5', label: t('referralManagement.maxUses5') },
|
|
{ value: '10', label: t('referralManagement.maxUses10') },
|
|
{ value: '50', label: t('referralManagement.maxUses50') },
|
|
{ value: '-1', label: t('referralManagement.maxUsesUnlimited') },
|
|
],
|
|
[t]
|
|
)
|
|
|
|
// Handlers that enforce coupling
|
|
const onChangeMaxUses = (val: string) => {
|
|
setMaxUses(val)
|
|
if (val === '-1') {
|
|
// Unlimited -> force never expires, lock expires
|
|
setExpiresInDays('-1')
|
|
setLockedBy('max')
|
|
} else {
|
|
// Unlock if this was the locker
|
|
if (lockedBy === 'max') setLockedBy(null)
|
|
}
|
|
}
|
|
|
|
const onChangeExpires = (val: string) => {
|
|
setExpiresInDays(val)
|
|
if (val === '-1') {
|
|
// Never expires -> force unlimited, lock max uses
|
|
setMaxUses('-1')
|
|
setLockedBy('exp')
|
|
} else {
|
|
// Unlock if this was the locker
|
|
if (lockedBy === 'exp') setLockedBy(null)
|
|
}
|
|
}
|
|
|
|
const onGenerate = async () => {
|
|
setIsGenerating(true)
|
|
setGeneratedLink('')
|
|
|
|
try {
|
|
const payload = {
|
|
expiresInDays: parseInt(expiresInDays, 10),
|
|
maxUses: parseInt(maxUses, 10),
|
|
}
|
|
|
|
const res = await createReferralLink(payload)
|
|
console.log('✅ Referral create result:', res)
|
|
|
|
const body: any = res.body
|
|
const url =
|
|
body?.data?.url ||
|
|
body?.data?.link ||
|
|
body?.url ||
|
|
body?.link ||
|
|
(body?.data?.code ? `${window.location.origin}/signup?ref=${body.data.code}` : '') ||
|
|
(body?.code ? `${window.location.origin}/signup?ref=${body.code}` : '')
|
|
|
|
if (url) setGeneratedLink(url)
|
|
|
|
if (res.ok) {
|
|
showToast({
|
|
variant: 'success',
|
|
title: t('referralManagement.createLink'),
|
|
message: t('referralManagement.createSuccess'),
|
|
})
|
|
} else {
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('referralManagement.createLink'),
|
|
message: body?.message || body?.error || t('referralManagement.createError'),
|
|
})
|
|
}
|
|
|
|
if (res.ok && onCreated) await onCreated()
|
|
} catch {
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('referralManagement.createLink'),
|
|
message: t('referralManagement.createError'),
|
|
})
|
|
} finally {
|
|
setIsGenerating(false)
|
|
}
|
|
}
|
|
|
|
const onCopy = async () => {
|
|
if (!generatedLink) return
|
|
try {
|
|
setIsCopying(true)
|
|
await navigator.clipboard.writeText(generatedLink)
|
|
showToast({
|
|
variant: 'success',
|
|
title: t('referralManagement.copyLink'),
|
|
message: t('referralManagement.copiedMessage'),
|
|
})
|
|
setTimeout(() => setIsCopying(false), 800)
|
|
} catch {
|
|
showToast({
|
|
variant: 'error',
|
|
title: t('referralManagement.copyFailed'),
|
|
message: t('referralManagement.copyFailedMessage'),
|
|
})
|
|
setIsCopying(false)
|
|
}
|
|
}
|
|
|
|
const disableExpires = lockedBy === 'max'
|
|
const disableMaxUses = lockedBy === 'exp'
|
|
|
|
return (
|
|
<div className="rounded-[28px] border border-white/80 bg-white/85 p-6 shadow-[0_22px_60px_-34px_rgba(15,23,42,0.28)] backdrop-blur">
|
|
<h2 className="text-xl font-bold text-slate-950 mb-4 break-words">{t('referralManagement.generateTitle')}</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1 break-words">{t('referralManagement.maxUsesLabel')}</label>
|
|
<select
|
|
value={maxUses}
|
|
onChange={(e) => onChangeMaxUses(e.target.value)}
|
|
disabled={disableMaxUses}
|
|
className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-900/20 disabled:bg-slate-100 disabled:text-slate-500"
|
|
>
|
|
{maxUsesOptions.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
{disableMaxUses && <p className="mt-1 text-xs text-slate-500 break-words">{t('referralManagement.lockedByNeverExpires')}</p>}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1 break-words">{t('referralManagement.expiresIn')}</label>
|
|
<select
|
|
value={expiresInDays}
|
|
onChange={(e) => onChangeExpires(e.target.value)}
|
|
disabled={disableExpires}
|
|
className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm transition focus:border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-900/20 disabled:bg-slate-100 disabled:text-slate-500"
|
|
>
|
|
{expiryOptions.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
{disableExpires && <p className="mt-1 text-xs text-slate-500 break-words">{t('referralManagement.lockedByUnlimited')}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex flex-wrap items-start gap-3">
|
|
<button
|
|
onClick={onGenerate}
|
|
disabled={isGenerating}
|
|
className="inline-flex items-center gap-2 rounded-2xl bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-[0_18px_40px_-24px_rgba(141,107,29,0.85)] transition hover:bg-[#7A5E1A] disabled:opacity-60"
|
|
>
|
|
{isGenerating ? t('referralManagement.generating') : t('referralManagement.generateLink')}
|
|
</button>
|
|
{generatedLink && (
|
|
<div className="flex flex-1 min-w-[16rem] flex-wrap items-center gap-2">
|
|
<code className="max-w-full rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 break-all">{generatedLink}</code>
|
|
<button
|
|
onClick={onCopy}
|
|
className="inline-flex items-center gap-1 rounded-2xl border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800 transition hover:bg-slate-50"
|
|
>
|
|
<ClipboardDocumentIcon className="h-4 w-4" />
|
|
{isCopying ? t('referralManagement.copied') : t('referralManagement.copy')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|