Add phone and membership request modal
This commit is contained in:
1 parent
d7376c055c
commit
93f0187706
6 files changed
+110
-56
No files matched your search
@@ -83,10 +83,10 @@ publicRouter.post(
|
|||||||
validate(demandeSchema),
|
validate(demandeSchema),
|
||||||
async (req, res, next) => {
|
async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { nom, fonction, entreprise, email } = req.data;
|
const { nom, fonction, entreprise, email, tel } = req.data;
|
||||||
await query(
|
await query(
|
||||||
'INSERT INTO demandes_adhesion (nom, fonction, entreprise, email) VALUES ($1, $2, $3, $4)',
|
'INSERT INTO demandes_adhesion (nom, fonction, entreprise, email, tel) VALUES ($1, $2, $3, $4, $5)',
|
||||||
[nom, fonction, entreprise, email]
|
[nom, fonction, entreprise, email, tel]
|
||||||
);
|
);
|
||||||
res.status(201).json({ ok: true });
|
res.status(201).json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const demandeSchema = z.object({
|
|||||||
fonction: trimmed(120).optional().default(''),
|
fonction: trimmed(120).optional().default(''),
|
||||||
entreprise: trimmed(120, 1),
|
entreprise: trimmed(120, 1),
|
||||||
email: trimmed(254, 3).email(),
|
email: trimmed(254, 3).email(),
|
||||||
|
tel: trimmed(30, 6),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const inscriptionPublicSchema = z.object({
|
export const inscriptionPublicSchema = z.object({
|
||||||
|
|||||||
@@ -135,9 +135,11 @@ CREATE TABLE IF NOT EXISTS demandes_adhesion (
|
|||||||
fonction TEXT NOT NULL DEFAULT '' CHECK (char_length(fonction) <= 120),
|
fonction TEXT NOT NULL DEFAULT '' CHECK (char_length(fonction) <= 120),
|
||||||
entreprise TEXT NOT NULL CHECK (char_length(entreprise) BETWEEN 1 AND 120),
|
entreprise TEXT NOT NULL CHECK (char_length(entreprise) BETWEEN 1 AND 120),
|
||||||
email CITEXT NOT NULL CHECK (char_length(email) <= 254),
|
email CITEXT NOT NULL CHECK (char_length(email) <= 254),
|
||||||
|
tel TEXT CHECK (char_length(tel) <= 30),
|
||||||
statut TEXT NOT NULL DEFAULT 'nouvelle' CHECK (statut IN ('nouvelle', 'traitee')),
|
statut TEXT NOT NULL DEFAULT 'nouvelle' CHECK (statut IN ('nouvelle', 'traitee')),
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
ALTER TABLE demandes_adhesion ADD COLUMN IF NOT EXISTS tel TEXT CHECK (char_length(tel) <= 30);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS site_content (
|
CREATE TABLE IF NOT EXISTS site_content (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { api } from '../lib/api.js';
|
||||||
|
import Modal from './Modal.jsx';
|
||||||
|
|
||||||
|
const EMPTY_FORM = { nom: '', fonction: '', entreprise: '', email: '', tel: '' };
|
||||||
|
|
||||||
|
export function AdhesionForm({ showTitle = true }) {
|
||||||
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
|
const [done, setDone] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const onChange = (event) => setForm({ ...form, [event.target.name]: event.target.value });
|
||||||
|
const submit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await api.post('/api/public/demandes-adhesion', form);
|
||||||
|
setDone(true);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: '30px 10px' }}>
|
||||||
|
<div style={{ width: 58, height: 58, borderRadius: '50%', background: '#FBEDEC', color: 'var(--red)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28, margin: '0 auto 20px' }}>✓</div>
|
||||||
|
<h3 className="serif" style={{ fontSize: 26, fontWeight: 600, marginBottom: 10 }}>Demande envoyée !</h3>
|
||||||
|
<p style={{ fontSize: 15, color: 'var(--gray)', lineHeight: 1.6, marginBottom: 24 }}>
|
||||||
|
Merci pour votre intérêt. Notre équipe vous recontacte sous 48h.
|
||||||
|
</p>
|
||||||
|
<button className="btn btn-outline btn-sm" style={{ fontSize: 14, padding: '12px 24px' }} onClick={() => { setDone(false); setForm(EMPTY_FORM); }}>
|
||||||
|
Nouvelle demande
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={submit}>
|
||||||
|
{showTitle && <h3 className="serif" style={{ fontSize: 24, fontWeight: 600, marginBottom: 22 }}>Demande d'adhésion</h3>}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||||
|
<label className="field-plain">Nom & prénom
|
||||||
|
<input name="nom" value={form.nom} onChange={onChange} required maxLength={120} autoComplete="name" />
|
||||||
|
</label>
|
||||||
|
<label className="field-plain">Fonction
|
||||||
|
<input name="fonction" value={form.fonction} onChange={onChange} maxLength={120} autoComplete="organization-title" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="field-plain">Entreprise
|
||||||
|
<input name="entreprise" value={form.entreprise} onChange={onChange} required maxLength={120} autoComplete="organization" />
|
||||||
|
</label>
|
||||||
|
<label className="field-plain">Email professionnel
|
||||||
|
<input name="email" type="email" value={form.email} onChange={onChange} required maxLength={254} autoComplete="email" />
|
||||||
|
</label>
|
||||||
|
<label className="field-plain">Numéro de téléphone
|
||||||
|
<input name="tel" type="tel" value={form.tel} onChange={onChange} required minLength={6} maxLength={30} autoComplete="tel" placeholder="+33 6 12 34 56 78" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{error && <p className="error-text" style={{ marginTop: 12 }}>{error}</p>}
|
||||||
|
<button type="submit" className="btn btn-red" style={{ width: '100%', marginTop: 24, padding: 15 }} disabled={busy}>
|
||||||
|
{busy ? 'Envoi…' : 'Envoyer ma demande'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AdhesionModal({ onClose }) {
|
||||||
|
return (
|
||||||
|
<Modal onClose={onClose} maxWidth={560}>
|
||||||
|
<div style={{ position: 'relative', padding: '34px 38px 38px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Fermer"
|
||||||
|
style={{ position: 'absolute', top: 14, right: 14, width: 32, height: 32, borderRadius: '50%', border: 'none', background: 'var(--beige)', color: 'var(--gray)', cursor: 'pointer', fontSize: 18 }}
|
||||||
|
>×</button>
|
||||||
|
<h2 className="serif" style={{ fontSize: 27, fontWeight: 600, marginBottom: 24, paddingRight: 28 }}>Demande d'adhésion</h2>
|
||||||
|
<AdhesionForm showTitle={false} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ export default function Header() {
|
|||||||
<NavLink to="/rencontres-passees" className={navLinkClass}>Rencontres passées</NavLink>
|
<NavLink to="/rencontres-passees" className={navLinkClass}>Rencontres passées</NavLink>
|
||||||
<NavLink to="/association" className={navLinkClass}>L'association</NavLink>
|
<NavLink to="/association" className={navLinkClass}>L'association</NavLink>
|
||||||
<NavLink to="/espace-membre" className={navLinkClass}>Espace membre</NavLink>
|
<NavLink to="/espace-membre" className={navLinkClass}>Espace membre</NavLink>
|
||||||
<Link to="/" style={{ marginLeft: 10, textDecoration: 'none' }}>
|
<Link to="/?adhesion=1" style={{ marginLeft: 10, textDecoration: 'none' }}>
|
||||||
<button className="btn btn-red" style={{ padding: '12px 22px', fontSize: 14 }}>Devenir membre</button>
|
<button className="btn btn-red" style={{ padding: '12px 22px', fontSize: 14 }}>Devenir membre</button>
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
+15
-52
@@ -5,6 +5,7 @@ import { useAuth } from '../lib/AuthContext.jsx';
|
|||||||
import { api, dateParts } from '../lib/api.js';
|
import { api, dateParts } from '../lib/api.js';
|
||||||
import Modal from '../components/Modal.jsx';
|
import Modal from '../components/Modal.jsx';
|
||||||
import PastEventCard from '../components/PastEventCard.jsx';
|
import PastEventCard from '../components/PastEventCard.jsx';
|
||||||
|
import { AdhesionForm, AdhesionModal } from '../components/AdhesionForm.jsx';
|
||||||
|
|
||||||
export function InscriptionModal({ rencontre, onClose, onDone }) {
|
export function InscriptionModal({ rencontre, onClose, onDone }) {
|
||||||
const { user, login } = useAuth();
|
const { user, login } = useAuth();
|
||||||
@@ -161,9 +162,7 @@ export default function Home() {
|
|||||||
const { data, reload } = usePublicData();
|
const { data, reload } = usePublicData();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [inscrRenc, setInscrRenc] = useState(null);
|
const [inscrRenc, setInscrRenc] = useState(null);
|
||||||
const [homeDone, setHomeDone] = useState(false);
|
const [adhesionOpen, setAdhesionOpen] = useState(false);
|
||||||
const [homeError, setHomeError] = useState('');
|
|
||||||
const [form, setForm] = useState({ nom: '', fonction: '', entreprise: '', email: '' });
|
|
||||||
|
|
||||||
// Registration links in invitation emails point to /?inscription=<id> :
|
// Registration links in invitation emails point to /?inscription=<id> :
|
||||||
// once the public data is loaded, open the matching inscription modal.
|
// once the public data is loaded, open the matching inscription modal.
|
||||||
@@ -174,21 +173,21 @@ export default function Home() {
|
|||||||
if (renc) setInscrRenc(renc);
|
if (renc) setInscrRenc(renc);
|
||||||
}, [data, searchParams]);
|
}, [data, searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.has('adhesion')) setAdhesionOpen(true);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
const closeInscription = () => {
|
const closeInscription = () => {
|
||||||
setInscrRenc(null);
|
setInscrRenc(null);
|
||||||
if (searchParams.has('inscription')) setSearchParams({}, { replace: true });
|
if (searchParams.has('inscription')) setSearchParams({}, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (e) => setForm({ ...form, [e.target.name]: e.target.value });
|
const closeAdhesion = () => {
|
||||||
|
setAdhesionOpen(false);
|
||||||
const submitHome = async (e) => {
|
if (searchParams.has('adhesion')) {
|
||||||
e.preventDefault();
|
const next = new URLSearchParams(searchParams);
|
||||||
setHomeError('');
|
next.delete('adhesion');
|
||||||
try {
|
setSearchParams(next, { replace: true });
|
||||||
await api.post('/api/public/demandes-adhesion', form);
|
|
||||||
setHomeDone(true);
|
|
||||||
} catch (err) {
|
|
||||||
setHomeError(err.message);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,9 +211,7 @@ export default function Home() {
|
|||||||
de haut niveau : performance, engagement et esprit collectif.
|
de haut niveau : performance, engagement et esprit collectif.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', gap: 14, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 14, flexWrap: 'wrap' }}>
|
||||||
<a href="#adhesion" style={{ textDecoration: 'none' }}>
|
<button className="btn btn-red" onClick={() => setAdhesionOpen(true)}>Rejoindre le club</button>
|
||||||
<button className="btn btn-red">Rejoindre le club</button>
|
|
||||||
</a>
|
|
||||||
<Link to="/association" style={{ textDecoration: 'none' }}>
|
<Link to="/association" style={{ textDecoration: 'none' }}>
|
||||||
<button className="btn btn-outline">Découvrir l'association</button>
|
<button className="btn btn-outline">Découvrir l'association</button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -390,42 +387,7 @@ export default function Home() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ background: '#fff', color: 'var(--ink)', borderRadius: 6, padding: 38 }}>
|
<div style={{ background: '#fff', color: 'var(--ink)', borderRadius: 6, padding: 38 }}>
|
||||||
{homeDone ? (
|
<AdhesionForm />
|
||||||
<div style={{ textAlign: 'center', padding: '30px 10px' }}>
|
|
||||||
<div style={{ width: 58, height: 58, borderRadius: '50%', background: '#FBEDEC', color: 'var(--red)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28, margin: '0 auto 20px' }}>✓</div>
|
|
||||||
<h3 className="serif" style={{ fontSize: 26, fontWeight: 600, marginBottom: 10 }}>Demande envoyée !</h3>
|
|
||||||
<p style={{ fontSize: 15, color: 'var(--gray)', lineHeight: 1.6, marginBottom: 24 }}>
|
|
||||||
Merci pour votre intérêt. Notre équipe vous recontacte sous 48h.
|
|
||||||
</p>
|
|
||||||
<button className="btn btn-outline btn-sm" style={{ fontSize: 14, padding: '12px 24px' }} onClick={() => { setHomeDone(false); setForm({ nom: '', fonction: '', entreprise: '', email: '' }); }}>
|
|
||||||
Nouvelle demande
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={submitHome}>
|
|
||||||
<h3 className="serif" style={{ fontSize: 24, fontWeight: 600, marginBottom: 22 }}>Demande d'adhésion</h3>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
|
||||||
<label className="field-plain">Nom & prénom
|
|
||||||
<input name="nom" value={form.nom} onChange={onChange} required maxLength={120} />
|
|
||||||
</label>
|
|
||||||
<label className="field-plain">Fonction
|
|
||||||
<input name="fonction" value={form.fonction} onChange={onChange} maxLength={120} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label className="field-plain">Entreprise
|
|
||||||
<input name="entreprise" value={form.entreprise} onChange={onChange} required maxLength={120} />
|
|
||||||
</label>
|
|
||||||
<label className="field-plain">Email professionnel
|
|
||||||
<input name="email" type="email" value={form.email} onChange={onChange} required maxLength={254} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{homeError && <p className="error-text" style={{ marginTop: 12 }}>{homeError}</p>}
|
|
||||||
<button type="submit" className="btn btn-red" style={{ width: '100%', marginTop: 24, padding: 15 }}>
|
|
||||||
Envoyer ma demande
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -433,6 +395,7 @@ export default function Home() {
|
|||||||
{inscrRenc && (
|
{inscrRenc && (
|
||||||
<InscriptionModal key={inscrRenc.id} rencontre={inscrRenc} onClose={closeInscription} onDone={reload} />
|
<InscriptionModal key={inscrRenc.id} rencontre={inscrRenc} onClose={closeInscription} onDone={reload} />
|
||||||
)}
|
)}
|
||||||
|
{adhesionOpen && <AdhesionModal onClose={closeAdhesion} />}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user