Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
142 lines
5.0 KiB
TypeScript
142 lines
5.0 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
import { motion, AnimatePresence } from 'motion/react'
|
|
import { AlertTriangle, Sparkles, Key, X, ArrowRight } from 'lucide-react'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
|
|
interface InlinePaywallProps {
|
|
/** Action en cours (pour contexte uniquement — le solde est global) */
|
|
feature: string
|
|
onDismiss: () => void
|
|
/** Coût de l'action si connu */
|
|
creditCost?: number
|
|
/** Solde restant si connu */
|
|
creditsRemaining?: number
|
|
}
|
|
|
|
export function InlinePaywall({
|
|
feature,
|
|
onDismiss,
|
|
creditCost,
|
|
creditsRemaining,
|
|
}: InlinePaywallProps) {
|
|
const { t } = useLanguage()
|
|
const router = useRouter()
|
|
const [timeLeft, setTimeLeft] = useState(12)
|
|
|
|
useEffect(() => {
|
|
if (timeLeft <= 0) {
|
|
onDismiss()
|
|
return
|
|
}
|
|
const timer = setTimeout(() => {
|
|
setTimeLeft((prev) => prev - 1)
|
|
}, 1000)
|
|
return () => clearTimeout(timer)
|
|
}, [timeLeft, onDismiss])
|
|
|
|
const handleUpgrade = () => {
|
|
router.push('/settings/billing')
|
|
onDismiss()
|
|
}
|
|
|
|
const handleByok = () => {
|
|
router.push('/settings/ai#byok')
|
|
onDismiss()
|
|
}
|
|
|
|
const featureLabels: Record<string, string> = {
|
|
semantic_search: t('usageMeter.featureSearch'),
|
|
auto_tag: t('usageMeter.featureTags'),
|
|
auto_title: t('usageMeter.featureTitles'),
|
|
reformulate: t('usageMeter.featureReformulate'),
|
|
chat: t('usageMeter.featureChat'),
|
|
brainstorm_create: t('usageMeter.featureBrainstormSessions'),
|
|
brainstorm_expand: t('usageMeter.featureBrainstormExpand'),
|
|
brainstorm_enrich: t('usageMeter.featureBrainstormEnrich'),
|
|
publish_enhance: t('usageMeter.featurePublishEnhance'),
|
|
slide_generate: t('usageMeter.featureSlides'),
|
|
excalidraw_generate: t('usageMeter.featureDiagrams'),
|
|
ai_flashcard: t('usageMeter.featureFlashcards'),
|
|
voice_transcribe: t('usageMeter.featureVoice'),
|
|
credits: t('billing.aiCredits'),
|
|
}
|
|
|
|
const currentFeatureLabel = featureLabels[feature] || feature
|
|
|
|
let description = t('quotaPaywall.description').replace('{feature}', currentFeatureLabel)
|
|
if (
|
|
typeof creditCost === 'number' &&
|
|
typeof creditsRemaining === 'number' &&
|
|
Number.isFinite(creditCost) &&
|
|
Number.isFinite(creditsRemaining)
|
|
) {
|
|
description = t('quotaPaywall.descriptionWithCost', {
|
|
remaining: String(creditsRemaining),
|
|
cost: String(creditCost),
|
|
feature: currentFeatureLabel,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: -10 }}
|
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
|
className="rounded-2xl border border-rose-200 dark:border-rose-900/40 bg-rose-50/60 dark:bg-rose-950/15 backdrop-blur-md p-6 space-y-4 shadow-lg"
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div className="p-2.5 bg-rose-500/10 text-rose-500 rounded-xl shrink-0">
|
|
<AlertTriangle size={20} />
|
|
</div>
|
|
<div className="flex-1 space-y-1">
|
|
<h4 className="text-sm font-bold text-rose-800 dark:text-rose-400">
|
|
{t('quotaPaywall.title')}
|
|
</h4>
|
|
<p className="text-xs text-rose-700/80 dark:text-rose-400/70 font-light leading-relaxed">
|
|
{description}
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onDismiss}
|
|
className="text-rose-500/60 hover:text-rose-500 dark:text-rose-400/50 dark:hover:text-rose-400 p-1 hover:bg-rose-500/5 dark:hover:bg-rose-400/5 rounded-lg transition-all"
|
|
title={t('quotaPaywall.later')}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3 pt-2 sm:pt-0">
|
|
<button
|
|
type="button"
|
|
onClick={handleUpgrade}
|
|
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 bg-[#D4A373] text-white hover:bg-[#C49363] text-xs font-semibold rounded-xl transition-all shadow-sm shadow-[#D4A373]/15"
|
|
>
|
|
<Sparkles size={14} />
|
|
{t('quotaPaywall.upgrade')}
|
|
<ArrowRight size={12} className="ml-1" />
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleByok}
|
|
className="inline-flex items-center justify-center gap-2 px-4 py-2.5 border border-rose-300 dark:border-rose-800/40 text-rose-800 dark:text-rose-400 hover:bg-rose-100/30 dark:hover:bg-rose-400/5 text-xs font-semibold rounded-xl transition-all"
|
|
>
|
|
<Key size={14} />
|
|
{t('quotaPaywall.useOwnKey')}
|
|
</button>
|
|
|
|
<div className="sm:ml-auto flex items-center justify-center gap-1.5 text-[10px] text-rose-500/60 dark:text-rose-400/40 font-mono">
|
|
<span>{t('quotaPaywall.closingIn').replace('{n}', String(timeLeft))}</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
)
|
|
}
|