fix(ui): thème, textes et lisibilité restants de la revue
Les boutons suivent la couleur d’apparence, les libellés trop petits ou trop techniques sont clarifiés, et le catalogue des fournisseurs se met à jour tout seul. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -35,7 +35,7 @@ export function SettingsNav({ className }: SettingsNavProps) {
|
||||
<Link
|
||||
key={tab.id}
|
||||
href={tab.href}
|
||||
className="flex items-center gap-1.5 sm:gap-2.5 px-2 sm:px-4 py-3 text-[10px] font-bold uppercase tracking-[0.18em] transition-all relative whitespace-nowrap text-concrete hover:text-ink/60"
|
||||
className="flex items-center gap-1.5 sm:gap-2 px-2.5 sm:px-3 py-2.5 text-[13px] font-medium transition-all relative whitespace-nowrap text-concrete hover:text-ink"
|
||||
style={{ color: isActive(tab.href) ? 'var(--ink)' : undefined }}
|
||||
>
|
||||
<span style={{ color: isActive(tab.href) ? 'var(--ink)' : 'var(--concrete)' }}>{tab.icon}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { loadStripe } from '@stripe/stripe-js';
|
||||
import { EmbeddedCheckoutProvider, EmbeddedCheckout } from '@stripe/react-stripe-js';
|
||||
@@ -59,7 +59,7 @@ function getStripePromise(enabled: boolean) {
|
||||
}
|
||||
|
||||
export function BillingPlans() {
|
||||
const { t } = useLanguage();
|
||||
const { t, language } = useLanguage();
|
||||
const queryClient = useQueryClient();
|
||||
const [interval, setInterval] = useState<Interval>('month');
|
||||
const [checkoutClientSecret, setCheckoutClientSecret] = useState<string | null>(null);
|
||||
@@ -69,6 +69,7 @@ export function BillingPlans() {
|
||||
const [portalLoading, setPortalLoading] = useState(false);
|
||||
const [cancelLoading, setCancelLoading] = useState(false);
|
||||
const [successBanner, setSuccessBanner] = useState<string | null>(null);
|
||||
const plansSectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: status, isLoading } = useQuery<BillingStatus>({
|
||||
queryKey: ['billing', 'status'],
|
||||
@@ -80,6 +81,17 @@ export function BillingPlans() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: byokCatalog } = useQuery({
|
||||
queryKey: ['public', 'byok-catalog'],
|
||||
queryFn: async () => {
|
||||
const res = await fetch('/api/public/byok-catalog')
|
||||
if (!res.ok) throw new Error('catalog')
|
||||
return res.json() as Promise<{ providers: { id: string }[] }>
|
||||
},
|
||||
staleTime: 60_000,
|
||||
})
|
||||
const providerCount = byokCatalog?.providers.length || '…'
|
||||
|
||||
const { data: usageData } = useQuery({
|
||||
queryKey: ['usage', 'current'],
|
||||
queryFn: async () => {
|
||||
@@ -235,6 +247,18 @@ export function BillingPlans() {
|
||||
}
|
||||
};
|
||||
|
||||
const scrollToPlans = () => {
|
||||
plansSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
};
|
||||
|
||||
const handleChangePlan = (tier: Tier) => {
|
||||
if (status?.hasStripeSubscription) {
|
||||
void handlePortal('portal');
|
||||
return;
|
||||
}
|
||||
void handleCheckout(tier);
|
||||
};
|
||||
|
||||
const handleCheckoutComplete = useCallback(() => {
|
||||
setIsCheckoutOpen(false);
|
||||
setCheckoutClientSecret(null);
|
||||
@@ -276,11 +300,15 @@ export function BillingPlans() {
|
||||
t('billing.freeF5'),
|
||||
],
|
||||
current: effectiveTier === 'BASIC',
|
||||
buttonText: effectiveTier === 'BASIC' ? (t('billing.currentPlan') || 'Plan Actuel') : t('billing.startCheckout'),
|
||||
buttonText: effectiveTier === 'BASIC'
|
||||
? (t('billing.currentPlan') || 'Plan Actuel')
|
||||
: t('billing.downgradeToFree'),
|
||||
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: () => {},
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => {
|
||||
if (effectiveTier !== 'BASIC') void handleCancelSubscription();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pro',
|
||||
@@ -293,7 +321,7 @@ export function BillingPlans() {
|
||||
...(trialEligible ? [t('billing.trialFeature', { days: trialDays })] : []),
|
||||
t('billing.proFeature1'),
|
||||
t('billing.proFeature2'),
|
||||
t('billing.proFeature3'),
|
||||
t('billing.proFeature3', { count: providerCount }),
|
||||
t('billing.proFeature4'),
|
||||
t('billing.proFeature5'),
|
||||
t('billing.proFeature6'),
|
||||
@@ -304,7 +332,7 @@ export function BillingPlans() {
|
||||
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'),
|
||||
onClick: () => handleChangePlan('PRO'),
|
||||
},
|
||||
{
|
||||
id: 'business',
|
||||
@@ -316,7 +344,7 @@ export function BillingPlans() {
|
||||
...(trialEligible ? [t('billing.trialFeature', { days: trialDays })] : []),
|
||||
t('billing.businessFeature1'),
|
||||
t('billing.businessFeature2'),
|
||||
t('billing.businessFeature3'),
|
||||
t('billing.businessFeature3', { count: providerCount }),
|
||||
t('billing.businessFeature4'),
|
||||
t('billing.businessFeature5'),
|
||||
t('billing.businessFeature6'),
|
||||
@@ -325,8 +353,8 @@ export function BillingPlans() {
|
||||
buttonText: effectiveTier === 'BUSINESS' ? (t('billing.currentPlan') || 'Plan Actuel') : trialCta(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'),
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => handleChangePlan('BUSINESS'),
|
||||
},
|
||||
{
|
||||
id: 'enterprise',
|
||||
@@ -345,22 +373,24 @@ export function BillingPlans() {
|
||||
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',
|
||||
: 'bg-brand-accent text-white shadow-xl shadow-brand-accent/20 hover:scale-[1.02] active:scale-95',
|
||||
onClick: () => { window.location.href = 'mailto:sales@memento-note.com'; },
|
||||
},
|
||||
];
|
||||
|
||||
const plansToShow = isPaid ? plans.filter((p) => p.id !== 'free') : plans;
|
||||
const plansToShow = plans;
|
||||
|
||||
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';
|
||||
const locale = language === 'fa' ? 'fa-IR' : language === 'zh' ? 'zh-CN' : language;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
...(language === 'fa' ? { calendar: 'persian' as const } : {}),
|
||||
}).format(date);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
@@ -476,30 +506,44 @@ export function BillingPlans() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPaid && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={scrollToPlans}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-brand-accent text-white rounded-xl text-xs font-semibold hover:opacity-90 transition-all shadow-md shadow-brand-accent/20"
|
||||
>
|
||||
{t('billing.changeOffer')}
|
||||
</button>
|
||||
|
||||
{isPaid && !status?.cancelAtPeriodEnd && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelSubscription}
|
||||
disabled={cancelLoading}
|
||||
className="flex items-center gap-2 px-5 py-2.5 border border-rose-200 text-rose-600 dark:border-rose-800/40 dark:text-rose-400 hover:bg-rose-50/50 dark:hover:bg-rose-950/15 rounded-xl text-xs font-semibold transition-all"
|
||||
>
|
||||
{cancelLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
{t('billing.cancelSubscription')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isPaid && status?.hasStripeSubscription && (
|
||||
<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"
|
||||
className="flex items-center gap-2 px-5 py-2.5 border border-border text-ink rounded-xl text-xs font-semibold hover:bg-paper/60 dark:hover:bg-white/5 disabled:opacity-60 transition-all"
|
||||
>
|
||||
{portalLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
|
||||
{t('billing.manageBilling') || 'Gérer la facturation'}
|
||||
{t('billing.manageBilling')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status?.hasStripeSubscription && !status?.cancelAtPeriodEnd && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelSubscription}
|
||||
disabled={cancelLoading}
|
||||
className="flex items-center gap-2 px-5 py-2.5 border border-rose-200 text-rose-600 dark:border-rose-800/40 dark:text-rose-400 hover:bg-rose-50/50 dark:hover:bg-rose-950/15 rounded-xl text-xs font-semibold transition-all"
|
||||
>
|
||||
{cancelLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
||||
{t('billing.cancelSubscription') || "Résilier l'abonnement"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isPaid && status?.cancelAtPeriodEnd && (
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-xl px-3 py-2">
|
||||
{t('billing.cancellingNotice', { date: formatDate(status.currentPeriodEnd) })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -600,7 +644,7 @@ export function BillingPlans() {
|
||||
type="button"
|
||||
onClick={() => handleBuyPack(pack.id)}
|
||||
disabled={!pack.configured || packLoading !== null}
|
||||
className="mt-auto w-full py-3 rounded-2xl bg-ink text-white dark:bg-white dark:text-black text-[10px] font-bold uppercase tracking-[0.15em] hover:opacity-90 disabled:opacity-40 transition-all"
|
||||
className="mt-auto w-full py-3 rounded-2xl bg-brand-accent text-white text-[13px] font-semibold uppercase tracking-wider hover:opacity-90 disabled:opacity-40 transition-all"
|
||||
>
|
||||
{packLoading === pack.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mx-auto" />
|
||||
@@ -661,8 +705,8 @@ export function BillingPlans() {
|
||||
const pct = totalUsed > 0 && used > 0 ? (used / totalUsed) * 100 : 0
|
||||
const barFillColor =
|
||||
pct >= 40
|
||||
? 'bg-gradient-to-r from-violet-400 to-purple-400'
|
||||
: 'bg-gradient-to-r from-violet-300/80 to-purple-300/80'
|
||||
? 'bg-brand-accent'
|
||||
: 'bg-brand-accent/60'
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
@@ -693,8 +737,7 @@ export function BillingPlans() {
|
||||
{isPaid && <BillingHistory />}
|
||||
|
||||
{/* Interval Toggle & Plan Cards */}
|
||||
{!isPaid && (
|
||||
<div className="space-y-8 pt-6 border-t border-border/40">
|
||||
<div ref={plansSectionRef} 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'}
|
||||
@@ -708,7 +751,7 @@ export function BillingPlans() {
|
||||
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'
|
||||
interval === 'month' ? 'bg-brand-accent text-white' : 'text-concrete hover:text-ink'
|
||||
)}
|
||||
>
|
||||
{t('billing.monthly')}
|
||||
@@ -718,7 +761,7 @@ export function BillingPlans() {
|
||||
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'
|
||||
interval === 'year' ? 'bg-brand-accent text-white' : 'text-concrete hover:text-ink'
|
||||
)}
|
||||
>
|
||||
{t('billing.annual')}
|
||||
@@ -773,7 +816,12 @@ export function BillingPlans() {
|
||||
|
||||
<button
|
||||
onClick={plan.onClick}
|
||||
disabled={plan.current || checkoutLoading !== null}
|
||||
disabled={
|
||||
plan.current
|
||||
|| checkoutLoading !== null
|
||||
|| cancelLoading
|
||||
|| (plan.id === 'free' && !!status?.cancelAtPeriodEnd)
|
||||
}
|
||||
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">
|
||||
@@ -784,8 +832,7 @@ export function BillingPlans() {
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
|
||||
31
memento-note/components/settings/settings-document-title.tsx
Normal file
31
memento-note/components/settings/settings-document-title.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
|
||||
const TITLE_KEYS: Record<string, string> = {
|
||||
'/settings/general': 'generalSettings.title',
|
||||
'/settings/ai': 'aiSettings.title',
|
||||
'/settings/billing': 'billing.title',
|
||||
'/settings/appearance': 'appearance.title',
|
||||
'/settings/profile': 'profile.title',
|
||||
'/settings/data': 'dataManagement.title',
|
||||
'/settings/published': 'settings.publishedTitle',
|
||||
'/settings/integrations': 'integrations.title',
|
||||
'/settings/mcp': 'mcpSettings.title',
|
||||
'/settings/about': 'about.title',
|
||||
}
|
||||
|
||||
export function SettingsDocumentTitle() {
|
||||
const pathname = usePathname()
|
||||
const { t } = useLanguage()
|
||||
|
||||
useEffect(() => {
|
||||
const key = Object.keys(TITLE_KEYS).find((href) => pathname === href || pathname.startsWith(`${href}/`))
|
||||
const section = key ? t(TITLE_KEYS[key]) : t('settings.title')
|
||||
document.title = `${section} — Memento`
|
||||
}, [pathname, t])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-border/10 transition-colors"
|
||||
>
|
||||
<HelpCircle size={14} className="text-brand-accent shrink-0" />
|
||||
<span className="text-[12px] font-semibold text-ink flex-1">{title}</span>
|
||||
<span className="text-sm font-semibold text-ink flex-1">{title}</span>
|
||||
{open ? (
|
||||
<ChevronUp size={13} className="text-concrete shrink-0" />
|
||||
) : (
|
||||
@@ -40,10 +40,10 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
|
||||
<ol className="px-4 pb-4 space-y-2.5 border-t border-border/30 pt-3">
|
||||
{steps.map((step, i) => (
|
||||
<li key={i} className="flex items-start gap-2.5">
|
||||
<span className="w-5 h-5 rounded-full bg-brand-accent/10 text-brand-accent text-[10px] font-bold flex items-center justify-center shrink-0 mt-0.5">
|
||||
<span className="w-5 h-5 rounded-full bg-brand-accent/10 text-brand-accent text-xs font-semibold flex items-center justify-center shrink-0 mt-0.5">
|
||||
{step.icon ?? i + 1}
|
||||
</span>
|
||||
<span className="text-[12px] text-concrete leading-relaxed">
|
||||
<span className="text-sm text-concrete leading-relaxed">
|
||||
{step.text}
|
||||
{step.link && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user