Files
parenthese/frontend/src/components/ContributionSessionModal.jsx
T
IliesAllaliandClaude Opus 5.5 6f28918dde Interface en anglais : traduction, bascule FR · EN, accents corrigés
Module frontend/src/i18n : le texte français sert de clé, dictionnaires anglais
par zone, langue du navigateur puis choix mémorisé, ?lang=en pour forcer.
Bascule dans le menu du compte, la pastille visiteur, le menu Plus, l'écran
d'accès et la connexion. Démo traduite (noms et lieux gardés). Liens d'aide et
de vie privée selon la langue. Textes français sans accents corrigés.
Test : chaque texte passé à t() a sa traduction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:01:15 +02:00

151 lines
5.9 KiB
React

import { useState, useEffect } from 'react'
import { Send, X } from 'lucide-react'
import './ContributionSessionModal.css'
import PzBusy from './PzBusy.jsx'
import { readContributorName } from '../utils/contributorName'
import { t } from '../i18n/index.js'
function formatAction(action) {
if (action === 'create') return t('Ajout')
if (action === 'update') return t('Modification')
if (action === 'delete') return t('Suppression')
return t('Changement')
}
/**
* Modal de finalisation de session contribution
* Pour les contributeurs : commentaire optionnel + récap + liste des changements
*
* @param {Object} props
* @param {boolean} props.visible - Afficher la modal
* @param {Array} props.changes - Liste des changements { entityType, action, label }
* @param {Object} props.recap - Récap des modifications { added, modified, deleted }
* @param {boolean} props.loading - Soumission en cours
* @param {string} props.error - Message d'erreur
* @param {Function} props.onSubmit - Handler soumission ({ comment })
* @param {Function} props.onCancel - Handler annulation
*/
function ContributionSessionModal({ visible, changes = [], recap = {}, loading = false, error = '', askName = true, onSubmit, onCancel }) {
const [comment, setComment] = useState('')
const [name, setName] = useState('')
// Reset à l'ouverture ; le prénom déjà donné sur cet appareil est repris
useEffect(() => {
if (visible) {
setComment('')
setName(readContributorName())
}
}, [visible])
// ESC pour fermer
useEffect(() => {
if (!visible) return
function handleKeyDown(e) {
if (e.key === 'Escape') onCancel()
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [visible, onCancel])
const handleSubmit = (e) => {
e.preventDefault()
if (askName && !name.trim()) return
onSubmit({ comment: comment.trim(), name: name.trim() })
}
const totalChanges = (recap.added || 0) + (recap.modified || 0) + (recap.deleted || 0)
if (!visible) return null
const [titleBefore, titleAfter] = t('Envoyer vos {em}').split('{em}')
return (
<div className="pz-overlay" onClick={onCancel}>
<div
className="pz-modal contrib-session-modal"
role="dialog"
aria-modal="true"
aria-labelledby="contrib-session-title"
onClick={(e) => e.stopPropagation()}
>
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onCancel} aria-label={t('Fermer')}>
<X size={18} strokeWidth={2} />
</button>
<div className="pz-modal-head">
<p className="pz-eyebrow">{t('Votre contribution')}</p>
<h2 id="contrib-session-title" className="pz-title">{titleBefore}<em>{t('modifications')}</em>{titleAfter}</h2>
<p className="pz-sub">{t("La personne qui gère l'arbre les relit, puis elles apparaissent pour toute la famille.")}</p>
</div>
{totalChanges > 0 && (
<div className="cs-recap">
{recap.added > 0 && <span className="pz-tag cs-tag cs-tag--added">{recap.added > 1 ? t('{n} ajouts', { n: recap.added }) : t('{n} ajout', { n: recap.added })}</span>}
{recap.modified > 0 && <span className="pz-tag cs-tag cs-tag--modified">{recap.modified > 1 ? t('{n} modifications', { n: recap.modified }) : t('{n} modification', { n: recap.modified })}</span>}
{recap.deleted > 0 && <span className="pz-tag cs-tag cs-tag--deleted">{recap.deleted > 1 ? t('{n} suppressions', { n: recap.deleted }) : t('{n} suppression', { n: recap.deleted })}</span>}
</div>
)}
{changes.length > 0 && (
<ul className="cs-changes">
{changes.map((ch, i) => (
<li key={i} className="cs-change">
<span className={`cs-dot cs-dot--${ch.action}`} aria-hidden="true" />
<span className="cs-change-action">{formatAction(ch.action)}</span>
<span className="cs-change-label">{ch.label}</span>
</li>
))}
</ul>
)}
<form className="cs-form" onSubmit={handleSubmit}>
{askName && (
<div className="pz-field">
<label htmlFor="cs-name">{t('Votre prénom')}</label>
<input
id="cs-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value.slice(0, 120))}
placeholder={t('Pour que la famille sache qui a ajouté quoi')}
autoComplete="given-name"
required
/>
</div>
)}
<div className="pz-field">
<label htmlFor="cs-comment">
{t('Un mot pour accompagner')} <span className="cs-optional">{t('facultatif')}</span>
</label>
<textarea
id="cs-comment"
value={comment}
onChange={(e) => setComment(e.target.value.slice(0, 500))}
placeholder={t("Par exemple, j'ai ajouté les photos du mariage de mes grands-parents")}
rows={3}
maxLength={500}
/>
<span className="pz-hint cs-count">{comment.length} / 500</span>
</div>
{error && <div className="pz-error" role="alert">{error}</div>}
<div className="pz-modal-actions">
<button type="button" className="pz-btn pz-btn--ghost" onClick={onCancel} disabled={loading}>
{t('Annuler')}
</button>
<button type="submit" className="pz-btn pz-btn--primary" disabled={loading || (askName && !name.trim())}>
<Send size={16} strokeWidth={2} />
<PzBusy busy={loading} busyLabel={t('Envoi en cours')}>{t('Envoyer mes contributions')}</PzBusy>
</button>
</div>
</form>
</div>
</div>
)
}
export default ContributionSessionModal