All checks were successful
Build & Deploy API / build-and-deploy (push) Successful in 1m8s
Avant : body crème + container blanc avec border-radius + bordure → effet "card flottante" qui laissait des marges crème en haut/bas et un fond blanc pour le contenu. Sur iPhone Mail, cette structure créait un gap mal rendu entre la frame Mail.app et le header rubis-deep. Maintenant : - bodyStyle.padding 24px 0 → 0 (rubis-deep colle au haut de la zone mail) - containerStyle background blanc → crème (toute la zone email est crème, cohérente avec la palette) - containerStyle borderRadius + border supprimés (edge-to-edge) - invoiceCardStyle (checkin) + summaryCardStyle (relance) passent en blanc pour se détacher du nouveau fond crème - Dark mode CSS : .rubis-container override aussi mis à crème Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
216 lines
5.7 KiB
TypeScript
216 lines
5.7 KiB
TypeScript
/**
|
|
* Template email check-in — envoyé à L'UTILISATEUR (le patron de TPE)
|
|
* pour lui demander si une facture donnée a été payée AVANT que la 1re
|
|
* relance ne parte chez son client.
|
|
*
|
|
* Structure :
|
|
* - Header rubis-deep "Rubis · Confirmation requise"
|
|
* - Eyebrow + question Avez-vous été payé ?
|
|
* - Card facture (numéro + montant + client + échéance)
|
|
* - 2 gros boutons CTA :
|
|
* • "✓ Oui — la facture est payée" → /paid (status=paid + cancel relances)
|
|
* • "→ Non — toujours impayée, lance les relances" → /pending
|
|
* - Mention TTL 24h
|
|
*/
|
|
|
|
import * as React from 'react'
|
|
import { Section, Text, Button, Heading, Hr } from '@react-email/components'
|
|
|
|
import { BRAND, sp } from './_brand.js'
|
|
import { EmailLayout } from './_layout.js'
|
|
|
|
export type CheckinEmailProps = {
|
|
invoice: {
|
|
numero: string
|
|
amountFormatted: string
|
|
dueDateFormatted: string
|
|
}
|
|
client: { name: string }
|
|
user: { fullName: string | null }
|
|
paidUrl: string
|
|
pendingUrl: string
|
|
/** URL landing publique (footer cliquable "Rubis sur l'ongle"). */
|
|
landingUrl?: string
|
|
}
|
|
|
|
export function CheckinEmail({
|
|
invoice,
|
|
client,
|
|
user,
|
|
paidUrl,
|
|
pendingUrl,
|
|
landingUrl,
|
|
}: CheckinEmailProps) {
|
|
const greeting = user.fullName ? `Bonjour ${user.fullName.split(' ')[0]},` : 'Bonjour,'
|
|
|
|
return (
|
|
<EmailLayout
|
|
preview={`${invoice.numero} (${invoice.amountFormatted}) — payée par ${client.name} ?`}
|
|
brandName="Rubis"
|
|
brandSubtitle="Confirmation requise"
|
|
landingUrl={landingUrl}
|
|
>
|
|
<Text style={greetingStyle}>{greeting}</Text>
|
|
|
|
<Heading as="h1" style={titleStyle}>
|
|
Avez-vous été <em style={emStyle}>payé</em> sur cette facture ?
|
|
</Heading>
|
|
|
|
<Text style={leadStyle}>
|
|
Aucune relance ne part sans votre validation. Si la facture est déjà
|
|
réglée, on évite l'email inutile et on encaisse +1 rubis.
|
|
</Text>
|
|
|
|
{/* Card facture */}
|
|
<Section style={invoiceCardStyle}>
|
|
<Text style={invoiceNumeroStyle}>{invoice.numero}</Text>
|
|
<Text style={invoiceClientStyle}>{client.name}</Text>
|
|
<Text style={invoiceAmountStyle}>{invoice.amountFormatted}</Text>
|
|
<Text style={invoiceDueStyle}>
|
|
Échéance le <strong>{invoice.dueDateFormatted}</strong>
|
|
</Text>
|
|
</Section>
|
|
|
|
{/* CTA boutons */}
|
|
<Section style={ctaSectionStyle}>
|
|
<Button href={paidUrl} style={primaryButtonStyle}>
|
|
✓ Oui, la facture est payée
|
|
</Button>
|
|
</Section>
|
|
<Section style={{ marginTop: sp.md }}>
|
|
<Button href={pendingUrl} style={secondaryButtonStyle}>
|
|
Non, toujours en attente — lance les relances
|
|
</Button>
|
|
</Section>
|
|
|
|
<Hr style={hrStyle} />
|
|
|
|
<Text style={footnoteStyle}>
|
|
Ces liens expirent dans 24h. Vous pouvez aussi répondre directement
|
|
depuis l'app sur la fiche facture.
|
|
</Text>
|
|
</EmailLayout>
|
|
)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Styles inline
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const greetingStyle: React.CSSProperties = {
|
|
fontSize: '14px',
|
|
color: BRAND.ink2,
|
|
margin: `0 0 ${sp.md} 0`,
|
|
}
|
|
|
|
const titleStyle: React.CSSProperties = {
|
|
color: BRAND.ink,
|
|
fontSize: '24px',
|
|
fontWeight: 700,
|
|
letterSpacing: '-0.018em',
|
|
lineHeight: '1.2',
|
|
margin: `0 0 ${sp.md} 0`,
|
|
}
|
|
|
|
const emStyle: React.CSSProperties = {
|
|
color: BRAND.rubis,
|
|
fontStyle: 'normal',
|
|
}
|
|
|
|
const leadStyle: React.CSSProperties = {
|
|
color: BRAND.ink2,
|
|
fontSize: '14px',
|
|
lineHeight: '1.6',
|
|
margin: `0 0 ${sp.xl} 0`,
|
|
}
|
|
|
|
const invoiceCardStyle: React.CSSProperties = {
|
|
// Blanc sur fond crème pour détacher la card visuellement (avant
|
|
// c'était crème sur container blanc — maintenant le container est crème).
|
|
backgroundColor: BRAND.white,
|
|
border: `1px solid ${BRAND.line}`,
|
|
borderRadius: BRAND.radiusCard,
|
|
padding: `${sp.lg} ${sp.xl}`,
|
|
margin: `0 0 ${sp.xl} 0`,
|
|
}
|
|
|
|
const invoiceNumeroStyle: React.CSSProperties = {
|
|
fontSize: '13px',
|
|
fontWeight: 600,
|
|
color: BRAND.ink2,
|
|
letterSpacing: '0.02em',
|
|
margin: 0,
|
|
}
|
|
|
|
const invoiceClientStyle: React.CSSProperties = {
|
|
fontSize: '13px',
|
|
color: BRAND.ink3,
|
|
margin: `${sp.xs} 0 ${sp.md} 0`,
|
|
}
|
|
|
|
const invoiceAmountStyle: React.CSSProperties = {
|
|
fontSize: '28px',
|
|
fontWeight: 800,
|
|
letterSpacing: '-0.02em',
|
|
color: BRAND.ink,
|
|
margin: 0,
|
|
lineHeight: '1',
|
|
fontVariantNumeric: 'tabular-nums',
|
|
}
|
|
|
|
const invoiceDueStyle: React.CSSProperties = {
|
|
fontSize: '12px',
|
|
color: BRAND.ink3,
|
|
margin: `${sp.sm} 0 0 0`,
|
|
}
|
|
|
|
const ctaSectionStyle: React.CSSProperties = {
|
|
marginTop: 0,
|
|
}
|
|
|
|
const primaryButtonStyle: React.CSSProperties = {
|
|
backgroundColor: BRAND.rubis,
|
|
color: BRAND.white,
|
|
borderRadius: BRAND.radiusButton,
|
|
display: 'block',
|
|
textAlign: 'center',
|
|
textDecoration: 'none',
|
|
padding: `${sp.md} ${sp.xl}`,
|
|
fontSize: '15px',
|
|
fontWeight: 600,
|
|
width: '100%',
|
|
boxSizing: 'border-box',
|
|
// Shadow rubis-teintée pour cohérence avec les boutons SPA
|
|
boxShadow: '0 4px 12px rgba(159, 18, 57, 0.25)',
|
|
}
|
|
|
|
const secondaryButtonStyle: React.CSSProperties = {
|
|
backgroundColor: BRAND.white,
|
|
color: BRAND.ink,
|
|
border: `1px solid ${BRAND.ink}`,
|
|
borderRadius: BRAND.radiusButton,
|
|
display: 'block',
|
|
textAlign: 'center',
|
|
textDecoration: 'none',
|
|
padding: `${sp.md} ${sp.xl}`,
|
|
fontSize: '15px',
|
|
fontWeight: 600,
|
|
width: '100%',
|
|
boxSizing: 'border-box',
|
|
}
|
|
|
|
const hrStyle: React.CSSProperties = {
|
|
borderColor: BRAND.line,
|
|
borderStyle: 'solid',
|
|
borderWidth: '0 0 1px 0',
|
|
margin: `${sp.xl} 0 ${sp.lg} 0`,
|
|
}
|
|
|
|
const footnoteStyle: React.CSSProperties = {
|
|
fontSize: '11.5px',
|
|
color: BRAND.ink3,
|
|
lineHeight: '1.5',
|
|
margin: 0,
|
|
fontStyle: 'italic',
|
|
}
|