Files
Momento/memento-note/components/settings/inline-paywall.tsx
Antigravity 556a0b2f3f feat(credits): solde IA unique (option M), packs Stripe et polish billing
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.
2026-07-16 20:43:18 +00:00

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>
)
}