Files
Momento/memento-note/components/settings/billing-plans.tsx

600 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { loadStripe } from '@stripe/stripe-js';
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
import { Check, Shield, Zap, Crown, X, ExternalLink, Loader2, CheckCircle2, Activity, Clock, ArrowRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useLanguage } from '@/lib/i18n';
import { toast } from 'sonner';
import { format } from 'date-fns';
import { motion } from 'motion/react';
import { BillingHistory } from './billing-history';
type Tier = 'PRO' | 'BUSINESS';
type Interval = 'month' | 'year';
interface BillingStatus {
tier: string;
effectiveTier: string;
status: string;
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
cancelAtPeriodEnd: boolean;
hasStripeSubscription: boolean;
}
const billingEnabled = process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true';
let stripePromise: ReturnType<typeof loadStripe> | null = null;
function getStripePromise() {
if (!billingEnabled) return null;
if (!stripePromise && process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
}
return stripePromise;
}
export function BillingPlans() {
const { t } = useLanguage();
const queryClient = useQueryClient();
const [interval, setInterval] = useState<Interval>('month');
const [checkoutClientSecret, setCheckoutClientSecret] = useState<string | null>(null);
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
const [checkoutLoading, setCheckoutLoading] = useState<Tier | null>(null);
const [portalLoading, setPortalLoading] = useState(false);
const [successBanner, setSuccessBanner] = useState<string | null>(null);
const { data: status, isLoading } = useQuery<BillingStatus>({
queryKey: ['billing', 'status'],
queryFn: async () => {
const res = await fetch('/api/billing/status');
if (!res.ok) throw new Error('Failed to fetch billing status');
return res.json();
},
});
const { data: usageData } = useQuery({
queryKey: ['usage', 'current'],
queryFn: async () => {
const res = await fetch('/api/usage/current');
if (!res.ok) throw new Error('Failed to fetch quotas');
return res.json();
},
refetchInterval: 30000,
});
const quotas = usageData?.quotas;
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const sessionId = params.get('session_id');
if (sessionId) {
const tier = status?.effectiveTier ?? 'Pro';
setSuccessBanner(t('billing.checkoutSuccessBody').replace('{tier}', tier));
queryClient.invalidateQueries({ queryKey: ['usage', 'current'] });
queryClient.invalidateQueries({ queryKey: ['billing', 'status'] });
window.history.replaceState({}, '', '/settings/billing');
}
}, [status, t, queryClient]);
useEffect(() => {
if (successBanner) {
const timer = setTimeout(() => {
setSuccessBanner(null);
}, 5000);
return () => clearTimeout(timer);
}
}, [successBanner]);
const handleCheckout = async (tier: Tier) => {
if (!billingEnabled) return;
setCheckoutLoading(tier);
try {
const res = await fetch('/api/billing/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tier, interval }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? 'Failed to create checkout');
if (data.clientSecret) {
setCheckoutClientSecret(data.clientSecret);
setIsCheckoutOpen(true);
} else if (data.url) {
window.location.href = data.url;
}
} catch (err) {
console.error('[BillingPlans] checkout error:', err);
toast.error('Failed to start checkout. Please try again.');
} finally {
setCheckoutLoading(null);
}
};
const handlePortal = async () => {
setPortalLoading(true);
try {
const res = await fetch('/api/billing/portal', { method: 'POST' });
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? 'Failed to open portal');
window.location.href = data.url;
} catch (err) {
console.error('[BillingPlans] portal error:', err);
toast.error('Failed to open billing portal.');
} finally {
setPortalLoading(false);
}
};
const handleCheckoutComplete = useCallback(() => {
setIsCheckoutOpen(false);
setCheckoutClientSecret(null);
const tier = status?.effectiveTier ?? 'Pro';
setSuccessBanner(t('billing.checkoutSuccessBody').replace('{tier}', tier));
queryClient.invalidateQueries({ queryKey: ['usage', 'current'] });
queryClient.invalidateQueries({ queryKey: ['billing', 'status'] });
}, [status, t, queryClient]);
if (isLoading) {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
);
}
const effectiveTier = status?.effectiveTier ?? 'BASIC';
const isPaid = effectiveTier !== 'BASIC';
const aiUsed = quotas?.semantic_search?.used ?? 0;
const aiLimit = quotas?.semantic_search?.limit ?? 50;
const aiPct = aiLimit > 0 ? (aiUsed / aiLimit) * 100 : 0;
const plans = [
{
id: 'free',
name: t('billing.freePlan'),
price: t('billing.freePrice') || 'Gratuit',
period: '',
description: t('billing.freeDescription') || 'Pour découvrir la magie de Momento.',
features: [
t('billing.freeF1') || '100 Notes max',
t('billing.freeF2') || '3 Carnets',
t('billing.freeF3') || '50 crédits IA (Lifetime)',
t('billing.freeF4') || 'Recherche sémantique',
t('billing.freeF5') || 'Historique 7 jours',
],
current: effectiveTier === 'BASIC',
buttonText: effectiveTier === 'BASIC' ? (t('billing.currentPlan') || 'Plan Actuel') : t('billing.startCheckout'),
buttonClass: effectiveTier === 'BASIC'
? 'bg-paper text-concrete cursor-default'
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
onClick: () => {},
},
{
id: 'pro',
name: t('billing.proPlan'),
price: interval === 'month' ? (t('billing.proPrice') || '9,90€') : (t('billing.proAnnualPrice') || '99€'),
period: interval === 'month' ? '/mois' : '/an',
description: t('billing.proDescription') || 'Pour les consultants et créateurs exigeants.',
features: [
t('billing.proFeature1') || 'Notes illimitées',
t('billing.proFeature2') || 'BYOK (OpenAI/Anthropic)',
t('billing.proFeature3') || '200 recherches sémantiques',
t('billing.proFeature4') || 'Agents (12 runs/mois)',
t('billing.proFeature5') || 'Historique 30 jours',
t('billing.proFeature6') || 'Support Email',
],
current: effectiveTier === 'PRO',
popular: true,
buttonText: effectiveTier === 'PRO' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.proCta') || 'Passer au Plan Pro'),
buttonClass: effectiveTier === 'PRO'
? 'bg-paper text-concrete cursor-default'
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
onClick: () => handleCheckout('PRO'),
},
{
id: 'business',
name: t('billing.businessPlan'),
price: interval === 'month' ? (t('billing.businessPrice') || '29,90€') : (t('billing.businessAnnualPrice') || '299€'),
period: interval === 'month' ? '/mois' : '/an',
description: t('billing.businessDescription') || 'Pour les équipes et chefs de produit.',
features: [
t('billing.businessFeature1') || '10 Collaborateurs inclus',
t('billing.businessFeature2') || 'BYOK (13 fournisseurs)',
t('billing.businessFeature3') || '1000 recherches sémantiques',
t('billing.businessFeature4') || 'Agents (60 runs/mois)',
t('billing.businessFeature5') || 'Brainstorm illimité',
t('billing.businessFeature6') || 'Accès API',
],
current: effectiveTier === 'BUSINESS',
buttonText: effectiveTier === 'BUSINESS' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.businessCta') || 'Choisir Plan Business'),
buttonClass: effectiveTier === 'BUSINESS'
? 'bg-paper text-concrete cursor-default'
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
onClick: () => handleCheckout('BUSINESS'),
},
{
id: 'enterprise',
name: t('billing.enterpriseTitle') || 'Enterprise',
price: t('billing.contactSales') || 'Sur devis',
period: '',
description: t('billing.enterpriseDescription') || 'Quotas personnalisés, SSO, support prioritaire.',
features: [
t('billing.enterpriseFeature1') || 'Quotas illimités',
t('billing.enterpriseFeature2') || 'SSO / SAML',
t('billing.enterpriseFeature3') || 'Support dédié',
t('billing.enterpriseFeature4') || 'Facturation personnalisée',
t('billing.enterpriseFeature5') || 'SLA garanti',
],
current: effectiveTier === 'ENTERPRISE',
buttonText: effectiveTier === 'ENTERPRISE' ? (t('billing.currentPlan') || 'Plan Actuel') : (t('billing.contactSales') || 'Contact Sales'),
buttonClass: effectiveTier === 'ENTERPRISE'
? 'bg-paper text-concrete cursor-default'
: 'bg-ink text-white shadow-xl shadow-ink/20 hover:scale-[1.02] active:scale-95',
onClick: () => { window.location.href = 'mailto:sales@memento-note.com'; },
},
];
const formatDate = (dateStr: string | null | undefined) => {
if (!dateStr) return '—';
try {
const date = new Date(dateStr);
const locale = typeof window !== 'undefined' ? window.navigator.language : 'fr-FR';
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(date);
} catch (e) {
return dateStr;
}
};
// Sommes pour l'usage global agrégé (donut)
let totalUsed = 0;
let totalLimit = 0;
if (quotas) {
Object.entries(quotas).forEach(([_, q]) => {
if (q.limit > 0 && q.limit !== Infinity) {
totalUsed += q.used;
totalLimit += q.limit;
}
});
}
const globalPct = totalLimit > 0 ? (totalUsed / totalLimit) * 100 : 0;
// SVG Donut Config
const radius = 28;
const strokeWidth = 5;
const circumference = 2 * Math.PI * radius;
const strokeDashoffset = circumference - (Math.min(globalPct, 100) / 100) * circumference;
const donutColor = globalPct >= 100 ? '#f43f5e' : globalPct >= 75 ? '#f59e0b' : '#a78bfa'; // Rose, Ambre, Violet
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="space-y-12"
>
{/* Success Banner */}
{successBanner && (
<div className="flex items-start gap-3 rounded-xl border border-emerald-500/30 bg-emerald-500/10 p-4">
<CheckCircle2 className="h-5 w-5 text-emerald-600 dark:text-emerald-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-foreground">{t('billing.checkoutSuccessTitle') || 'Abonnement activé !'}</p>
<p className="text-xs text-muted-foreground mt-0.5">{successBanner}</p>
</div>
<button type="button" onClick={() => setSuccessBanner(null)} className="text-muted-foreground hover:text-foreground transition-colors">
<X className="h-4 w-4" />
</button>
</div>
)}
{/* Current Plan Card & Usage Ring */}
<div className="bg-white/40 dark:bg-white/5 border border-border rounded-3xl p-8 grid grid-cols-1 md:grid-cols-3 gap-8 items-center">
<div className="md:col-span-2 space-y-6">
<div className="flex items-center gap-4">
<div className="p-2.5 bg-brand-accent/10 text-brand-accent rounded-2xl">
<Crown size={24} className="text-brand-accent" />
</div>
<div>
<p className="text-[10px] text-concrete uppercase tracking-widest">{t('billing.currentPlan')}</p>
<h4 className="text-xl font-bold font-serif text-ink mt-0.5">
{effectiveTier === 'BASIC' ? t('billing.freePlan') : effectiveTier === 'PRO' ? t('billing.proPlan') : effectiveTier === 'BUSINESS' ? t('billing.businessPlan') : t('billing.enterprisePlan')}
</h4>
</div>
<div className="ml-auto">
<span className={cn(
'px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest',
status?.status === 'active' || status?.status === 'ACTIVE'
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20'
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/20'
)}>
{status?.status ? t(`billing.${status.status.toLowerCase()}`) || status.status : t('billing.active')}
</span>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4 border-t border-border/40">
<div className="space-y-1">
<span className="text-[10px] text-concrete uppercase tracking-wider">{t('billing.billingPeriod')}</span>
<p className="text-xs font-semibold text-ink">
{status?.currentPeriodStart && status?.currentPeriodEnd ? (
`${formatDate(status.currentPeriodStart)} ${formatDate(status.currentPeriodEnd)}`
) : (
'—'
)}
</p>
</div>
<div className="space-y-1">
<span className="text-[10px] text-concrete uppercase tracking-wider">
{status?.cancelAtPeriodEnd ? t('billing.expiresOn') : t('billing.nextBillingDate')}
</span>
<p className="text-xs font-semibold text-ink">
{status?.currentPeriodEnd ? formatDate(status.currentPeriodEnd) : '—'}
</p>
</div>
</div>
{isPaid && (
<button
type="button"
onClick={handlePortal}
disabled={portalLoading}
className="flex items-center gap-2 px-5 py-2.5 bg-ink text-white dark:bg-white dark:text-black rounded-xl text-xs font-semibold hover:opacity-90 disabled:opacity-60 transition-all shadow-md shadow-black/5"
>
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
{t('billing.manageBilling') || 'Gérer la facturation'}
</button>
)}
</div>
{/* Global Usage Donut */}
<div className="flex flex-col items-center justify-center p-6 bg-paper/30 dark:bg-white/5 rounded-3xl border border-border/40 relative">
<div className="relative w-28 h-28 flex items-center justify-center">
<svg className="w-full h-full transform -rotate-90">
{/* Background circle */}
<circle
cx="56"
cy="56"
r={radius}
className="stroke-slate-100 dark:stroke-white/5 fill-transparent"
strokeWidth={strokeWidth}
/>
{/* Foreground circle */}
<circle
cx="56"
cy="56"
r={radius}
className="fill-transparent transition-all duration-500 ease-out"
stroke={donutColor}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
strokeLinecap="round"
/>
</svg>
<div className="absolute flex flex-col items-center justify-center">
<span className="text-xl font-bold text-ink">{Math.round(globalPct)}%</span>
<span className="text-[8px] text-concrete uppercase tracking-widest">{t('billing.used')}</span>
</div>
</div>
<div className="mt-4 text-center">
<p className="text-xs font-bold text-ink">
{totalUsed} <span className="text-concrete font-light">/ {totalLimit} {t('billing.aiCredits') || 'crédits IA'}</span>
</p>
</div>
</div>
</div>
{/* Usage breakdown per feature */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-concrete">
{t('billing.usageThisPeriod') || 'Utilisation sur cette période'}
</h3>
<p className="text-[10px] text-concrete mt-1">
{status?.currentPeriodStart && status?.currentPeriodEnd ? (
`${formatDate(status.currentPeriodStart)} ${formatDate(status.currentPeriodEnd)}`
) : (
'—'
)}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{!quotas && (
<div className="col-span-full flex items-center justify-center py-12 bg-white/40 dark:bg-white/5 border border-border rounded-3xl">
<Loader2 className="h-6 w-6 animate-spin text-brand-accent" />
</div>
)}
{quotas && Object.entries(quotas).map(([key, q]) => {
const pct = q.limit > 0 && q.limit !== Infinity ? (q.used / q.limit) * 100 : 0;
const isUnlimited = q.limit === Infinity || q.limit <= 0;
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 IA',
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',
};
// Couleur de barre : normal (violet), warning >= 75% (ambre), exhausted >= 100% (rose)
const barFillColor = pct >= 100 ? 'bg-rose-400' : pct >= 75 ? 'bg-amber-400' : 'bg-gradient-to-r from-violet-400 to-purple-400';
return (
<div key={key} className="p-5 rounded-2xl bg-white/40 dark:bg-white/5 border border-border space-y-3">
<div className="flex justify-between items-center">
<span className="text-[11px] font-bold text-ink uppercase tracking-wider truncate">
{featureLabels[key] || key}
</span>
<span className="text-[11px] font-medium text-concrete tabular-nums">
{isUnlimited ? t('billing.unlimited') || 'Illimité' : `${q.used} / ${q.limit}`}
</span>
</div>
<div className="h-2 w-full bg-secondary/40 rounded-full overflow-hidden">
<div
className={cn('h-full rounded-full transition-all duration-500', barFillColor)}
style={{ width: `${isUnlimited ? 100 : Math.min(pct, 100)}%` }}
/>
</div>
</div>
);
})}
</div>
</div>
{/* Billing History (Only for paid users) */}
{isPaid && <BillingHistory />}
{/* Interval Toggle & Plan Cards */}
<div className="space-y-8 pt-6 border-t border-border/40">
<div className="text-center space-y-2">
<h3 className="text-xs font-bold uppercase tracking-[0.2em] text-concrete">
{t('billing.upgradePlan') || 'Changer de plan'}
</h3>
</div>
{billingEnabled && (
<div className="flex items-center gap-2 justify-center">
<button
type="button"
onClick={() => setInterval('month')}
className={cn(
'px-4 py-1.5 text-xs font-medium rounded-full transition-all',
interval === 'month' ? 'bg-ink text-paper' : 'text-concrete hover:text-ink'
)}
>
{t('billing.monthly')}
</button>
<button
type="button"
onClick={() => setInterval('year')}
className={cn(
'px-4 py-1.5 text-xs font-medium rounded-full transition-all',
interval === 'year' ? 'bg-ink text-paper' : 'text-concrete hover:text-ink'
)}
>
{t('billing.annual')}
<span className="ms-1 text-emerald-600 dark:text-emerald-400">{t('billing.save')} ~17%</span>
</button>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{plans.map((plan) => (
<div
key={plan.id}
className={cn(
'relative p-8 rounded-[40px] border transition-all duration-500 overflow-hidden group flex flex-col',
plan.popular
? 'bg-white dark:bg-paper border-brand-accent shadow-2xl shadow-brand-accent/10 scale-105 z-10'
: 'bg-white/40 dark:bg-white/5 border-border hover:border-concrete/30'
)}
>
{plan.popular && (
<div className="absolute top-0 right-0 py-1.5 px-6 bg-brand-accent text-white text-[9px] font-bold uppercase tracking-widest rounded-bl-2xl">
{t('billing.recommended') || 'Recommandé'}
</div>
)}
<div className="mb-8 space-y-2">
<h4 className="text-xl font-serif font-bold text-ink">{plan.name}</h4>
<div className="flex items-baseline gap-1">
<span className="text-4xl font-serif font-bold text-ink">{plan.price}</span>
<span className="text-concrete text-xs font-light italic">{plan.period}</span>
</div>
<p className="text-xs text-concrete font-light leading-relaxed pe-4">{plan.description}</p>
</div>
<div className="space-y-4 mb-10 flex-1">
{plan.features.map((feature, i) => (
<div key={i} className="flex items-start gap-3">
<div className={cn('mt-1 p-0.5 rounded-full', plan.popular ? 'bg-brand-accent/10 text-brand-accent' : 'bg-concrete/10 text-concrete')}>
<Check size={10} />
</div>
<span className="text-xs font-light text-ink/80">{feature}</span>
</div>
))}
</div>
<button
onClick={plan.onClick}
disabled={plan.current || checkoutLoading !== null}
className={cn('w-full py-4 rounded-2xl text-[10px] font-bold uppercase tracking-[0.2em] transition-all duration-300', plan.buttonClass)}
>
<div className="flex items-center justify-center gap-2">
{checkoutLoading && !plan.current ? <Loader2 className="h-4 w-4 animate-spin" /> : plan.buttonText}
{!plan.current && <ArrowRight size={14} />}
</div>
</button>
</div>
))}
</div>
</div>
{/* Footer Info */}
<div className="bg-slate-50 dark:bg-black/20 rounded-[32px] p-8 border border-border/40 flex flex-col md:flex-row items-center justify-between gap-8">
<div className="flex items-center gap-4">
<div className="p-3 bg-white dark:bg-paper rounded-2xl shadow-sm border border-border">
<Shield size={24} className="text-brand-accent" />
</div>
<div>
<h5 className="text-sm font-bold text-ink">{t('billing.secureTransactions') || 'Transactions sécurisées'}</h5>
<p className="text-xs text-concrete font-light">{t('billing.secureDesc') || 'Paiement via Stripe. Annulez à tout moment, sans engagement.'}</p>
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<Zap size={16} className="text-ochre" />
<span className="text-[10px] font-bold uppercase tracking-widest text-concrete">{t('billing.instantActivation') || 'Activation instantanée'}</span>
</div>
<div className="flex items-center gap-2">
<Crown size={16} className="text-amber-500" />
<span className="text-[10px] font-bold uppercase tracking-widest text-concrete">{t('billing.satisfactionGuarantee') || 'Garantie satisfait'}</span>
</div>
</div>
</div>
{/* Embedded Checkout Modal */}
{isCheckoutOpen && checkoutClientSecret && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-4 border-b border-border">
<h3 className="font-semibold text-foreground">{t('billing.upgradeTitle')}</h3>
<button
type="button"
onClick={() => {
setIsCheckoutOpen(false);
setCheckoutClientSecret(null);
toast.info(t('billing.checkoutCanceled'));
}}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
<div className="p-2">
<EmbeddedCheckoutProvider
stripe={getStripePromise()}
options={{ clientSecret: checkoutClientSecret, onComplete: handleCheckoutComplete }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</div>
</div>
</div>
)}
</motion.div>
);
}