Skip to content

Commit 57dcf57

Browse files
committed
Refacto and UI imrpovement
1 parent 261952e commit 57dcf57

5 files changed

Lines changed: 450 additions & 543 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ log.txt
77

88
.idea
99

10-
node_modules/
10+
node_modules/
11+
/.phpunit.result.cache
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
{# ── Modal création stream ──
2+
Variables attendues : isAdmin, events, ownerEmail, currentUser,
3+
organizationSlug, formSlug, title
4+
#}
5+
<div class="modal fade" id="createStreamModal" tabindex="-1">
6+
<div class="modal-dialog modal-lg modal-dialog-centered">
7+
<div class="modal-content border-0 shadow-lg">
8+
<div class="modal-header border-bottom border-secondary px-4 py-3">
9+
<div class="d-flex align-items-center gap-2">
10+
<span class="fs-4">🎮</span>
11+
<h5 class="modal-title fw-bold mb-0">Nouveau stream</h5>
12+
</div>
13+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
14+
</div>
15+
<div class="modal-body px-4 py-4">
16+
17+
{# Sélection du mode via cartes cliquables #}
18+
<p class="text-muted small mb-3">Comment souhaitez-vous créer ce stream ?</p>
19+
<div class="row g-3 mb-4" id="modeSelectorCards">
20+
<div class="col-6">
21+
<div class="stream-mode-card card h-100 border border-secondary rounded-3 p-3 text-center cursor-pointer active"
22+
id="cardManuel" onclick="selectStreamMode('manuel')">
23+
<div class="fs-2 mb-2">⌨️</div>
24+
<div class="fw-semibold">Manuellement</div>
25+
<div class="text-muted small mt-1">Renseignez les slugs vous-même</div>
26+
</div>
27+
</div>
28+
<div class="col-6">
29+
<div class="stream-mode-card card h-100 border border-secondary rounded-3 p-3 text-center cursor-pointer"
30+
id="cardAsso" onclick="selectStreamMode('asso')">
31+
<div class="fs-2 mb-2">🔗</div>
32+
<div class="fw-semibold">Depuis mon association</div>
33+
<div class="text-muted small mt-1">Importez via HelloAsso OAuth</div>
34+
</div>
35+
</div>
36+
</div>
37+
38+
{# == MODE MANUEL == #}
39+
<div id="modeManuel">
40+
<form action="/admin/stream" method="POST" class="needs-validation" novalidate>
41+
{% if events %}
42+
<div class="mb-3">
43+
<label for="parent_event" class="form-label fw-semibold">
44+
<span class="me-1">📅</span> Évènement parent
45+
<span class="text-muted fw-normal small ms-1">(optionnel)</span>
46+
</label>
47+
<select class="form-select" id="parent_event" name="parent_event">
48+
<option value="">— Aucun évènement —</option>
49+
{% for event in events %}
50+
<option value="{{ event.id }}">{{ event.title }}</option>
51+
{% endfor %}
52+
</select>
53+
</div>
54+
<div class="mb-3 p-3 rounded-3 bg-body-tertiary">
55+
<div class="form-check form-switch mb-0">
56+
<input class="form-check-input" type="checkbox" role="switch" id="parent_style" name="parent_style">
57+
<label for="parent_style" class="form-check-label">
58+
Appliquer le même style que l'évènement parent ?
59+
</label>
60+
</div>
61+
</div>
62+
{% endif %}
63+
64+
{# Email : visible pour admin, hidden pour user #}
65+
{% if isAdmin %}
66+
<div class="mb-3">
67+
<label for="owner_email" class="form-label fw-semibold">
68+
<span class="me-1">📧</span> Email du propriétaire
69+
</label>
70+
<input type="email" class="form-control" id="owner_email" name="owner_email"
71+
value="{{ ownerEmail }}" placeholder="utilisateur@exemple.com" required>
72+
<div class="invalid-feedback">Veuillez saisir un email valide.</div>
73+
</div>
74+
{% else %}
75+
<input type="hidden" name="owner_email" value="{{ currentUser.email }}">
76+
{% endif %}
77+
78+
<div class="mb-3">
79+
<label for="organization_slug" class="form-label fw-semibold">
80+
<span class="me-1">🏢</span> Slug de l'association
81+
</label>
82+
<div class="input-group">
83+
<span class="input-group-text text-muted small">helloasso.com/associations/</span>
84+
<input type="text" class="form-control" id="organization_slug" name="organization_slug"
85+
value="{{ organizationSlug }}" placeholder="mon-association" required>
86+
</div>
87+
<div class="invalid-feedback">Le slug de l'association est requis.</div>
88+
</div>
89+
<div class="mb-3">
90+
<label for="form_id" class="form-label fw-semibold">
91+
<span class="me-1">📋</span> Slug du formulaire de don
92+
</label>
93+
<div class="input-group">
94+
<span class="input-group-text text-muted small">…/formulaires/don/</span>
95+
<input type="text" class="form-control" id="form_id" name="form_slug"
96+
value="{{ formSlug }}" placeholder="mon-formulaire-de-don" required>
97+
</div>
98+
<div class="invalid-feedback">Le slug du formulaire est requis.</div>
99+
</div>
100+
<div class="mb-4">
101+
<label for="title" class="form-label fw-semibold">
102+
<span class="me-1">🎯</span> Titre du stream
103+
</label>
104+
<input type="text" class="form-control" id="title" name="title"
105+
value="{{ title }}" placeholder="ex : Stream caritatif Juin 2025" required>
106+
<div class="invalid-feedback">Le titre est requis.</div>
107+
</div>
108+
<div class="d-grid">
109+
<button type="submit" class="btn btn-success btn-lg fw-semibold" name="create_charity_stream">
110+
✅ Créer le stream
111+
</button>
112+
</div>
113+
</form>
114+
</div>
115+
116+
{# == MODE DEPUIS MON ASSO == #}
117+
<div id="modeAsso" style="display:none">
118+
119+
{# Stepper #}
120+
<div class="d-flex align-items-center mb-4" id="assoStepper">
121+
<div class="asso-step active" id="assoStep1Indicator">
122+
<div class="step-bubble">1</div>
123+
<div class="step-label">Connexion</div>
124+
</div>
125+
<div class="step-line flex-grow-1"></div>
126+
<div class="asso-step" id="assoStep2Indicator">
127+
<div class="step-bubble">2</div>
128+
<div class="step-label">Formulaire</div>
129+
</div>
130+
</div>
131+
132+
{# Étape 1 : connexion OAuth #}
133+
<div id="stepConnect" class="text-center py-2">
134+
<div class="mb-3 p-4 rounded-3 bg-body-tertiary">
135+
<div class="fs-1 mb-2">🔑</div>
136+
<p class="text-muted mb-0">Connectez votre association HelloAsso pour récupérer automatiquement vos formulaires de don.</p>
137+
</div>
138+
<button type="button" class="btn btn-primary btn-lg px-5 fw-semibold" id="connectAssoBtn" onclick="connectAsso()">
139+
Connecter mon asso
140+
</button>
141+
<div id="connectSpinner" class="mt-3" style="display:none">
142+
<div class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></div>
143+
<span class="ms-2 text-muted small">Ouverture de la fenêtre d'autorisation…</span>
144+
</div>
145+
</div>
146+
147+
{# Étape 2 : sélection du formulaire #}
148+
<div id="stepForms" style="display:none">
149+
<form action="/admin/stream" method="POST" class="needs-validation" novalidate>
150+
151+
<div class="alert alert-success d-flex align-items-center gap-2 mb-4 py-2">
152+
<span class="fs-5">✅</span>
153+
<span>Association connectée : <strong id="assoSlugDisplay"></strong></span>
154+
</div>
155+
156+
{% if events %}
157+
<div class="mb-3">
158+
<label for="asso_parent_event" class="form-label fw-semibold">
159+
<span class="me-1">📅</span> Évènement parent
160+
<span class="text-muted fw-normal small ms-1">(optionnel)</span>
161+
</label>
162+
<select class="form-select" id="asso_parent_event" name="parent_event">
163+
<option value="">— Aucun évènement —</option>
164+
{% for event in events %}
165+
<option value="{{ event.id }}">{{ event.title }}</option>
166+
{% endfor %}
167+
</select>
168+
</div>
169+
<div class="mb-3 p-3 rounded-3 bg-body-tertiary">
170+
<div class="form-check form-switch mb-0">
171+
<input class="form-check-input" type="checkbox" role="switch" id="asso_parent_style" name="parent_style">
172+
<label for="asso_parent_style" class="form-check-label">
173+
Appliquer le même style que l'évènement parent ?
174+
</label>
175+
</div>
176+
</div>
177+
{% endif %}
178+
179+
{# Email : visible pour admin, hidden pour user #}
180+
{% if isAdmin %}
181+
<div class="mb-3">
182+
<label for="asso_owner_email" class="form-label fw-semibold">
183+
<span class="me-1">📧</span> Email du propriétaire
184+
</label>
185+
<input type="email" class="form-control" id="asso_owner_email" name="owner_email"
186+
value="{{ currentUser.email }}" placeholder="utilisateur@exemple.com" required>
187+
<div class="invalid-feedback">Veuillez saisir un email valide.</div>
188+
</div>
189+
{% else %}
190+
<input type="hidden" name="owner_email" value="{{ currentUser.email }}">
191+
{% endif %}
192+
193+
<input type="hidden" id="asso_organization_slug" name="organization_slug">
194+
195+
<div class="mb-3">
196+
<label for="asso_form_slug" class="form-label fw-semibold">
197+
<span class="me-1">📋</span> Formulaire de don
198+
</label>
199+
<select class="form-select" id="asso_form_slug" name="form_slug" required>
200+
<option value="">— Sélectionner un formulaire —</option>
201+
</select>
202+
<div class="invalid-feedback">Veuillez sélectionner un formulaire.</div>
203+
</div>
204+
205+
<div class="mb-4">
206+
<label for="asso_title" class="form-label fw-semibold">
207+
<span class="me-1">🎯</span> Titre du stream
208+
</label>
209+
<input type="text" class="form-control" id="asso_title" name="title"
210+
placeholder="ex : Stream caritatif Juin 2025" required>
211+
<div class="form-text text-muted small">Pré-rempli à partir du formulaire sélectionné.</div>
212+
<div class="invalid-feedback">Le titre est requis.</div>
213+
</div>
214+
215+
<div class="d-grid">
216+
<button type="submit" class="btn btn-success btn-lg fw-semibold" name="create_charity_stream">
217+
✅ Créer le stream
218+
</button>
219+
</div>
220+
</form>
221+
</div>
222+
223+
</div>
224+
225+
</div>
226+
</div>
227+
</div>
228+
</div>
229+
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
{# ── Scripts partagés pour la création de stream ──
2+
Variables attendues : openCreateStream, selectedEventId, openCreateEvent
3+
#}
4+
<script>
5+
document.addEventListener('DOMContentLoaded', function() {
6+
/* ── Auto-ouverture de la modal de création de stream ── */
7+
{% if openCreateStream %}
8+
var createStreamModal = document.getElementById('createStreamModal');
9+
if (createStreamModal) {
10+
var modal = new bootstrap.Modal(createStreamModal);
11+
modal.show();
12+
{% if selectedEventId %}
13+
var parentEventSelect = document.getElementById('parent_event');
14+
if (parentEventSelect) parentEventSelect.value = '{{ selectedEventId }}';
15+
var assoParentSelect = document.getElementById('asso_parent_event');
16+
if (assoParentSelect) assoParentSelect.value = '{{ selectedEventId }}';
17+
{% endif %}
18+
}
19+
{% endif %}
20+
21+
{% if openCreateEvent %}
22+
var createEventModal = document.getElementById('createEventModal');
23+
if (createEventModal) {
24+
var modal = new bootstrap.Modal(createEventModal);
25+
modal.show();
26+
}
27+
{% endif %}
28+
29+
/* ── Validation Bootstrap ── */
30+
document.querySelectorAll('.needs-validation').forEach(function(form) {
31+
form.addEventListener('submit', function(e) {
32+
if (!form.checkValidity()) {
33+
e.preventDefault();
34+
e.stopPropagation();
35+
}
36+
form.classList.add('was-validated');
37+
}, true);
38+
});
39+
40+
/* ── Init tooltips Bootstrap ── */
41+
var tooltipEls = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
42+
tooltipEls.forEach(function(el) { new bootstrap.Tooltip(el); });
43+
44+
/* ── Auto-show toast ── */
45+
var toastEl = document.getElementById('liveToast');
46+
if (toastEl) { new bootstrap.Toast(toastEl).show(); }
47+
});
48+
49+
/* ── Sélection du mode (cartes cliquables) ── */
50+
function selectStreamMode(mode) {
51+
var isAsso = (mode === 'asso');
52+
document.getElementById('modeManuel').style.display = isAsso ? 'none' : 'block';
53+
document.getElementById('modeAsso').style.display = isAsso ? 'block' : 'none';
54+
document.getElementById('cardManuel').classList.toggle('active', !isAsso);
55+
document.getElementById('cardAsso').classList.toggle('active', isAsso);
56+
}
57+
58+
function confirmDelete(event) {
59+
if (!confirm("Êtes-vous sûr de vouloir supprimer cet élément ? Cette action est irréversible.")) {
60+
event.preventDefault();
61+
}
62+
}
63+
64+
/* ── Stepper helpers ── */
65+
function setStepActive(stepNum) {
66+
var s1 = document.getElementById('assoStep1Indicator');
67+
var s2 = document.getElementById('assoStep2Indicator');
68+
if (stepNum === 1) {
69+
s1.className = 'asso-step active';
70+
s2.className = 'asso-step';
71+
} else {
72+
s1.className = 'asso-step done';
73+
s2.className = 'asso-step active';
74+
}
75+
}
76+
77+
/* ── OAuth HelloAsso ── */
78+
function connectAsso() {
79+
var btn = document.getElementById('connectAssoBtn');
80+
var spinner = document.getElementById('connectSpinner');
81+
btn.disabled = true;
82+
spinner.style.display = 'block';
83+
84+
fetch('/admin/stream/init-auth')
85+
.then(function(r) {
86+
if (!r.ok) throw new Error('Erreur réseau');
87+
return r.json();
88+
})
89+
.then(function(data) {
90+
var authWindow = window.open(data.url, 'ha_auth_stream', 'width=900,height=700,noopener=0');
91+
if (!authWindow) {
92+
alert('Veuillez autoriser les popups pour ce site afin de connecter votre association.');
93+
btn.disabled = false;
94+
spinner.style.display = 'none';
95+
}
96+
})
97+
.catch(function() {
98+
btn.disabled = false;
99+
spinner.style.display = 'none';
100+
alert('Une erreur est survenue lors de l\'initialisation de l\'autorisation.');
101+
});
102+
}
103+
104+
/* ── Écoute du callback OAuth ── */
105+
window.addEventListener('message', function(event) {
106+
if (event.origin !== window.location.origin) return;
107+
if (!event.data || event.data.type !== 'ha_stream_auth_success') return;
108+
109+
var organizationSlug = event.data.organizationSlug;
110+
var forms = event.data.forms || [];
111+
112+
document.getElementById('asso_organization_slug').value = organizationSlug;
113+
document.getElementById('assoSlugDisplay').textContent = organizationSlug;
114+
115+
var formSelect = document.getElementById('asso_form_slug');
116+
formSelect.innerHTML = '<option value="">— Sélectionner un formulaire —</option>';
117+
forms.forEach(function(form) {
118+
var slug = form.slug || form.formSlug || '';
119+
var label = form.privateTitle || form.title || slug;
120+
var opt = document.createElement('option');
121+
opt.value = slug;
122+
opt.textContent = label;
123+
opt.dataset.title = label;
124+
formSelect.appendChild(opt);
125+
});
126+
127+
formSelect.addEventListener('change', function() {
128+
var titleInput = document.getElementById('asso_title');
129+
var selectedOpt = this.options[this.selectedIndex];
130+
titleInput.value = selectedOpt && selectedOpt.dataset.title ? selectedOpt.dataset.title : '';
131+
});
132+
133+
setStepActive(2);
134+
document.getElementById('stepConnect').style.display = 'none';
135+
document.getElementById('stepForms').style.display = 'block';
136+
});
137+
</script>
138+

0 commit comments

Comments
 (0)