diff --git a/memento-note/app/(public)/pricing/page.tsx b/memento-note/app/(public)/pricing/page.tsx new file mode 100644 index 0000000..d779778 --- /dev/null +++ b/memento-note/app/(public)/pricing/page.tsx @@ -0,0 +1,94 @@ +'use client' + +import { motion } from 'motion/react' +import { Shield } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { useLanguage } from '@/lib/i18n' +import { useState } from 'react' + +export default function PricingPage() { + const { t } = useLanguage() + const router = useRouter() + const [billingInterval, setBillingInterval] = useState<'monthly' | 'annual'>('monthly') + + const PLANS = [ + { key: 'basic', popular: false, price: t('landing.pricing.basicPrice'), period: '' }, + { key: 'pro', popular: true, price: billingInterval === 'monthly' ? '9,90€' : '7,90€', period: billingInterval === 'monthly' ? t('landing.pricing.perMonth') : t('landing.pricing.perMonthAnnual') }, + { key: 'business', popular: false, price: billingInterval === 'monthly' ? '29,90€' : '23,90€', period: billingInterval === 'monthly' ? t('landing.pricing.perMonth') : t('landing.pricing.perMonthAnnual') }, + { key: 'enterprise', popular: false, price: billingInterval === 'monthly' ? '49,90€' : '39,90€', period: billingInterval === 'monthly' ? t('landing.pricing.perUser') : t('landing.pricing.perUserAnnual') }, + ] + + return ( +
+
+
+
+ {t('landing.pricing.label')} +

{t('landing.pricing.title')}

+

{t('landing.pricing.desc')}

+ +
+ +
+ +
+ (-20%) +
+
+
+
+ +
+ {PLANS.map((plan) => ( +
+ {plan.popular && ( +
+ {t('landing.pricing.popular')} +
+ )} +
+

{t(`landing.pricing.${plan.key}.name`)}

+
+ {plan.price} + {plan.period && {plan.period}} +
+

{t(`landing.pricing.${plan.key}.desc`)}

+
+
+ {[0, 1, 2, 3, 4, 5].map(j => { + const feat = t(`landing.pricing.${plan.key}.feature${j}`) + if (!feat || feat === `landing.pricing.${plan.key}.feature${j}`) return null + return ( +
+
+ +
+ {feat} +
+ ) + })} +
+ +
+ ))} +
+
+
+
+ ) +} diff --git a/memento-note/lib/ai/tools/pptx.tool.ts b/memento-note/lib/ai/tools/pptx.tool.ts index 4389c37..d84325f 100644 --- a/memento-note/lib/ai/tools/pptx.tool.ts +++ b/memento-note/lib/ai/tools/pptx.tool.ts @@ -624,26 +624,30 @@ function addImageContentSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, /** Full-bleed image slide with title + subtitle overlay */ function addImageFullSlide(pres: PptxGenJSModule, slide: SlideSpec, t: Theme, style: StyleCfg, idx: number) { const s = pres.addSlide() - s.background = { color: t.primary } + const hasImage = !!slide.imageUrl + // Use light bg as fallback when no image — t.primary makes the slide appear black without an image + s.background = { color: hasImage ? t.primary : t.bg } - if (slide.imageUrl) { + if (hasImage) { try { - s.addImage({ x: 0, y: 0, w: 10, h: 5.63, ...resolveImageProps(slide.imageUrl) }) - } catch { /* fallback: colored bg */ } + s.addImage({ x: 0, y: 0, w: 10, h: 5.63, ...resolveImageProps(slide.imageUrl!) }) + } catch { s.background = { color: t.bg } } } - // Dark overlay at bottom for text legibility - s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 10, h: 2.33, fill: { color: '000000', transparency: 40 } }) + // Overlay at bottom (dark for image, accent-tinted for no-image) + const overlayColor = hasImage ? '000000' : t.primary + s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 10, h: 2.33, fill: { color: overlayColor, transparency: hasImage ? 40 : 10 } }) s.addShape(SHAPE_RECT, { x: 0, y: 3.3, w: 0.12, h: 2.33, fill: { color: t.accent } }) + const titleColor = hasImage ? 'FFFFFF' : textOnBg(t.primary) s.addText(slide.title, { x: 0.3, y: 3.45, w: 9.4, h: 1.0, - fontSize: 28, fontFace: 'Arial', color: 'FFFFFF', bold: true, valign: 'middle', fit: 'shrink', + fontSize: 28, fontFace: 'Arial', color: titleColor, bold: true, valign: 'middle', fit: 'shrink', }) if (slide.subtitle) { s.addText(slide.subtitle, { x: 0.3, y: 4.5, w: 9.4, h: 0.8, - fontSize: 15, fontFace: 'Arial', color: 'FFFFFF', valign: 'middle', fit: 'shrink', transparency: 15, + fontSize: 15, fontFace: 'Arial', color: titleColor, valign: 'middle', fit: 'shrink', transparency: 15, }) } diff --git a/memento-note/lib/brainstorm/export-pptx.ts b/memento-note/lib/brainstorm/export-pptx.ts index 2569304..5b086fc 100644 --- a/memento-note/lib/brainstorm/export-pptx.ts +++ b/memento-note/lib/brainstorm/export-pptx.ts @@ -68,11 +68,11 @@ function truncate(text: string, max: number): string { /** * PLG viral watermark — added to every slide automatically via addSlide monkey-patch. - * Subtle branding in bottom-right corner to drive organic acquisition. + * LAYOUT_WIDE = 13.33" × 7.5" → watermark anchored near bottom-right at y ≈ 7.1 */ function addWatermark(slide: any) { slide.addText('memento-note.com', { - x: 7.0, y: 5.35, w: 2.7, h: 0.2, + x: 10.0, y: 7.1, w: 3.0, h: 0.25, fontSize: 7, fontFace: 'Arial', color: 'B8B0A8', align: 'right', italic: true, }) @@ -261,40 +261,44 @@ function buildTopIdeasSlide(pres: PptxGenJSModule, starred: IdeaLike[], converte function buildSummarySlide(pres: PptxGenJSModule, session: SessionLike, stats: { total: number; converted: number; starred: number; dismissed: number }) { const slide = pres.addSlide() addBg(slide) - - // Dark left panel - slide.addShape('rect', { x: 0, y: 0, w: '100%', h: '100%', fill: { color: T.primary } }) + addTopBar(slide, T.accent) slide.addText('BILAN DE SESSION', { - x: 0.8, y: 0.8, w: 8.4, h: 0.6, + x: 0.8, y: 0.25, w: 11.5, h: 0.5, fontSize: 10, fontFace: 'Arial', color: T.accent, bold: true, charSpacing: 5, align: 'left', }) - slide.addText(truncate(session.seedIdea, 70), { - x: 0.8, y: 1.55, w: 8.4, h: 1.0, - fontSize: 20, fontFace: 'Georgia', color: 'FFFFFF', align: 'left', wrap: true, + slide.addText(truncate(session.seedIdea, 80), { + x: 0.8, y: 0.9, w: 11.5, h: 1.0, + fontSize: 20, fontFace: 'Georgia', color: T.primary, align: 'left', wrap: true, }) // Divider - slide.addShape('rect', { x: 0.8, y: 2.8, w: 8.4, h: 0.02, fill: { color: T.accent } }) + slide.addShape('rect', { x: 0.8, y: 2.1, w: 11.5, h: 0.02, fill: { color: T.accent } }) - // Stats grid + // Stats cards const statCols = [ - { label: 'IDÉES GÉNÉRÉES', value: String(stats.total), color: T.wave2 }, - { label: 'CONVERTIES EN NOTES', value: String(stats.converted), color: T.green }, - { label: 'FAVORITES', value: String(stats.starred), color: T.wave1 }, - { label: 'REJETÉES', value: String(stats.dismissed), color: T.muted }, + { label: 'IDÉES GÉNÉRÉES', value: String(stats.total), color: T.wave2, bg: 'EEF6FF' }, + { label: 'CONVERTIES EN NOTES', value: String(stats.converted), color: T.green, bg: 'EDFAF4' }, + { label: 'FAVORITES', value: String(stats.starred), color: T.wave1, bg: 'FFF4EE' }, + { label: 'REJETÉES', value: String(stats.dismissed), color: T.muted, bg: 'F5F4F2' }, ] - statCols.forEach((s, i) => { - const x = 0.8 + i * 2.3 - slide.addText(s.value, { x, y: 3.2, w: 2.2, h: 0.9, fontSize: 36, fontFace: 'Georgia', color: s.color, bold: true, align: 'left' }) - slide.addText(s.label, { x, y: 4.1, w: 2.2, h: 0.5, fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1.5, wrap: true }) - }) + const cardW = 2.7 + const cardH = 2.8 + const startX = 0.8 + const startY = 2.3 - slide.addText('Généré par Momento', { - x: 0.8, y: 5.5, w: 8.4, h: 0.3, - fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'right', italic: true, + statCols.forEach((s, i) => { + const x = startX + i * (cardW + 0.2) + // Card bg + slide.addShape('roundRect', { x, y: startY, w: cardW, h: cardH, fill: { color: s.bg }, rectRadius: 0.1 }) + // Top accent strip + slide.addShape('roundRect', { x, y: startY, w: cardW, h: 0.1, fill: { color: s.color }, rectRadius: 0.05 }) + // Big number + slide.addText(s.value, { x: x + 0.2, y: startY + 0.35, w: cardW - 0.4, h: 1.2, fontSize: 48, fontFace: 'Georgia', color: s.color, bold: true, align: 'left', valign: 'middle' }) + // Label + slide.addText(s.label, { x: x + 0.2, y: startY + 1.7, w: cardW - 0.4, h: 0.9, fontSize: 8, fontFace: 'Arial', color: T.muted, align: 'left', charSpacing: 1.5, wrap: true }) }) }