Files
Momento/memento-note/components/settings/inline-paywall.tsx
Antigravity 96e7902f01
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped
feat: publication IA (magazine/brief/essay) + fixes critique
Publication IA:
- 4 templates (magazine, brief, essay, simple) avec CSS riche
- Rewrite IA (article/exercises/tutorial/reference/mixed)
- Modération avec timeout 12s + fallback safe
- Quotas publish_enhance par tier (basic=2, pro=15, business=100)
- Détection contenu stale (hash)
- Migration DB publishedContent/publishedTemplate/publishedSourceHash

Fixes:
- cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast
- _isShared ajouté au type Note (champ virtuel serveur)
- callout colors PDF export: extraction fonction pure testable
- admin/published: guard note.userId null
- Cmd+S fonctionne en mode dialog (pas seulement fullPage)

i18n:
- 23 clés publish* traduites dans les 15 locales
- Extension Web Clipper: 13 locales mise à jour

Tests:
- callout-colors.test.ts (6 tests)
- note-visible-in-view.test.ts (5 tests)
- entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs
- 199/199 tests passent

Tracker: user-stories.md sync avec sprint-status.yaml
2026-06-28 07:32:57 +00:00

113 lines
4.5 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 {
feature: string;
onDismiss: () => void;
}
export function InlinePaywall({ feature, onDismiss }: InlinePaywallProps) {
const { t } = useLanguage();
const router = useRouter();
const [timeLeft, setTimeLeft] = useState(10);
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') || 'Recherche Sémantique',
auto_tag: t('usageMeter.featureTags') || 'Tags Automatiques',
auto_title: t('usageMeter.featureTitles') || 'Titres Automatiques',
reformulate: t('usageMeter.featureReformulate') || 'Reformulation',
chat: t('usageMeter.featureChat') || 'Chat IA',
brainstorm_create: t('usageMeter.featureBrainstormCreate') || 'Création de Brainstorm',
brainstorm_expand: t('usageMeter.featureBrainstormExpand') || 'Expansion de Brainstorm',
brainstorm_enrich: t('usageMeter.featureBrainstormEnrich') || 'Enrichissement de Brainstorm',
publish_enhance: t('usageMeter.featurePublishEnhance') || 'Publication IA',
};
const currentFeatureLabel = featureLabels[feature] || feature;
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') || 'Limite mensuelle atteinte'}
</h4>
<p className="text-xs text-rose-700/80 dark:text-rose-400/70 font-light leading-relaxed">
{(t('quotaPaywall.description') || "Vous avez épuisé vos crédits pour la fonctionnalité {feature} ce mois-ci.").replace('{feature}', currentFeatureLabel)}
</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') || 'Peut-être plus tard'}
>
<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') || 'Passer au plan Pro'}
<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') || 'Utiliser ma propre clé'}
</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>Fermeture dans {timeLeft}s</span>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}