Add reliable Outlook draft export
This commit is contained in:
1 parent
8678e92b54
commit
25fceefb51
3 files changed
+73
-6
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { api } from '../../lib/api.js';
|
import { api } from '../../lib/api.js';
|
||||||
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||||
|
import { downloadOutlookDraft } from '../../lib/outlookDraft.js';
|
||||||
|
|
||||||
const INITIAL = {
|
const INITIAL = {
|
||||||
subject: 'Actualités du Business Club SLUC Nancy',
|
subject: 'Actualités du Business Club SLUC Nancy',
|
||||||
@@ -65,6 +66,16 @@ export function EmailCreatorTab() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadOutlook = () => {
|
||||||
|
const slug = form.subject.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '').toLowerCase() || 'email';
|
||||||
|
downloadOutlookDraft({
|
||||||
|
subject: form.subject,
|
||||||
|
html: data.html,
|
||||||
|
filename: `${slug}-outlook.eml`,
|
||||||
|
});
|
||||||
|
flash('outlook');
|
||||||
|
};
|
||||||
|
|
||||||
const download = () => {
|
const download = () => {
|
||||||
const blob = new Blob([data.html], { type: 'text/html' });
|
const blob = new Blob([data.html], { type: 'text/html' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -115,12 +126,13 @@ export function EmailCreatorTab() {
|
|||||||
</div>
|
</div>
|
||||||
{error && <p className="error-text" style={{ marginTop: 12 }}>{error}</p>}
|
{error && <p className="error-text" style={{ marginTop: 12 }}>{error}</p>}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 20 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 20 }}>
|
||||||
<button className="btn btn-red" onClick={copyEmail} disabled={!data}>{copied === 'email' ? '✓ E-mail copié' : "Copier l'e-mail"}</button>
|
<button className="btn btn-red" onClick={downloadOutlook} disabled={!data}>{copied === 'outlook' ? '✓ Brouillon téléchargé' : 'Télécharger le brouillon Outlook'}</button>
|
||||||
|
<button className="btn btn-outline-soft btn-sm" style={{ padding: 12 }} onClick={copyEmail} disabled={!data}>{copied === 'email' ? '✓ E-mail copié' : "Copier l'e-mail (autres logiciels)"}</button>
|
||||||
<button className="btn btn-outline-soft btn-sm" style={{ padding: 12 }} onClick={copySubject}>{copied === 'subject' ? '✓ Objet copié' : "Copier l'objet"}</button>
|
<button className="btn btn-outline-soft btn-sm" style={{ padding: 12 }} onClick={copySubject}>{copied === 'subject' ? '✓ Objet copié' : "Copier l'objet"}</button>
|
||||||
<button className="btn btn-outline-soft btn-sm" style={{ padding: 12 }} onClick={download} disabled={!data}>Télécharger le HTML</button>
|
<button className="btn btn-outline-soft btn-sm" style={{ padding: 12 }} onClick={download} disabled={!data}>Télécharger le HTML</button>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: 11.5, color: 'var(--gray-light)', lineHeight: 1.5, marginTop: 14 }}>
|
<p style={{ fontSize: 11.5, color: 'var(--gray-light)', lineHeight: 1.5, marginTop: 14 }}>
|
||||||
Dans un brouillon Outlook, utilisez Ctrl + clic pour tester un lien. Après envoi, un clic normal suffit.
|
Pour Outlook, ouvrez le fichier .eml téléchargé : le mail est chargé comme un vrai brouillon HTML, sans collage ni remplacement des styles. Dans le brouillon, utilisez Ctrl + clic pour tester un lien.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { api, dateParts, seasonLabel, statutLabel } from '../../lib/api.js';
|
import { api, dateParts, seasonLabel, statutLabel } from '../../lib/api.js';
|
||||||
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
import { copyRichEmail } from '../../lib/emailClipboard.js';
|
||||||
|
import { downloadOutlookDraft } from '../../lib/outlookDraft.js';
|
||||||
import { useAuth } from '../../lib/AuthContext.jsx';
|
import { useAuth } from '../../lib/AuthContext.jsx';
|
||||||
import Modal from '../Modal.jsx';
|
import Modal from '../Modal.jsx';
|
||||||
import ImageSlot from '../ImageSlot.jsx';
|
import ImageSlot from '../ImageSlot.jsx';
|
||||||
@@ -392,6 +393,16 @@ function EmailModal({ renc, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadOutlook = () => {
|
||||||
|
const slug = renc.titre.normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase();
|
||||||
|
downloadOutlookDraft({
|
||||||
|
subject: data.subject,
|
||||||
|
html: data.html,
|
||||||
|
filename: `invitation-${slug}-outlook.eml`,
|
||||||
|
});
|
||||||
|
flash('outlook');
|
||||||
|
};
|
||||||
|
|
||||||
const download = () => {
|
const download = () => {
|
||||||
const blob = new Blob([data.html], { type: 'text/html' });
|
const blob = new Blob([data.html], { type: 'text/html' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -429,8 +440,11 @@ function EmailModal({ renc, onClose }) {
|
|||||||
automatiquement. Videz un champ pour retirer ce bloc de l'email.
|
automatiquement. Videz un champ pour retirer ce bloc de l'email.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 4 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 4 }}>
|
||||||
<button className="btn btn-red btn-sm" style={{ fontSize: 13.5, padding: '12px 18px' }} onClick={copyEmail}>
|
<button className="btn btn-red btn-sm" style={{ fontSize: 13.5, padding: '12px 18px' }} onClick={downloadOutlook}>
|
||||||
{copied === 'email' ? '✓ Copié' : "Copier l'email"}
|
{copied === 'outlook' ? '✓ Brouillon téléchargé' : 'Télécharger le brouillon Outlook'}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-outline-soft btn-sm" style={{ fontSize: 13, padding: '11px 18px' }} onClick={copyEmail}>
|
||||||
|
{copied === 'email' ? '✓ Copié' : "Copier l'email (autres logiciels)"}
|
||||||
</button>
|
</button>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<button className="btn btn-sm" style={{ flex: 1, background: 'var(--admin-bg)', fontSize: 13, padding: '11px 10px' }} onClick={() => copyText(data.subject, 'subject')}>
|
<button className="btn btn-sm" style={{ flex: 1, background: 'var(--admin-bg)', fontSize: 13, padding: '11px 10px' }} onClick={() => copyText(data.subject, 'subject')}>
|
||||||
@@ -445,8 +459,7 @@ function EmailModal({ renc, onClose }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p style={{ fontSize: 12, color: 'var(--gray-light)', lineHeight: 1.55 }}>
|
<p style={{ fontSize: 12, color: 'var(--gray-light)', lineHeight: 1.55 }}>
|
||||||
Collez l'email directement dans votre logiciel de messagerie (« Copier l'email »
|
Pour Outlook, ouvrez le fichier .eml téléchargé : il charge directement le vrai corps HTML sans passer par le presse-papiers. La copie reste disponible pour les autres logiciels de messagerie.
|
||||||
conserve la mise en forme), ou envoyez le fichier .html avec votre outil d'emailing.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
function utf8Base64(value) {
|
||||||
|
const bytes = new TextEncoder().encode(String(value ?? ''));
|
||||||
|
let binary = '';
|
||||||
|
|
||||||
|
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
|
||||||
|
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalThis.btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapBase64(value) {
|
||||||
|
return value.match(/.{1,76}/g)?.join('\r\n') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOutlookDraftMime({ subject, html }) {
|
||||||
|
// X-Unsent asks Outlook to open the MIME message as a message to compose.
|
||||||
|
// Loading HTML as a real MIME body bypasses Outlook's destructive paste merge.
|
||||||
|
return [
|
||||||
|
'X-Unsent: 1',
|
||||||
|
`Subject: =?UTF-8?B?${utf8Base64(subject)}?=`,
|
||||||
|
'MIME-Version: 1.0',
|
||||||
|
'Content-Type: text/html; charset=UTF-8',
|
||||||
|
'Content-Transfer-Encoding: base64',
|
||||||
|
'',
|
||||||
|
wrapBase64(utf8Base64(html)),
|
||||||
|
'',
|
||||||
|
].join('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadOutlookDraft({ subject, html, filename }) {
|
||||||
|
const mime = buildOutlookDraftMime({ subject, html });
|
||||||
|
const blob = new Blob([mime], { type: 'message/rfc822' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = filename;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 2000);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user