From 2d7a8367d48028d1ea8a2394ec4f7e8c3b2167b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o?= Date: Tue, 12 May 2026 11:09:50 +0200 Subject: [PATCH 1/4] refactor --- migrations/10-add-widget-card.sql | 24 ++ public/dist/.vite/manifest.json | 24 ++ public/dist/admin.min.js | 2 +- public/dist/alert.min.js | 2 +- public/dist/cardEvent.min.js | 1 + public/dist/cardStream.min.js | 1 + public/dist/cardUtilities.min.js | 1 + public/dist/event.min.js | 2 +- public/dist/stream.min.js | 2 +- public/dist/utilities.min.js | 2 +- public/index.php | 4 + src/Assets/js/admin.js | 128 ++++++-- src/Assets/js/cardEvent.js | 86 ++++++ src/Assets/js/cardStream.js | 86 ++++++ src/Assets/js/cardUtilities.js | 19 ++ src/Controllers/AdminController.php | 85 +++--- src/Controllers/ApiController.php | 5 +- src/Controllers/LoginController.php | 20 +- src/Controllers/WidgetController.php | 391 ++++++++++++++++-------- src/Middlewares/AuthAdminMiddleware.php | 18 +- src/Middlewares/AuthMiddleware.php | 7 +- src/Models/WidgetCard.php | 24 ++ src/Repositories/EventRepository.php | 20 +- src/Repositories/StreamRepository.php | 34 ++- src/Repositories/UserRepository.php | 5 + src/Repositories/WidgetRepository.php | 186 +++++++---- src/Services/ApiWrapper.php | 82 +++-- src/views/event/edit.html.twig | 223 ++++++++++---- src/views/stream/edit.html.twig | 345 ++++++++++++++------- src/views/widget/card.html.twig | 180 +++++++++++ 30 files changed, 1521 insertions(+), 488 deletions(-) create mode 100644 migrations/10-add-widget-card.sql create mode 100644 public/dist/cardEvent.min.js create mode 100644 public/dist/cardStream.min.js create mode 100644 public/dist/cardUtilities.min.js create mode 100644 src/Assets/js/cardEvent.js create mode 100644 src/Assets/js/cardStream.js create mode 100644 src/Assets/js/cardUtilities.js create mode 100644 src/Models/WidgetCard.php create mode 100644 src/views/widget/card.html.twig diff --git a/migrations/10-add-widget-card.sql b/migrations/10-add-widget-card.sql new file mode 100644 index 0000000..a702a1e --- /dev/null +++ b/migrations/10-add-widget-card.sql @@ -0,0 +1,24 @@ +CREATE TABLE {prefix}widget_card ( + id INT AUTO_INCREMENT PRIMARY KEY, + charity_stream_guid CHAR(32) NULL, + charity_event_guid CHAR(32) NULL, + image VARCHAR(255) NULL, + tag VARCHAR(255) NOT NULL DEFAULT '', + title VARCHAR(500) NOT NULL DEFAULT '', + description TEXT NOT NULL, + goal INT NOT NULL DEFAULT 1000, + background_color CHAR(7) NOT NULL DEFAULT '#ffffff', + bar_color CHAR(7) NOT NULL DEFAULT '#2563eb', + bar_background_color CHAR(7) NOT NULL DEFAULT '#e5e7eb', + text_color CHAR(7) NOT NULL DEFAULT '#1a1a1a', + tag_color CHAR(7) NOT NULL DEFAULT '#166534', + tag_background_color CHAR(7) NOT NULL DEFAULT '#dcfce7', + cache_data JSON NULL, + creation_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + last_update DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6) NOT NULL, + CONSTRAINT fk_{prefix}charity_stream_guid_widget_card FOREIGN KEY (charity_stream_guid) REFERENCES {prefix}charity_stream(guid), + CONSTRAINT fk_{prefix}charity_event_guid_widget_card FOREIGN KEY (charity_event_guid) REFERENCES {prefix}charity_event(guid), + INDEX idx_charity_stream_guid (charity_stream_guid), + INDEX idx_charity_event_guid (charity_event_guid) +); + diff --git a/public/dist/.vite/manifest.json b/public/dist/.vite/manifest.json index 634dac9..7ba3d6b 100644 --- a/public/dist/.vite/manifest.json +++ b/public/dist/.vite/manifest.json @@ -27,6 +27,30 @@ "assets/app-MiSapeh1.css" ] }, + "src/Assets/js/cardEvent.js": { + "file": "cardEvent.min.js", + "name": "cardEvent", + "src": "src/Assets/js/cardEvent.js", + "isEntry": true, + "imports": [ + "_countUp.min.bkv6uBOp.min.js" + ] + }, + "src/Assets/js/cardStream.js": { + "file": "cardStream.min.js", + "name": "cardStream", + "src": "src/Assets/js/cardStream.js", + "isEntry": true, + "imports": [ + "_countUp.min.bkv6uBOp.min.js" + ] + }, + "src/Assets/js/cardUtilities.js": { + "file": "cardUtilities.min.js", + "name": "cardUtilities", + "src": "src/Assets/js/cardUtilities.js", + "isEntry": true + }, "src/Assets/js/event.js": { "file": "event.min.js", "name": "event", diff --git a/public/dist/admin.min.js b/public/dist/admin.min.js index d7224a5..85ba14f 100644 --- a/public/dist/admin.min.js +++ b/public/dist/admin.min.js @@ -1 +1 @@ -import{d as r}from"./alert.min.js";t();function t(){var d=document.querySelector(".back"),n=document.querySelector(".front"),l=document.getElementById("back-title"),a=document.getElementById("front-title"),e=document.getElementById("goal");d.style.backgroundColor=document.getElementById("background_color").value,n.style.backgroundColor=document.getElementById("bar_color").value,d.style.color=document.getElementById("text_color_main").value,n.style.color=document.getElementById("text_color_alt").value,l.textContent=document.getElementById("text_content").value,a.textContent=document.getElementById("text_content").value,e.textContent=document.getElementById("goal").value;var o=e.value/2;document.getElementById("back-goal-total").textContent=e.value+" €",document.getElementById("front-goal-total").textContent=e.value+" €",document.getElementById("back-goal-current").textContent=o+" €",document.getElementById("front-goal-current").textContent=o+" €",n.style.width=o/e*100+"%"}var c=document.getElementById("donationBarForm");c&&(document.getElementById("text_color_main").addEventListener("input",t),document.getElementById("text_color_alt").addEventListener("input",t),document.getElementById("text_content").addEventListener("input",t),document.getElementById("bar_color").addEventListener("input",t),document.getElementById("background_color").addEventListener("input",t),document.getElementById("goal").addEventListener("input",t));var m=document.getElementById("alertBoxForm");m&&document.getElementById("previewBtn").addEventListener("click",function(){r("test de pseudo","test de message","1000")}); +import{d as s}from"./alert.min.js";function a(){var l=document.querySelector(".back"),e=document.querySelector(".front"),t=document.getElementById("back-title"),d=document.getElementById("front-title"),r=document.getElementById("goal");l.style.backgroundColor=document.getElementById("background_color").value,e.style.backgroundColor=document.getElementById("bar_color").value,l.style.color=document.getElementById("text_color_main").value,e.style.color=document.getElementById("text_color_alt").value,t.textContent=document.getElementById("text_content").value,d.textContent=document.getElementById("text_content").value;var o=parseFloat(r.value)||0,n=o/2;document.getElementById("back-goal-total").textContent=o+" €",document.getElementById("front-goal-total").textContent=o+" €",document.getElementById("back-goal-current").textContent=n+" €",document.getElementById("front-goal-current").textContent=n+" €",e.style.width=o>0?n/o*100+"%":"0%"}var f=document.getElementById("donationBarForm");f&&(a(),document.getElementById("text_color_main").addEventListener("input",a),document.getElementById("text_color_alt").addEventListener("input",a),document.getElementById("text_content").addEventListener("input",a),document.getElementById("bar_color").addEventListener("input",a),document.getElementById("background_color").addEventListener("input",a),document.getElementById("goal").addEventListener("input",a));var b=document.getElementById("alertBoxForm");b&&document.getElementById("previewBtn").addEventListener("click",function(){s("test de pseudo","test de message","1000")});var x=document.getElementById("cardWidgetForm");if(x){let l=function(){var e=document.getElementById("cardPreview"),t=document.getElementById("cardPreviewTag"),d=document.getElementById("cardPreviewTitle"),r=document.getElementById("cardPreviewDesc"),o=document.getElementById("cardPreviewAmount"),n=document.getElementById("cardPreviewBarFill"),c=document.getElementById("cardPreviewGoal"),m=document.getElementById("cardPreviewPct"),i=document.getElementById("card_background_color").value,v=document.getElementById("card_text_color").value,y=document.getElementById("card_bar_color").value,E=document.getElementById("card_bar_background_color").value,B=document.getElementById("card_tag_color").value,_=document.getElementById("card_tag_background_color").value,u=parseFloat(document.getElementById("card_goal").value)||1;e&&(e.style.backgroundColor=i,e.style.color=v),t&&(t.style.color=B,t.style.backgroundColor=_,t.textContent="✏️ "+(document.getElementById("card_tag").value||"")),d&&(d.textContent=document.getElementById("card_title").value||""),r&&(r.textContent=document.getElementById("card_description").value||"");var I=u/2;o&&(o.textContent=I+" €"),n&&(n.style.backgroundColor=y,n.style.width="50%",n.parentElement.style.backgroundColor=E),c&&(c.innerHTML="Objectif : "+u+" €"),m&&(m.textContent="50%")};l(),["card_tag","card_title","card_description","card_goal","card_background_color","card_text_color","card_bar_color","card_bar_background_color","card_tag_color","card_tag_background_color"].forEach(function(e){var t=document.getElementById(e);t&&t.addEventListener("input",l)});var g=document.getElementById("card_image");g&&g.addEventListener("change",function(){var e=this.files[0];if(e){var t=new FileReader;t.onload=function(d){var r=document.getElementById("cardPreviewImage");r&&(r.style.backgroundImage="url("+d.target.result+")")},t.readAsDataURL(e)}})} diff --git a/public/dist/alert.min.js b/public/dist/alert.min.js index fca6a5c..e9a7589 100644 --- a/public/dist/alert.min.js +++ b/public/dist/alert.min.js @@ -1 +1 @@ -var l=[],d=!1,i=!1;u();setInterval(u,1e4);function w(t,s,r){s=s.substring(0,255),l.push({pseudo:t,message:s,amount:r}),d||c()}function c(){if(l.length===0){d=!1;return}d=!0;const t=l.shift(),{pseudo:s,message:r,amount:m}=t;var n=document.querySelector(".widget-alert-box");n.innerHTML="";let e;window.image.endsWith(".mp4")?(e=document.createElement("video"),e.src=window.image,e.autoplay=!0,e.loop=!0,e.muted=!1,e.style.maxWidth="100%",e.classList.add("fade")):(e=document.createElement("img"),e.src=window.image,e.style.maxWidth="100%",e.classList.add("fade")),n.appendChild(e);let p=new Intl.NumberFormat("fr-FR",{style:"currency",currency:"EUR"});var a=document.createElement("p");a.innerHTML=window.message_template.replace("{pseudo}",s).replace("{message}",r).replace("{amount}",p.format(m/100)),a.style.marginTop="10px",a.classList.add("fade"),n.appendChild(a);var o=new Audio(window.sound);setTimeout(function(){e.classList.add("show"),a.classList.add("show"),o.volume=window.sound_volume,o.play()},100),setTimeout(function(){e.classList.remove("show"),a.classList.remove("show"),o.pause(),o.currentTime=0,n.innerHTML="",c()},window.alert_duration)}async function u(){if(window.charityStreamId){if(i){console.warn("Un appel est déjà en cours, ignoré");return}i=!0;try{const t=await fetch(`/widget-stream-alert/${window.charityStreamId}/fetch`);t.ok?(await t.json()).donations.forEach(r=>{w(r.pseudo,r.message,r.amount)}):console.error("Erreur lors de la récupération des données de donation:",t.status)}catch(t){console.error("Erreur réseau lors de la récupération des données:",t)}finally{i=!1}}}export{w as d}; +var l=[],c=!1,i=!1;m();setInterval(m,1e4);function g(t,a,r){a=a.substring(0,255),l.push({pseudo:t,message:a,amount:r}),c||u()}function u(){if(l.length===0){c=!1;return}c=!0;const t=l.shift(),{pseudo:a,message:r,amount:p}=t;var n=document.querySelector(".widget-alert-box");n.innerHTML="";let e;window.image.endsWith(".mp4")?(e=document.createElement("video"),e.src=window.image,e.autoplay=!0,e.loop=!0,e.muted=!1,e.style.maxWidth="100%",e.classList.add("fade")):(e=document.createElement("img"),e.src=window.image,e.style.maxWidth="100%",e.classList.add("fade")),n.appendChild(e);let w=new Intl.NumberFormat("fr-FR",{style:"currency",currency:"EUR"});var s=document.createElement("p");const d=f=>String(f).replace(/&/g,"&").replace(//g,">").replace(/"/g,""");s.innerHTML=window.message_template.replace("{pseudo}",d(a)).replace("{message}",d(r)).replace("{amount}",w.format(p/100)),s.style.marginTop="10px",s.classList.add("fade"),n.appendChild(s);var o=new Audio(window.sound);setTimeout(function(){e.classList.add("show"),s.classList.add("show"),o.volume=window.sound_volume,o.play()},100),setTimeout(function(){e.classList.remove("show"),s.classList.remove("show"),o.pause(),o.currentTime=0,n.innerHTML="",u()},window.alert_duration)}async function m(){if(window.charityStreamId){if(i){console.warn("Un appel est déjà en cours, ignoré");return}i=!0;try{const t=await fetch(`/widget-stream-alert/${window.charityStreamId}/fetch`);t.ok?(await t.json()).donations.forEach(r=>{g(r.pseudo,r.message,r.amount)}):console.error("Erreur lors de la récupération des données de donation:",t.status)}catch(t){console.error("Erreur réseau lors de la récupération des données:",t)}finally{i=!1}}}export{g as d}; diff --git a/public/dist/cardEvent.min.js b/public/dist/cardEvent.min.js new file mode 100644 index 0000000..96cfeff --- /dev/null +++ b/public/dist/cardEvent.min.js @@ -0,0 +1 @@ +import{i as o}from"./countUp.min.bkv6uBOp.min.js";let n,r=!1;if(typeof o<"u"&&o){const t={separator:" ",decimal:",",suffix:" €"};n=new o("card-amount",window.currentAmount/100,t)}n&&n.start();l();u();setInterval(u,1e4);async function u(){if(r){console.warn("Un appel est déjà en cours, ignoré");return}r=!0;try{const t=await fetch(`/widget-event-card/${window.charityEventId}/fetch`);if(t.ok){const e=await t.json();window.currentAmount=e.amount,window.donorCount=e.donors??window.donorCount,l()}else console.error("Erreur lors de la récupération des données:",t.status)}catch(t){console.error("Erreur réseau:",t)}finally{r=!1}}function l(){const t=window.currentAmount/100,e=window.goalAmount||1,a=Math.min(100,Math.round(t/e*100)),c=document.getElementById("card-bar-fill"),d=document.getElementById("card-percentage"),i=document.getElementById("card-donors");if(n)n.update(t);else{const s=document.getElementById("card-amount");s&&(s.textContent=t.toLocaleString("fr-FR")+" €")}c&&(c.style.width=a+"%"),d&&(d.textContent=a+"%"),i&&(i.textContent=window.donorCount??0)} diff --git a/public/dist/cardStream.min.js b/public/dist/cardStream.min.js new file mode 100644 index 0000000..ed969d2 --- /dev/null +++ b/public/dist/cardStream.min.js @@ -0,0 +1 @@ +import{i as e}from"./countUp.min.bkv6uBOp.min.js";let n,r=!1;if(typeof e<"u"&&e){const t={separator:" ",decimal:",",suffix:" €"};n=new e("card-amount",window.currentAmount/100,t)}n&&n.start();l();u();setInterval(u,1e4);async function u(){if(r){console.warn("Un appel est déjà en cours, ignoré");return}r=!0;try{const t=await fetch(`/widget-stream-card/${window.charityStreamId}/fetch`);if(t.ok){const o=await t.json();window.currentAmount=o.amount,window.donorCount=o.donors??window.donorCount,l()}else console.error("Erreur lors de la récupération des données:",t.status)}catch(t){console.error("Erreur réseau:",t)}finally{r=!1}}function l(){const t=window.currentAmount/100,o=window.goalAmount||1,a=Math.min(100,Math.round(t/o*100)),c=document.getElementById("card-bar-fill"),d=document.getElementById("card-percentage"),i=document.getElementById("card-donors");if(n)n.update(t);else{const s=document.getElementById("card-amount");s&&(s.textContent=t.toLocaleString("fr-FR")+" €")}c&&(c.style.width=a+"%"),d&&(d.textContent=a+"%"),i&&(i.textContent=window.donorCount??0)} diff --git a/public/dist/cardUtilities.min.js b/public/dist/cardUtilities.min.js new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/public/dist/cardUtilities.min.js @@ -0,0 +1 @@ + diff --git a/public/dist/event.min.js b/public/dist/event.min.js index 080e848..eca2e2e 100644 --- a/public/dist/event.min.js +++ b/public/dist/event.min.js @@ -1 +1 @@ -import{i as n}from"./countUp.min.bkv6uBOp.min.js";import{u as c}from"./utilities.min.js";let o,t,r=!1;if(typeof n<"u"&&n){const e={separator:" ",decimal:",",suffix:" €"};o=new n("back-goal-current",window.currentAmount,e),t=new n("front-goal-current",window.currentAmount,e)}a();setInterval(a,1e4);async function a(){if(r){console.warn("Un appel est déjà en cours, ignoré");return}r=!0;try{const e=await fetch(`/widget-event/${window.charityEventId}/fetch`);if(e.ok){const s=await e.json();window.currentAmount=s.amount,c(o,t)}else console.error("Erreur lors de la récupération des données de donation:",e.status)}catch(e){console.error("Erreur réseau lors de la récupération des données:",e)}finally{r=!1}} +import{i as n}from"./countUp.min.bkv6uBOp.min.js";import{u as a}from"./utilities.min.js";let r,o,t=!1;if(typeof n<"u"&&n){const e={separator:" ",decimal:",",suffix:" €"};r=new n("back-goal-current",window.currentAmount,e),o=new n("front-goal-current",window.currentAmount,e)}r&&o&&a(r,o);s();setInterval(s,1e4);async function s(){if(t){console.warn("Un appel est déjà en cours, ignoré");return}t=!0;try{const e=await fetch(`/widget-event/${window.charityEventId}/fetch`);if(e.ok){const c=await e.json();window.currentAmount=c.amount,a(r,o)}else console.error("Erreur lors de la récupération des données de donation:",e.status)}catch(e){console.error("Erreur réseau lors de la récupération des données:",e)}finally{t=!1}} diff --git a/public/dist/stream.min.js b/public/dist/stream.min.js index adb680b..dc62fba 100644 --- a/public/dist/stream.min.js +++ b/public/dist/stream.min.js @@ -1 +1 @@ -import{i as o}from"./countUp.min.bkv6uBOp.min.js";import{u as a}from"./utilities.min.js";let e,t,r=!1;if(typeof o<"u"&&o){const n={separator:" ",decimal:",",suffix:" €"};e=new o("back-goal-current",window.currentAmount,n),t=new o("front-goal-current",window.currentAmount,n)}a(e,t);s();setInterval(s,1e4);async function s(){if(r){console.warn("Un appel est déjà en cours, ignoré");return}r=!0;try{const n=await fetch(`/widget-stream-donation/${window.charityStreamId}/fetch`);if(n.ok){const i=await n.json();window.currentAmount=i.amount,a(e,t)}else console.error("Erreur lors de la récupération des données de donation:",n.status)}catch(n){console.error("Erreur réseau lors de la récupération des données:",n)}finally{r=!1}} +import{i as o}from"./countUp.min.bkv6uBOp.min.js";import{u as a}from"./utilities.min.js";let r,e,t=!1;if(typeof o<"u"&&o){const n={separator:" ",decimal:",",suffix:" €"};r=new o("back-goal-current",window.currentAmount,n),e=new o("front-goal-current",window.currentAmount,n)}r&&e&&a(r,e);s();setInterval(s,1e4);async function s(){if(t){console.warn("Un appel est déjà en cours, ignoré");return}t=!0;try{const n=await fetch(`/widget-stream-donation/${window.charityStreamId}/fetch`);if(n.ok){const i=await n.json();window.currentAmount=i.amount,a(r,e)}else console.error("Erreur lors de la récupération des données de donation:",n.status)}catch(n){console.error("Erreur réseau lors de la récupération des données:",n)}finally{t=!1}} diff --git a/public/dist/utilities.min.js b/public/dist/utilities.min.js index 99f193a..52b3b7e 100644 --- a/public/dist/utilities.min.js +++ b/public/dist/utilities.min.js @@ -1 +1 @@ -function u(n,e){const t=window.currentAmount/100,o=Math.min(100,t/window.goalAmount*100);n.update(t),e.update(t),document.querySelector("div.front").style["-webkit-clip-path"]="inset(0 "+(100-o)+"% 0 0 round 999px)"}export{u}; +function i(n,e){const t=window.currentAmount/100,o=Math.min(100,t/window.goalAmount*100);n&&n.update(t),e&&e.update(t),document.querySelector("div.front").style["-webkit-clip-path"]="inset(0 "+(100-o)+"% 0 0 round 999px)"}export{i as u}; diff --git a/public/index.php b/public/index.php index 959c545..0fb7e9a 100644 --- a/public/index.php +++ b/public/index.php @@ -220,5 +220,9 @@ function (\Psr\Http\Message\ServerRequestInterface $request, \Throwable $excepti $app->get('/widget-stream-donation/{id}/fetch', [WidgetController::class, 'widgetDonationFetch'])->setName('app_stream_widget_donation_fetch'); $app->get('/widget-event/{id}', [WidgetController::class, 'widgetEventDonation'])->setName('app_event_widget_donation'); $app->get('/widget-event/{id}/fetch', [WidgetController::class, 'widgetEventDonationFetch'])->setName('app_event_widget_donation_fetch'); +$app->get('/widget-stream-card/{id}', [WidgetController::class, 'widgetStreamCard'])->setName('app_stream_widget_card'); +$app->get('/widget-stream-card/{id}/fetch', [WidgetController::class, 'widgetStreamCardFetch'])->setName('app_stream_widget_card_fetch'); +$app->get('/widget-event-card/{id}', [WidgetController::class, 'widgetEventCard'])->setName('app_event_widget_card'); +$app->get('/widget-event-card/{id}/fetch', [WidgetController::class, 'widgetEventCardFetch'])->setName('app_event_widget_card_fetch'); $app->run(); diff --git a/src/Assets/js/admin.js b/src/Assets/js/admin.js index 1c2bd62..3fe9fa6 100644 --- a/src/Assets/js/admin.js +++ b/src/Assets/js/admin.js @@ -1,11 +1,23 @@ import { displayAlertBox } from './alert.js'; +/** + * Lie une liste d'inputs à une fonction de mise à jour de preview. + * Exécute la fonction immédiatement, puis sur chaque événement 'input'. + */ +function bindPreviewInputs(inputIds, updateFn) { + updateFn(); + inputIds.forEach((id) => { + const el = document.getElementById(id); + if (el) el.addEventListener('input', updateFn); + }); +} + function updateDonationGoalPreview() { - var back = document.querySelector('.back'); - var front = document.querySelector('.front'); - var backTitle = document.getElementById('back-title'); - var frontTitle = document.getElementById('front-title'); - var goal = document.getElementById('goal'); + const back = document.querySelector('.back'); + const front = document.querySelector('.front'); + const backTitle = document.getElementById('back-title'); + const frontTitle = document.getElementById('front-title'); + const goal = document.getElementById('goal'); back.style.backgroundColor = document.getElementById('background_color').value; front.style.backgroundColor = document.getElementById('bar_color').value; @@ -13,34 +25,102 @@ function updateDonationGoalPreview() { back.style.color = document.getElementById('text_color_main').value; front.style.color = document.getElementById('text_color_alt').value; - backTitle.textContent = document.getElementById('text_content').value; - frontTitle.textContent = document.getElementById('text_content').value; + const textContent = document.getElementById('text_content').value; + backTitle.textContent = textContent; + frontTitle.textContent = textContent; - var goalValue = parseFloat(goal.value) || 0; - var currentDonation = goalValue / 2; + const goalValue = parseFloat(goal.value) || 0; + const currentDonation = goalValue / 2; - document.getElementById('back-goal-total').textContent = goalValue + ' €'; - document.getElementById('front-goal-total').textContent = goalValue + ' €'; - document.getElementById('back-goal-current').textContent = currentDonation + ' €'; - document.getElementById('front-goal-current').textContent = currentDonation + ' €'; + document.getElementById('back-goal-total').textContent = `${goalValue} €`; + document.getElementById('front-goal-total').textContent = `${goalValue} €`; + document.getElementById('back-goal-current').textContent = `${currentDonation} €`; + document.getElementById('front-goal-current').textContent = `${currentDonation} €`; - front.style.width = goalValue > 0 ? (currentDonation / goalValue * 100) + '%' : '0%'; + front.style.width = goalValue > 0 ? `${(currentDonation / goalValue) * 100}%` : '0%'; } -var donationBarForm = document.getElementById('donationBarForm'); +const donationBarForm = document.getElementById('donationBarForm'); if (donationBarForm) { - updateDonationGoalPreview(); - document.getElementById('text_color_main').addEventListener('input', updateDonationGoalPreview); - document.getElementById('text_color_alt').addEventListener('input', updateDonationGoalPreview); - document.getElementById('text_content').addEventListener('input', updateDonationGoalPreview); - document.getElementById('bar_color').addEventListener('input', updateDonationGoalPreview); - document.getElementById('background_color').addEventListener('input', updateDonationGoalPreview); - document.getElementById('goal').addEventListener('input', updateDonationGoalPreview); + bindPreviewInputs( + ['text_color_main', 'text_color_alt', 'text_content', 'bar_color', 'background_color', 'goal'], + updateDonationGoalPreview + ); } -var alertBoxForm = document.getElementById('alertBoxForm'); +const alertBoxForm = document.getElementById('alertBoxForm'); if (alertBoxForm) { - document.getElementById('previewBtn').addEventListener('click', function () { + document.getElementById('previewBtn').addEventListener('click', () => { displayAlertBox('test de pseudo', 'test de message', '1000'); }); } + +// ── Card widget live preview ────────────────────────────────── +const cardWidgetForm = document.getElementById('cardWidgetForm'); +if (cardWidgetForm) { + function updateCardPreview() { + const preview = document.getElementById('cardPreview'); + const tag = document.getElementById('cardPreviewTag'); + const title = document.getElementById('cardPreviewTitle'); + const desc = document.getElementById('cardPreviewDesc'); + const amount = document.getElementById('cardPreviewAmount'); + const barFill = document.getElementById('cardPreviewBarFill'); + const goalEl = document.getElementById('cardPreviewGoal'); + const pct = document.getElementById('cardPreviewPct'); + + const bgColor = document.getElementById('card_background_color').value; + const textColor = document.getElementById('card_text_color').value; + const barColor = document.getElementById('card_bar_color').value; + const barBgColor = document.getElementById('card_bar_background_color').value; + const tagColor = document.getElementById('card_tag_color').value; + const tagBgColor = document.getElementById('card_tag_background_color').value; + const goalValue = parseFloat(document.getElementById('card_goal').value) || 1; + + if (preview) { + preview.style.backgroundColor = bgColor; + preview.style.color = textColor; + } + if (tag) { + tag.style.color = tagColor; + tag.style.backgroundColor = tagBgColor; + tag.textContent = `✏️ ${document.getElementById('card_tag').value || ''}`; + } + if (title) title.textContent = document.getElementById('card_title').value || ''; + if (desc) desc.textContent = document.getElementById('card_description').value || ''; + + const halfGoal = goalValue / 2; + if (amount) amount.textContent = `${halfGoal} €`; + if (barFill) { + barFill.style.backgroundColor = barColor; + barFill.style.width = '50%'; + barFill.parentElement.style.backgroundColor = barBgColor; + } + if (goalEl) goalEl.innerHTML = `Objectif : ${goalValue} €`; + if (pct) pct.textContent = '50%'; + } + + bindPreviewInputs( + [ + 'card_tag', 'card_title', 'card_description', 'card_goal', + 'card_background_color', 'card_text_color', 'card_bar_color', + 'card_bar_background_color', 'card_tag_color', 'card_tag_background_color', + ], + updateCardPreview + ); + + // Live image preview + const cardImageInput = document.getElementById('card_image'); + if (cardImageInput) { + cardImageInput.addEventListener('change', function () { + const file = this.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const imgEl = document.getElementById('cardPreviewImage'); + if (imgEl) imgEl.style.backgroundImage = `url(${e.target.result})`; + }; + reader.readAsDataURL(file); + }); + } +} diff --git a/src/Assets/js/cardEvent.js b/src/Assets/js/cardEvent.js new file mode 100644 index 0000000..76dbc1f --- /dev/null +++ b/src/Assets/js/cardEvent.js @@ -0,0 +1,86 @@ +import { CountUp } from 'countup.js'; + +const FETCH_INTERVAL_MS = 10_000; +const CENTS_DIVISOR = 100; + +const COUNTUP_OPTIONS = { + separator: ' ', + decimal: ',', + suffix: ' €', +}; + +let counterAmount = null; +let isFetching = false; + +function initCounter() { + const initialAmount = window.currentAmount / CENTS_DIVISOR; + counterAmount = new CountUp('card-amount', initialAmount, COUNTUP_OPTIONS); + + if (counterAmount.error) { + console.error('CountUp init failed:', counterAmount.error); + counterAmount = null; + return; + } + + counterAmount.start(); +} + +function updateCardWidget() { + const currentAmountUnit = window.currentAmount / CENTS_DIVISOR; + const goal = window.goalAmount || 1; + const percentage = Math.min(100, Math.round((currentAmountUnit / goal) * 100)); + + const barFill = document.getElementById('card-bar-fill'); + const percentEl = document.getElementById('card-percentage'); + const donorsEl = document.getElementById('card-donors'); + + if (counterAmount) { + counterAmount.update(currentAmountUnit); + } else { + const amountEl = document.getElementById('card-amount'); + if (amountEl) { + amountEl.textContent = currentAmountUnit.toLocaleString('fr-FR') + ' €'; + } + } + + if (barFill) barFill.style.width = `${percentage}%`; + if (percentEl) percentEl.textContent = `${percentage}%`; + if (donorsEl) donorsEl.textContent = window.donorCount ?? 0; +} + +async function fetchEventData() { + if (isFetching) { + return; + } + + isFetching = true; + + try { + const response = await fetch(`/widget-event-card/${window.charityEventId}/fetch`); + + if (!response.ok) { + console.error('Failed to fetch event data:', response.status); + return; + } + + const { amount, donors } = await response.json(); + window.currentAmount = amount; + window.donorCount = donors ?? window.donorCount; + updateCardWidget(); + } catch (error) { + console.error('Network error:', error); + } finally { + isFetching = false; + } +} + +function init() { + initCounter(); + updateCardWidget(); + fetchEventData(); + setInterval(fetchEventData, FETCH_INTERVAL_MS); +} + +init(); + +export { fetchEventData }; diff --git a/src/Assets/js/cardStream.js b/src/Assets/js/cardStream.js new file mode 100644 index 0000000..bb8370f --- /dev/null +++ b/src/Assets/js/cardStream.js @@ -0,0 +1,86 @@ +import { CountUp } from 'countup.js'; + +const FETCH_INTERVAL_MS = 10_000; +const CENTS_DIVISOR = 100; + +const COUNTUP_OPTIONS = { + separator: ' ', + decimal: ',', + suffix: ' €', +}; + +let counterAmount = null; +let isFetching = false; + +function initCounter() { + const initialAmount = window.currentAmount / CENTS_DIVISOR; + counterAmount = new CountUp('card-amount', initialAmount, COUNTUP_OPTIONS); + + if (counterAmount.error) { + console.error('CountUp init failed:', counterAmount.error); + counterAmount = null; + return; + } + + counterAmount.start(); +} + +function updateCardWidget() { + const currentAmountUnit = window.currentAmount / CENTS_DIVISOR; + const goal = window.goalAmount || 1; + const percentage = Math.min(100, Math.round((currentAmountUnit / goal) * 100)); + + const barFill = document.getElementById('card-bar-fill'); + const percentEl = document.getElementById('card-percentage'); + const donorsEl = document.getElementById('card-donors'); + + if (counterAmount) { + counterAmount.update(currentAmountUnit); + } else { + const amountEl = document.getElementById('card-amount'); + if (amountEl) { + amountEl.textContent = currentAmountUnit.toLocaleString('fr-FR') + ' €'; + } + } + + if (barFill) barFill.style.width = `${percentage}%`; + if (percentEl) percentEl.textContent = `${percentage}%`; + if (donorsEl) donorsEl.textContent = window.donorCount ?? 0; +} + +async function fetchDonationData() { + if (isFetching) { + return; + } + + isFetching = true; + + try { + const response = await fetch(`/widget-stream-card/${window.charityStreamId}/fetch`); + + if (!response.ok) { + console.error('Failed to fetch donation data:', response.status); + return; + } + + const { amount, donors } = await response.json(); + window.currentAmount = amount; + window.donorCount = donors ?? window.donorCount; + updateCardWidget(); + } catch (error) { + console.error('Network error:', error); + } finally { + isFetching = false; + } +} + +function init() { + initCounter(); + updateCardWidget(); + fetchDonationData(); + setInterval(fetchDonationData, FETCH_INTERVAL_MS); +} + +init(); + +export { fetchDonationData }; diff --git a/src/Assets/js/cardUtilities.js b/src/Assets/js/cardUtilities.js new file mode 100644 index 0000000..2f330a4 --- /dev/null +++ b/src/Assets/js/cardUtilities.js @@ -0,0 +1,19 @@ +const CENTS_DIVISOR = 100; + +function updateCardWidget() { + const currentAmountUnit = window.currentAmount / CENTS_DIVISOR; + const goal = window.goalAmount || 1; + const percentage = Math.min(100, Math.round((currentAmountUnit / goal) * 100)); + + const amountEl = document.getElementById('card-amount'); + const barFill = document.getElementById('card-bar-fill'); + const percentEl = document.getElementById('card-percentage'); + const donorsEl = document.getElementById('card-donors'); + + if (amountEl) amountEl.textContent = currentAmountUnit.toLocaleString('fr-FR') + ' €'; + if (barFill) barFill.style.width = `${percentage}%`; + if (percentEl) percentEl.textContent = `${percentage}%`; + if (donorsEl) donorsEl.textContent = window.donorCount ?? 0; +} + +export default updateCardWidget; diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index a3ce996..a74fc83 100644 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -2,7 +2,6 @@ namespace App\Controllers; -use App\Models\AccessToken; use App\Repositories\AccessTokenRepository; use App\Repositories\AuthorizationCodeRepository; use App\Repositories\EventRepository; @@ -11,8 +10,6 @@ use App\Repositories\UserRepository; use App\Repositories\WidgetRepository; use App\Services\ApiWrapper; -use DateInterval; -use DateTime; use Exception; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -38,7 +35,7 @@ public function __construct( public function index(Request $request, Response $response): Response { $user = $request->getAttribute('user'); - if ($user->role == "ADMIN") { + if ($user->role === "ADMIN") { $streams = $this->streamRepository->selectList(); $events = $this->eventRepository->selectList(); } else { @@ -53,7 +50,7 @@ public function index(Request $request, Response $response): Response "currentUser" => $user, ]; - $template = $user->role == "ADMIN" ? 'stream/index-admin.html.twig' : 'stream/index.html.twig'; + $template = $user->role === "ADMIN" ? 'stream/index-admin.html.twig' : 'stream/index.html.twig'; return $this->view->render($response, $template, $data); } @@ -61,14 +58,8 @@ public function newEvent(Request $request, Response $response): Response { $data = $request->getParsedBody(); - $ownerEmail = $data['owner_email']; - $title = $data['title']; - - $user = $this->userRepository->select($ownerEmail); - if ($user == null) { - $user = $this->userRepository->insert($ownerEmail); - } - $event = $this->eventRepository->insert($title); + $user = $this->userRepository->findOrCreate($data['owner_email']); + $event = $this->eventRepository->insert($data['title']); $this->userRepository->insertRight($user, null, $event); $this->messages->addMessage('success', 'Évènement ajouté'); @@ -101,13 +92,17 @@ public function editEvent(Request $request, Response $response, array $args): Re $user = $request->getAttribute('user'); $event = $this->eventRepository->selectByUserAndGuid($user, $args['id']); $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $event->guid); + $cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $event->guid); $routeParser = RouteContext::fromRequest($request)->getRouteParser(); $data = [ "logged" => true, "event" => $event, "donationGoalWidget" => $donationGoalWidget, + "cardWidget" => $cardWidget, + "cardWidgetPictureUrl" => ($cardWidget && $cardWidget->image) ? $this->fileManager->getPictureUrl($cardWidget->image) : null, "widgetDonationGoalUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_donation', ["id" => $event->guid]), + "widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_card', ["id" => $event->guid]), ]; return $this->view->render($response, 'event/edit.html.twig', $data); @@ -116,12 +111,9 @@ public function editEvent(Request $request, Response $response, array $args): Re public function editEventPost(Request $request, Response $response, array $args): Response { $user = $request->getAttribute('user'); - $body = $request->getParsedBody(); $event = $this->eventRepository->selectByUserAndGuid($user, $args['id']); - if (isset($body['save_donation_goal'])) { - $this->widgetRepository->updateDonationWidget(null, $event->guid, $body); - } + $this->handleWidgetFormSave($request, null, $event->guid); $routeParser = RouteContext::fromRequest($request)->getRouteParser(); $url = $routeParser->urlFor('app_event_edit', ["id" => $event->guid]); @@ -136,22 +128,15 @@ public function newStream(Request $request, Response $response): Response $parentEvent = $data['parent_event'] ?? null; $parentStyle = isset($data['parent_style']); - $ownerEmail = $data['owner_email']; - $formSlug = $data['form_slug']; - $organizationSlug = $data['organization_slug']; - $title = $data['title']; - - $owner = $this->userRepository->select($ownerEmail); - if ($owner == null) { - $owner = $this->userRepository->insert($ownerEmail); - } + + $owner = $this->userRepository->findOrCreate($data['owner_email']); $event = null; if (!empty($parentEvent)) { $event = $this->eventRepository->selectByUserAndId($user, $parentEvent); } - $stream = $this->streamRepository->insert($formSlug, $organizationSlug, $title, $event->id ?? null); + $stream = $this->streamRepository->insert($data['form_slug'], $data['organization_slug'], $data['title'], $event->id ?? null); $this->userRepository->insertRight($owner, $stream, null); if ($event !== null && $parentStyle) { @@ -202,6 +187,7 @@ public function editStream(Request $request, Response $response, array $args): R $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid($guid, null); $alertBoxWidget = $this->widgetRepository->selectAlertWidgetByGuid($guid); + $cardWidget = $this->widgetRepository->selectCardWidgetByGuid($guid, null); $donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/formulaires/' . $charityStream->form_slug; $routeParser = RouteContext::fromRequest($request)->getRouteParser(); @@ -213,9 +199,12 @@ public function editStream(Request $request, Response $response, array $args): R "alertBoxWidget" => $alertBoxWidget, "alertBoxWidgetPictureUrl" => ($alertBoxWidget && $alertBoxWidget->image) ? $this->fileManager->getPictureUrl($alertBoxWidget->image) : null, "alertBoxWidgetSoundUrl" => ($alertBoxWidget && $alertBoxWidget->sound) ? $this->fileManager->getSoundUrl($alertBoxWidget->sound) : null, + "cardWidget" => $cardWidget, + "cardWidgetPictureUrl" => ($cardWidget && $cardWidget->image) ? $this->fileManager->getPictureUrl($cardWidget->image) : null, "donationUrl" => $donationUrl, "widgetDonationGoalUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_stream_widget_donation', ["id" => $guid]), "widgetAlertBoxUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_stream_widget_alert', ["id" => $guid]), + "widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_stream_widget_card', ["id" => $guid]), ]; return $this->view->render($response, 'stream/edit.html.twig', $data); @@ -224,13 +213,10 @@ public function editStream(Request $request, Response $response, array $args): R public function editStreamPost(Request $request, Response $response, array $args): Response { $user = $request->getAttribute('user'); - $body = $request->getParsedBody(); $charityStream = $this->streamRepository->selectByUserAndGuid($user, $args['id']); $guid = $charityStream->guid; - if (isset($body['save_donation_goal'])) { - $this->widgetRepository->updateDonationWidget($guid, null, $body); - } + $body = $request->getParsedBody(); if (isset($body['save_alert_box'])) { $uploadedFiles = $request->getUploadedFiles(); @@ -244,12 +230,34 @@ public function editStreamPost(Request $request, Response $response, array $args $this->widgetRepository->updateAlertWidget($guid, $body, $image, $sound); } + $this->handleWidgetFormSave($request, $guid, null); + $routeParser = RouteContext::fromRequest($request)->getRouteParser(); $url = $routeParser->urlFor('app_stream_edit', ["id" => $guid]); return $response->withHeader('Location', $url)->withStatus(302); } + /** + * Traite les sauvegardes communes des widgets donation goal et card widget. + */ + private function handleWidgetFormSave(Request $request, ?string $streamGuid, ?string $eventGuid): void + { + $body = $request->getParsedBody(); + + if (isset($body['save_donation_goal'])) { + $this->widgetRepository->updateDonationWidget($streamGuid, $eventGuid, $body); + } + + if (isset($body['save_card_widget'])) { + $uploadedFiles = $request->getUploadedFiles(); + $image = isset($uploadedFiles['card_image']) && $uploadedFiles['card_image']->getSize() > 0 + ? $this->fileManager->uploadPicture($uploadedFiles['card_image']) + : null; + $this->widgetRepository->updateCardWidget($streamGuid, $eventGuid, $body, $image); + } + } + /** * Génère l'URL d'autorisation OAuth HelloAsso pour connecter une association dans le cadre de la création d'un stream. * Retourne un JSON { "url": "..." } pour que le front puisse ouvrir la mire dans un nouvel onglet. @@ -309,20 +317,7 @@ public function streamAuthCallback(Request $request, Response $response): Respon $organizationSlug = $tokenData['organization_slug']; // Stocker / mettre à jour les tokens - $existingToken = $this->accessTokenRepository->selectBySlug($organizationSlug); - - $token = new AccessToken(); - $token->access_token = $tokenData['access_token']; - $token->refresh_token = $tokenData['refresh_token']; - $token->organization_slug = $organizationSlug; - $token->access_token_expires_at = (new DateTime())->add(new DateInterval('PT28M')); - $token->refresh_token_expires_at = (new DateTime())->add(new DateInterval('P28D')); - - if ($existingToken === null) { - $this->accessTokenRepository->insert($token); - } else { - $this->accessTokenRepository->update($token); - } + $this->apiWrapper->storeOrUpdateToken($tokenData); // Récupérer les formulaires de don $forms = $this->apiWrapper->getDonationForms($organizationSlug); diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php index 163f3ce..47e18ff 100644 --- a/src/Controllers/ApiController.php +++ b/src/Controllers/ApiController.php @@ -36,10 +36,7 @@ public function new(Request $request, Response $response): Response } $stream = $this->streamRepository->insert($formSlug, $organizationSlug, $title); - $user = $this->userRepository->select($ownerEmail); - if ($user == null) { - $user = $this->userRepository->insert($ownerEmail); - } + $user = $this->userRepository->findOrCreate($ownerEmail); $this->userRepository->insertRight($user, $stream, null); $this->userRepository->insertResetToken($user); diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index 0c80fec..ac63412 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -2,14 +2,11 @@ namespace App\Controllers; -use App\Models\AccessToken; use App\Repositories\AccessTokenRepository; use App\Repositories\AuthorizationCodeRepository; use App\Repositories\StreamRepository; use App\Repositories\UserRepository; use App\Services\ApiWrapper; -use DateTime; -use DateInterval; use Exception; use MailchimpTransactional\ApiClient; use Psr\Http\Message\ResponseInterface as Response; @@ -238,19 +235,10 @@ public function validateAuthPage(Request $request, Response $response): Response return $response->withStatus(400); } - $existingOrganizationToken = $this->accessTokenRepository->selectBySlug($tokenDataGrantAuthorization['organization_slug']); + $isNewToken = $this->accessTokenRepository->selectBySlug($tokenDataGrantAuthorization['organization_slug']) === null; + $this->apiWrapper->storeOrUpdateToken($tokenDataGrantAuthorization); - $token = new AccessToken(); - $token->access_token = $tokenDataGrantAuthorization['access_token']; - $token->refresh_token = $tokenDataGrantAuthorization['refresh_token']; - $token->organization_slug = $tokenDataGrantAuthorization['organization_slug']; - $token->access_token_expires_at = (new DateTime())->add(new DateInterval('PT28M')); - $token->refresh_token_expires_at = (new DateTime())->add(new DateInterval('P28D')); - - if ($existingOrganizationToken == null) { - - $this->accessTokenRepository->insert($token); - + if ($isNewToken) { $this->mailchimp->messages->send([ "message" => [ "from_email" => "contact@helloasso.io", @@ -264,8 +252,6 @@ public function validateAuthPage(Request $request, Response $response): Response ], ] ]); - } else { - $this->accessTokenRepository->update($token); } $response->getBody()->write('Votre compte ' . $tokenDataGrantAuthorization['organization_slug'] . ' à bien été lié à HelloAssoCharityStream, vous pouvez fermer cette page.'); diff --git a/src/Controllers/WidgetController.php b/src/Controllers/WidgetController.php index 3379697..8d62aa2 100644 --- a/src/Controllers/WidgetController.php +++ b/src/Controllers/WidgetController.php @@ -23,33 +23,27 @@ public function __construct( private WidgetRepository $widgetRepository, ) {} - public function widgetEventDonation(Request $request, Response $response, array $args): Response - { - $eventGuid = $args['id'] ?? ''; - if (!$eventGuid) { - throw new Exception("Event ID manquant ou incorrect."); - } + // ── Helpers ─────────────────────────────────────────────────── - $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $eventGuid); - if (!$donationGoalWidget) { - throw new Exception("Aucun widget trouvé pour le Event ID fourni."); - } - - $event = $this->eventRepository->selectByGuid($eventGuid); - if (!$event) { - throw new Exception("Event non trouvé."); - } + private function jsonResponse(Response $response, array $data, int $status = 200): Response + { + $response->getBody()->write(json_encode($data)); + return $response->withHeader('Content-Type', 'application/json')->withStatus($status); + } - $cacheData = $this->widgetRepository->selectEventDonationWidgetCacheData($event); - if (!$cacheData) { - $cacheData = [ - 'amount' => 0, - 'streams' => [] - ]; - } + private function jsonError(Response $response, string $message, int $status): Response + { + return $this->jsonResponse($response, ['error' => $message], $status); + } - $streams = $this->streamRepository->selectListByEvent($event); + /** + * Agrège les montants de tous les streams d'un event via le cache par stream. + * Retourne le cache mis à jour avec le total. + */ + private function aggregateEventStreams(array $streams, array $cacheData, bool $trackDonors = false): array + { $totalAmount = 0; + $totalDonors = 0; foreach ($streams as $stream) { $streamCache = null; @@ -63,11 +57,11 @@ public function widgetEventDonation(Request $request, Response $response, array unset($cachedStream); if (!$streamCache) { - $streamCache = [ - 'id' => $stream->id, - 'amount' => 0, - 'continuation_token' => null - ]; + $defaultCache = ['id' => $stream->id, 'amount' => 0, 'continuation_token' => null]; + if ($trackDonors) { + $defaultCache['donors'] = 0; + } + $streamCache = $defaultCache; $cacheData['streams'][] = &$streamCache; } @@ -81,19 +75,56 @@ public function widgetEventDonation(Request $request, Response $response, array $streamCache['amount'] = $result['amount']; $streamCache['continuation_token'] = $result['continuation_token']; + if ($trackDonors) { + $streamCache['donors'] = ($streamCache['donors'] ?? 0) + count($result['donations'] ?? []); + $totalDonors += $streamCache['donors']; + } + $totalAmount += $result['amount']; unset($streamCache); } - if ($cacheData['amount'] !== $totalAmount) { - $cacheData['amount'] = $totalAmount; + $cacheData['amount'] = $totalAmount; + if ($trackDonors) { + $cacheData['donors'] = $totalDonors; + } + + return $cacheData; + } + + // ── Event Donation ─────────────────────────────────────────── + public function widgetEventDonation(Request $request, Response $response, array $args): Response + { + $eventGuid = $args['id'] ?? ''; + if (!$eventGuid) { + throw new Exception("Event ID manquant ou incorrect."); + } + + $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $eventGuid); + if (!$donationGoalWidget) { + throw new Exception("Aucun widget trouvé pour le Event ID fourni."); + } + + $event = $this->eventRepository->selectByGuid($eventGuid); + if (!$event) { + throw new Exception("Event non trouvé."); + } + + $cacheData = $this->widgetRepository->selectEventDonationWidgetCacheData($event) + ?? ['amount' => 0, 'streams' => []]; + + $streams = $this->streamRepository->selectListByEvent($event); + $oldAmount = $cacheData['amount']; + $cacheData = $this->aggregateEventStreams($streams, $cacheData); + + if ($oldAmount !== $cacheData['amount']) { $this->widgetRepository->updateEventDonationWidgetCacheData($event->guid, $cacheData); } $data = [ "donationGoalWidget" => $donationGoalWidget, - "currentAmount" => $totalAmount, + "currentAmount" => $cacheData['amount'], "event" => 1 ]; @@ -104,74 +135,29 @@ public function widgetEventDonationFetch(Request $request, Response $response, a { $eventId = $args['id'] ?? ''; if (!$eventId) { - $response->getBody()->write(json_encode(['error' => 'Event ID manquant ou incorrect.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + return $this->jsonError($response, 'Event ID manquant ou incorrect.', 400); } $event = $this->eventRepository->selectByGuid($eventId); if (!$event) { - $response->getBody()->write(json_encode(['error' => 'Event non trouvé.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + return $this->jsonError($response, 'Event non trouvé.', 404); } - $cacheData = $this->widgetRepository->selectEventDonationWidgetCacheData($event); - if (!$cacheData) { - $cacheData = ['amount' => 0, 'streams' => []]; - } + $cacheData = $this->widgetRepository->selectEventDonationWidgetCacheData($event) + ?? ['amount' => 0, 'streams' => []]; try { $streams = $this->streamRepository->selectListByEvent($event); - $totalAmount = 0; - - foreach ($streams as $stream) { - $streamCache = null; - - foreach ($cacheData['streams'] as &$cachedStream) { - if ($cachedStream !== null && $cachedStream['id'] === $stream->id) { - $streamCache = &$cachedStream; - break; - } - } - unset($cachedStream); - - if (!$streamCache) { - $streamCache = [ - 'id' => $stream->id, - 'amount' => 0, - 'continuation_token' => null - ]; - $cacheData['streams'][] = &$streamCache; - } - - $result = $this->apiWrapper->getAllOrders( - $stream->organization_slug, - $stream->form_slug, - $streamCache['amount'], - $streamCache['continuation_token'] - ); - - $streamCache['amount'] = $result['amount']; - $streamCache['continuation_token'] = $result['continuation_token']; - - $totalAmount += $result['amount']; - unset($streamCache); - } - - if ($cacheData['amount'] !== $totalAmount) { - $cacheData['amount'] = $totalAmount; + $oldAmount = $cacheData['amount']; + $cacheData = $this->aggregateEventStreams($streams, $cacheData); + if ($oldAmount !== $cacheData['amount']) { $this->widgetRepository->updateEventDonationWidgetCacheData($event->guid, $cacheData); } - $result = [ - 'amount' => $totalAmount - ]; - - $response->getBody()->write(json_encode($result)); - return $response->withHeader('Content-Type', 'application/json'); + return $this->jsonResponse($response, ['amount' => $cacheData['amount']]); } catch (Exception $e) { - $response->getBody()->write(json_encode(['error' => 'Impossible de récupérer le montant'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + return $this->jsonError($response, 'Impossible de récupérer le montant', 500); } } @@ -180,9 +166,9 @@ public function widgetAlert(Request $request, Response $response, array $args): $charityStreamId = $args['id'] ?? ''; if (!$charityStreamId) { throw new Exception("Charity Stream ID manquant ou incorrect."); - } - - $alertBoxWidget = $this->widgetRepository->selectAlertWidgetByGuid($charityStreamId); + } + + $alertBoxWidget = $this->widgetRepository->selectAlertWidgetByGuid($charityStreamId); if (!$alertBoxWidget) { throw new Exception("Aucun widget trouvé pour le Charity Stream ID fourni."); } @@ -207,7 +193,7 @@ public function widgetAlert(Request $request, Response $response, array $args): $cacheData["continuation_token"], ); - if ($cacheData["continuation_token"] != $result['continuation_token']) { + if ($cacheData["continuation_token"] !== $result['continuation_token']) { $this->widgetRepository->updateAlertWidgetCacheData($charityStream->guid, [ "continuation_token" => $result['continuation_token'] ]); @@ -226,20 +212,16 @@ public function widgetAlertFetch(Request $request, Response $response, array $ar { $charityStreamId = $args['id'] ?? ''; if (!$charityStreamId) { - $response->getBody()->write(json_encode(['error' => 'Charity Stream ID manquant ou incorrect.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + return $this->jsonError($response, 'Charity Stream ID manquant ou incorrect.', 400); } $charityStream = $this->streamRepository->selectByGuid($charityStreamId); if (!$charityStream) { - $response->getBody()->write(json_encode(['error' => 'Charity Stream non trouvé.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + return $this->jsonError($response, 'Charity Stream non trouvé.', 404); } - $cacheData = $this->widgetRepository->selectAlertWidgetCacheData($charityStream); - if (!$cacheData) { - $cacheData = ['continuation_token' => '']; - } + $cacheData = $this->widgetRepository->selectAlertWidgetCacheData($charityStream) + ?? ['continuation_token' => '']; try { $result = $this->apiWrapper->getAllOrders( @@ -249,17 +231,15 @@ public function widgetAlertFetch(Request $request, Response $response, array $ar $cacheData['continuation_token'] ); - if ($cacheData['continuation_token'] != $result['continuation_token']) { + if ($cacheData['continuation_token'] !== $result['continuation_token']) { $this->widgetRepository->updateAlertWidgetCacheData($charityStream->guid, [ "continuation_token" => $result['continuation_token'] ]); } - $response->getBody()->write(json_encode($result)); - return $response->withHeader('Content-Type', 'application/json'); + return $this->jsonResponse($response, $result); } catch (Exception $e) { - $response->getBody()->write(json_encode(['error' => 'Impossible de récupérer les commandes.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + return $this->jsonError($response, 'Impossible de récupérer les commandes.', 500); } } @@ -278,13 +258,11 @@ public function widgetDonation(Request $request, Response $response, array $args $charityStream = $this->streamRepository->selectByGuid($streamGuid); if (!$charityStream) { throw new Exception("Charity Stream non trouvé."); - } - - $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream); - if (!$cacheData) { - $cacheData = ['amount' => 0, 'continuation_token' => '']; } + $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream) + ?? ['amount' => 0, 'continuation_token' => '']; + $result = $this->apiWrapper->getAllOrders( $charityStream->organization_slug, $charityStream->form_slug, @@ -292,7 +270,7 @@ public function widgetDonation(Request $request, Response $response, array $args $cacheData['continuation_token'], ); - if ($cacheData['continuation_token'] != $result['continuation_token'] || $cacheData['amount'] != $result['amount']) { + if ($cacheData['continuation_token'] !== $result['continuation_token'] || $cacheData['amount'] !== $result['amount']) { $this->widgetRepository->updateStreamDonationWidgetCacheData($charityStream->guid, [ "amount" => $result['amount'], "continuation_token" => $result['continuation_token'] @@ -312,20 +290,16 @@ public function widgetDonationFetch(Request $request, Response $response, array { $charityStreamId = $args['id'] ?? ''; if (!$charityStreamId) { - $response->getBody()->write(json_encode(['error' => 'Charity Stream ID manquant ou incorrect.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(400); + return $this->jsonError($response, 'Charity Stream ID manquant ou incorrect.', 400); } $charityStream = $this->streamRepository->selectByGuid($charityStreamId); if (!$charityStream) { - $response->getBody()->write(json_encode(['error' => 'Charity Stream non trouvé.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(404); + return $this->jsonError($response, 'Charity Stream non trouvé.', 404); } - $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream); - if (!$cacheData) { - $cacheData = ['amount' => 0, 'continuation_token' => '']; - } + $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream) + ?? ['amount' => 0, 'continuation_token' => '']; try { $result = $this->apiWrapper->getAllOrders( @@ -335,19 +309,192 @@ public function widgetDonationFetch(Request $request, Response $response, array $cacheData['continuation_token'] ); - if ($cacheData['continuation_token'] != $result['continuation_token'] - || $cacheData['amount'] != $result['amount']) { + if ($cacheData['continuation_token'] !== $result['continuation_token'] + || $cacheData['amount'] !== $result['amount']) { $this->widgetRepository->updateStreamDonationWidgetCacheData($charityStream->guid, [ "amount" => $result['amount'], "continuation_token" => $result['continuation_token'] ]); } - $response->getBody()->write(json_encode($result)); - return $response->withHeader('Content-Type', 'application/json'); + return $this->jsonResponse($response, $result); + } catch (Exception $e) { + return $this->jsonError($response, 'Impossible de récupérer les commandes.', 500); + } + } + + // ── Widget Card (Stream) ───────────────────────────────────── + + public function widgetStreamCard(Request $request, Response $response, array $args): Response + { + $streamGuid = $args['id'] ?? ''; + if (!$streamGuid) { + throw new Exception("Charity Stream ID manquant ou incorrect."); + } + + $cardWidget = $this->widgetRepository->selectCardWidgetByGuid($streamGuid, null); + if (!$cardWidget) { + throw new Exception("Aucun widget card trouvé pour le Charity Stream ID fourni."); + } + + $charityStream = $this->streamRepository->selectByGuid($streamGuid); + if (!$charityStream) { + throw new Exception("Charity Stream non trouvé."); + } + + $cacheData = $this->widgetRepository->selectStreamCardWidgetCacheData($charityStream) + ?? ['amount' => 0, 'donors' => 0, 'continuation_token' => '']; + + $result = $this->apiWrapper->getAllOrders( + $charityStream->organization_slug, + $charityStream->form_slug, + $cacheData['amount'], + $cacheData['continuation_token'], + ); + + $newDonors = count($result['donations'] ?? []); + $donors = ($cacheData['donors'] ?? 0) + $newDonors; + + if ($cacheData['continuation_token'] !== $result['continuation_token'] || $cacheData['amount'] !== $result['amount']) { + $this->widgetRepository->updateStreamCardWidgetCacheData($charityStream->guid, [ + "amount" => $result['amount'], + "donors" => $donors, + "continuation_token" => $result['continuation_token'] + ]); + } + + $currentAmount = $result['amount']; + $goal = $cardWidget->goal ?: 1; + $percentage = min(100, round(($currentAmount / 100) / $goal * 100)); + + $data = [ + "cardWidget" => $cardWidget, + "cardWidgetPictureUrl" => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, + "currentAmount" => $currentAmount, + "donorCount" => $donors, + "percentage" => $percentage, + "stream" => 1 + ]; + + return $this->view->render($response, 'widget/card.html.twig', $data); + } + + public function widgetStreamCardFetch(Request $request, Response $response, array $args): Response + { + $charityStreamId = $args['id'] ?? ''; + if (!$charityStreamId) { + return $this->jsonError($response, 'Charity Stream ID manquant ou incorrect.', 400); + } + + $charityStream = $this->streamRepository->selectByGuid($charityStreamId); + if (!$charityStream) { + return $this->jsonError($response, 'Charity Stream non trouvé.', 404); + } + + $cacheData = $this->widgetRepository->selectStreamCardWidgetCacheData($charityStream) + ?? ['amount' => 0, 'donors' => 0, 'continuation_token' => '']; + + try { + $result = $this->apiWrapper->getAllOrders( + $charityStream->organization_slug, + $charityStream->form_slug, + $cacheData['amount'], + $cacheData['continuation_token'] + ); + + $newDonors = count($result['donations'] ?? []); + $donors = ($cacheData['donors'] ?? 0) + $newDonors; + + if ($cacheData['continuation_token'] !== $result['continuation_token'] + || $cacheData['amount'] !== $result['amount']) { + $this->widgetRepository->updateStreamCardWidgetCacheData($charityStream->guid, [ + "amount" => $result['amount'], + "donors" => $donors, + "continuation_token" => $result['continuation_token'] + ]); + } + + return $this->jsonResponse($response, ['amount' => $result['amount'], 'donors' => $donors]); + } catch (Exception $e) { + return $this->jsonError($response, 'Impossible de récupérer les données.', 500); + } + } + + // ── Widget Card (Event) ────────────────────────────────────── + + public function widgetEventCard(Request $request, Response $response, array $args): Response + { + $eventGuid = $args['id'] ?? ''; + if (!$eventGuid) { + throw new Exception("Event ID manquant ou incorrect."); + } + + $cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $eventGuid); + if (!$cardWidget) { + throw new Exception("Aucun widget card trouvé pour le Event ID fourni."); + } + + $event = $this->eventRepository->selectByGuid($eventGuid); + if (!$event) { + throw new Exception("Event non trouvé."); + } + + $cacheData = $this->widgetRepository->selectEventCardWidgetCacheData($event) + ?? ['amount' => 0, 'donors' => 0, 'streams' => []]; + + $streams = $this->streamRepository->selectListByEvent($event); + $oldAmount = $cacheData['amount']; + $cacheData = $this->aggregateEventStreams($streams, $cacheData, true); + + if ($oldAmount !== $cacheData['amount']) { + $this->widgetRepository->updateEventCardWidgetCacheData($event->guid, $cacheData); + } + + $goal = $cardWidget->goal ?: 1; + $percentage = min(100, round(($cacheData['amount'] / 100) / $goal * 100)); + + $data = [ + "cardWidget" => $cardWidget, + "cardWidgetPictureUrl" => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, + "currentAmount" => $cacheData['amount'], + "donorCount" => $cacheData['donors'], + "percentage" => $percentage, + "event" => 1 + ]; + + return $this->view->render($response, 'widget/card.html.twig', $data); + } + + public function widgetEventCardFetch(Request $request, Response $response, array $args): Response + { + $eventId = $args['id'] ?? ''; + if (!$eventId) { + return $this->jsonError($response, 'Event ID manquant ou incorrect.', 400); + } + + $event = $this->eventRepository->selectByGuid($eventId); + if (!$event) { + return $this->jsonError($response, 'Event non trouvé.', 404); + } + + $cacheData = $this->widgetRepository->selectEventCardWidgetCacheData($event) + ?? ['amount' => 0, 'donors' => 0, 'streams' => []]; + + try { + $streams = $this->streamRepository->selectListByEvent($event); + $oldAmount = $cacheData['amount']; + $cacheData = $this->aggregateEventStreams($streams, $cacheData, true); + + if ($oldAmount !== $cacheData['amount']) { + $this->widgetRepository->updateEventCardWidgetCacheData($event->guid, $cacheData); + } + + return $this->jsonResponse($response, [ + 'amount' => $cacheData['amount'], + 'donors' => $cacheData['donors'] + ]); } catch (Exception $e) { - $response->getBody()->write(json_encode(['error' => 'Impossible de récupérer les commandes.'])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(500); + return $this->jsonError($response, 'Impossible de récupérer les données.', 500); } } } diff --git a/src/Middlewares/AuthAdminMiddleware.php b/src/Middlewares/AuthAdminMiddleware.php index c3510c7..30b76ec 100644 --- a/src/Middlewares/AuthAdminMiddleware.php +++ b/src/Middlewares/AuthAdminMiddleware.php @@ -5,17 +5,21 @@ use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\RequestHandlerInterface as Handler; -use Slim\Psr7\Response as SlimResponse; +/** + * @deprecated Utiliser AuthMiddleware('ADMIN') à la place. + */ class AuthAdminMiddleware { - public function __invoke(Request $request, Handler $handler): Response + private AuthMiddleware $middleware; + + public function __construct() { - if (isset($_SESSION['user']) && $_SESSION['user']->role == "ADMIN") { - return $handler->handle($request->withAttribute('user', $_SESSION['user'])); - } + $this->middleware = new AuthMiddleware('ADMIN'); + } - $response = new SlimResponse(); - return $response->withHeader('Location', '/')->withStatus(302); + public function __invoke(Request $request, Handler $handler): Response + { + return ($this->middleware)($request, $handler); } } diff --git a/src/Middlewares/AuthMiddleware.php b/src/Middlewares/AuthMiddleware.php index c86eb12..8ba68ca 100644 --- a/src/Middlewares/AuthMiddleware.php +++ b/src/Middlewares/AuthMiddleware.php @@ -9,10 +9,15 @@ class AuthMiddleware { + public function __construct(private ?string $requiredRole = null) {} + public function __invoke(Request $request, Handler $handler): Response { if (isset($_SESSION['user'])) { - return $handler->handle($request->withAttribute('user', $_SESSION['user'])); + $user = $_SESSION['user']; + if ($this->requiredRole === null || $user->role === $this->requiredRole) { + return $handler->handle($request->withAttribute('user', $user)); + } } $response = new SlimResponse(); diff --git a/src/Models/WidgetCard.php b/src/Models/WidgetCard.php new file mode 100644 index 0000000..cd92145 --- /dev/null +++ b/src/Models/WidgetCard.php @@ -0,0 +1,24 @@ +role == "ADMIN") { + if ($user->role === "ADMIN") { $stmt = $this->pdo->prepare(' SELECT c.* FROM ' . $this->prefix . 'charity_event c @@ -68,7 +68,7 @@ public function selectByUserAndId(User $user, int $id): ?Event public function selectByUserAndGuid(User $user, string $guid): ?Event { - if ($user->role == "ADMIN") { + if ($user->role === "ADMIN") { $stmt = $this->pdo->prepare(' SELECT c.* FROM ' . $this->prefix . 'charity_event c @@ -119,6 +119,13 @@ public function insert(string $title): Event $stmt = $this->pdo->prepare('INSERT INTO ' . $this->prefix . 'widget_donation_goal_bar (charity_event_guid) VALUES (:guid)'); $stmt->execute([':guid' => $guid]); + try { + $stmt = $this->pdo->prepare('INSERT INTO ' . $this->prefix . 'widget_card (charity_event_guid, description) VALUES (:guid, "")'); + $stmt->execute([':guid' => $guid]); + } catch (Exception $e) { + // Table may not exist yet (migration not run) — skip silently + } + $this->pdo->commit(); $event = new Event(); @@ -153,6 +160,15 @@ public function delete(Event $event) $stmt = $this->pdo->prepare($query); $stmt->execute([$event->guid]); + $query = 'DELETE FROM ' . $this->prefix . 'widget_card + WHERE charity_event_guid = ?'; + try { + $stmt = $this->pdo->prepare($query); + $stmt->execute([$event->guid]); + } catch (Exception $e) { + // Table may not exist yet — skip silently + } + $query = 'DELETE FROM ' . $this->prefix . 'charity_event WHERE id = ?'; $stmt = $this->pdo->prepare($query); diff --git a/src/Repositories/StreamRepository.php b/src/Repositories/StreamRepository.php index 6599166..dfdbe81 100644 --- a/src/Repositories/StreamRepository.php +++ b/src/Repositories/StreamRepository.php @@ -15,7 +15,7 @@ public function __construct( private string $prefix ) {} - function selectList(): array + public function selectList(): array { $stmt = $this->pdo->query(' SELECT c.*, u.email as admin @@ -28,7 +28,7 @@ function selectList(): array return $stmt->fetchAll(); } - function selectByGuid($guid): ?Stream + public function selectByGuid(string $guid): ?Stream { $stmt = $this->pdo->prepare(' SELECT * @@ -40,7 +40,7 @@ function selectByGuid($guid): ?Stream return $stmt->fetch() ?: null; } - function selectListByUser(User $user): array + public function selectListByUser(User $user): array { $stmt = $this->pdo->prepare(' SELECT c.*, u.email as admin @@ -62,7 +62,7 @@ function selectListByUser(User $user): array return $stmt->fetchAll(); } - function selectListByEvent(Event $event): array + public function selectListByEvent(Event $event): array { $stmt = $this->pdo->prepare(' SELECT * @@ -74,9 +74,9 @@ function selectListByEvent(Event $event): array return $stmt->fetchAll(); } - function selectByUserAndGuid(User $user, $guid): ?Stream + public function selectByUserAndGuid(User $user, string $guid): ?Stream { - if ($user->role == "ADMIN") { + if ($user->role === "ADMIN") { $stmt = $this->pdo->prepare(' SELECT c.* FROM ' . $this->prefix . 'charity_stream c @@ -106,7 +106,7 @@ function selectByUserAndGuid(User $user, $guid): ?Stream return $stream ?: null; } - function insert($form_slug, $organization_slug, $title, $parent = null): Stream + public function insert(string $form_slug, string $organization_slug, string $title, ?int $parent = null): Stream { $guid = bin2hex(random_bytes(16)); @@ -140,6 +140,15 @@ function insert($form_slug, $organization_slug, $title, $parent = null): Stream ':guid' => $guid ]); + $query = 'INSERT INTO ' . $this->prefix . 'widget_card (charity_stream_guid, description) + VALUES (:guid, "")'; + try { + $stmt = $this->pdo->prepare($query); + $stmt->execute([':guid' => $guid]); + } catch (Exception $e) { + // Table may not exist yet (migration not run) — skip silently + } + $this->pdo->commit(); $stream = new Stream(); @@ -155,7 +164,7 @@ function insert($form_slug, $organization_slug, $title, $parent = null): Stream } } - public function delete($stream) + public function delete(Stream $stream): void { $this->pdo->beginTransaction(); @@ -170,6 +179,15 @@ public function delete($stream) $stmt = $this->pdo->prepare($query); $stmt->execute([$stream->guid]); + $query = 'DELETE FROM ' . $this->prefix . 'widget_card + WHERE charity_stream_guid = ?'; + try { + $stmt = $this->pdo->prepare($query); + $stmt->execute([$stream->guid]); + } catch (Exception $e) { + // Table may not exist yet — skip silently + } + $query = 'DELETE FROM ' . $this->prefix . 'user_right WHERE id_charity_stream = ?'; $stmt = $this->pdo->prepare($query); diff --git a/src/Repositories/UserRepository.php b/src/Repositories/UserRepository.php index 7f18ad3..7808ecc 100644 --- a/src/Repositories/UserRepository.php +++ b/src/Repositories/UserRepository.php @@ -50,6 +50,11 @@ public function select(string $email): ?User return $stmt->fetch() ?: null; } + public function findOrCreate(string $email): User + { + return $this->select($email) ?? $this->insert($email); + } + /** * Recherche un utilisateur par son token de réinitialisation, uniquement si le token n'est pas expiré. */ diff --git a/src/Repositories/WidgetRepository.php b/src/Repositories/WidgetRepository.php index 219973a..cfcef9e 100644 --- a/src/Repositories/WidgetRepository.php +++ b/src/Repositories/WidgetRepository.php @@ -2,7 +2,10 @@ namespace App\Repositories; +use App\Models\Event; +use App\Models\Stream; use App\Models\WidgetAlert; +use App\Models\WidgetCard; use App\Models\WidgetDonation; use Exception; use PDO; @@ -107,93 +110,154 @@ public function updateAlertWidget(string $guid, array $postData, ?string $image } } - public function selectAlertWidgetCacheData($stream): ?array + // ── Generic cache helpers ──────────────────────────────────── + + private function selectCacheData(string $table, string $column, string $guid): ?array { - $stmt = $this->pdo->prepare(' - SELECT cache_data - FROM ' . $this->prefix . 'widget_alert_box - WHERE charity_stream_guid = ? - '); - $stmt->execute([$stream->guid]); + $stmt = $this->pdo->prepare( + 'SELECT cache_data FROM ' . $this->prefix . $table . ' WHERE ' . $column . ' = ?' + ); + $stmt->execute([$guid]); $data = $stmt->fetch(); - if ($data) { - return json_decode($data["cache_data"] ?? "", true); - } + return $data ? json_decode($data["cache_data"] ?? "", true) : null; + } - return null; + private function updateCacheData(string $table, string $column, string $guid, array $data): void + { + $stmt = $this->pdo->prepare( + 'UPDATE ' . $this->prefix . $table . ' SET cache_data = ? WHERE ' . $column . ' = ?' + ); + $stmt->execute([json_encode($data), $guid]); } - public function updateAlertWidgetCacheData(string $streamGuid, array $data): void + // ── Alert widget cache ──────────────────────────────────────── + + public function selectAlertWidgetCacheData(Stream $stream): ?array { - $stmt = $this->pdo->prepare(' - UPDATE ' . $this->prefix . 'widget_alert_box - SET cache_data = ? - WHERE charity_stream_guid = ? - '); - $stmt->execute([ - json_encode($data), - $streamGuid - ]); + return $this->selectCacheData('widget_alert_box', 'charity_stream_guid', $stream->guid); } - public function selectStreamDonationWidgetCacheData($stream): ?array + public function updateAlertWidgetCacheData(string $streamGuid, array $data): void { - $stmt = $this->pdo->prepare(' - SELECT cache_data - FROM ' . $this->prefix . 'widget_donation_goal_bar - WHERE charity_stream_guid = ? - '); - $stmt->execute([$stream->guid]); - $data = $stmt->fetch(); + $this->updateCacheData('widget_alert_box', 'charity_stream_guid', $streamGuid, $data); + } - if ($data) { - return json_decode($data["cache_data"] ?? "", true); - } + // ── Donation widget cache ───────────────────────────────────── - return null; + public function selectStreamDonationWidgetCacheData(Stream $stream): ?array + { + return $this->selectCacheData('widget_donation_goal_bar', 'charity_stream_guid', $stream->guid); } public function updateStreamDonationWidgetCacheData(string $streamGuid, array $data): void { - $stmt = $this->pdo->prepare(' - UPDATE ' . $this->prefix . 'widget_donation_goal_bar - SET cache_data = ? - WHERE charity_stream_guid = ? - '); - $stmt->execute([ - json_encode($data), - $streamGuid - ]); + $this->updateCacheData('widget_donation_goal_bar', 'charity_stream_guid', $streamGuid, $data); + } + + public function selectEventDonationWidgetCacheData(Event $event): ?array + { + return $this->selectCacheData('widget_donation_goal_bar', 'charity_event_guid', $event->guid); + } + + public function updateEventDonationWidgetCacheData(string $eventGuid, array $data): void + { + $this->updateCacheData('widget_donation_goal_bar', 'charity_event_guid', $eventGuid, $data); + } + + // ── Widget Card ────────────────────────────────────────────── + + public function selectCardWidgetByGuid(?string $streamGuid, ?string $eventGuid): ?WidgetCard + { + try { + $stmt = $this->pdo->prepare(' + SELECT * + FROM ' . $this->prefix . 'widget_card + WHERE charity_stream_guid = ? + OR charity_event_guid = ? + '); + $stmt->setFetchMode(PDO::FETCH_CLASS, WidgetCard::class); + $stmt->execute([$streamGuid, $eventGuid]); + return $stmt->fetch() ?: null; + } catch (Exception $e) { + // Table may not exist yet (migration not run) + return null; + } } - public function selectEventDonationWidgetCacheData($event): ?array + public function insertCardWidget(?string $streamGuid, ?string $eventGuid): void { $stmt = $this->pdo->prepare(' - SELECT cache_data - FROM ' . $this->prefix . 'widget_donation_goal_bar - WHERE charity_event_guid = ? + INSERT INTO ' . $this->prefix . 'widget_card (charity_stream_guid, charity_event_guid, description) + VALUES (?, ?, "") '); - $stmt->execute([$event->guid]); - $data = $stmt->fetch(); + $stmt->execute([$streamGuid, $eventGuid]); + } + + public function updateCardWidget(?string $streamGuid, ?string $eventGuid, array $data, ?string $image = null): void + { + $this->pdo->beginTransaction(); + + try { + $stmt = $this->pdo->prepare(' + UPDATE ' . $this->prefix . 'widget_card + SET tag = ?, title = ?, description = ?, goal = ?, + background_color = ?, bar_color = ?, bar_background_color = ?, + text_color = ?, tag_color = ?, tag_background_color = ? + WHERE charity_stream_guid = ? + OR charity_event_guid = ? + '); + $stmt->execute([ + $data['card_tag'] ?? '', + $data['card_title'] ?? '', + $data['card_description'] ?? '', + $data['card_goal'] ?? 1000, + $data['card_background_color'] ?? '#ffffff', + $data['card_bar_color'] ?? '#2563eb', + $data['card_bar_background_color'] ?? '#e5e7eb', + $data['card_text_color'] ?? '#1a1a1a', + $data['card_tag_color'] ?? '#166534', + $data['card_tag_background_color'] ?? '#dcfce7', + $streamGuid, + $eventGuid + ]); - if ($data) { - return json_decode($data["cache_data"] ?? "", true); + if ($image !== null) { + $stmt = $this->pdo->prepare(' + UPDATE ' . $this->prefix . 'widget_card + SET image = ? + WHERE charity_stream_guid = ? + OR charity_event_guid = ? + '); + $stmt->execute([$image, $streamGuid, $eventGuid]); + } + + $this->pdo->commit(); + } catch (Exception $e) { + $this->pdo->rollBack(); + throw $e; } + } + + // ── Card widget cache ───────────────────────────────────────── - return null; + public function selectStreamCardWidgetCacheData(Stream $stream): ?array + { + return $this->selectCacheData('widget_card', 'charity_stream_guid', $stream->guid); } - public function updateEventDonationWidgetCacheData(string $eventGuid, array $data): void + public function updateStreamCardWidgetCacheData(string $streamGuid, array $data): void { - $stmt = $this->pdo->prepare(' - UPDATE ' . $this->prefix . 'widget_donation_goal_bar - SET cache_data = ? - WHERE charity_event_guid = ? - '); - $stmt->execute([ - json_encode($data), - $eventGuid - ]); + $this->updateCacheData('widget_card', 'charity_stream_guid', $streamGuid, $data); + } + + public function selectEventCardWidgetCacheData(Event $event): ?array + { + return $this->selectCacheData('widget_card', 'charity_event_guid', $event->guid); + } + + public function updateEventCardWidgetCacheData(string $eventGuid, array $data): void + { + $this->updateCacheData('widget_card', 'charity_event_guid', $eventGuid, $data); } } diff --git a/src/Services/ApiWrapper.php b/src/Services/ApiWrapper.php index 409e9f0..45aae85 100644 --- a/src/Services/ApiWrapper.php +++ b/src/Services/ApiWrapper.php @@ -341,6 +341,30 @@ public function setClientDomain($accessToken) } } + /** + * Stocke ou met à jour un AccessToken à partir des données retournées par l'échange OAuth. + */ + public function storeOrUpdateToken(array $tokenData): AccessToken + { + $organizationSlug = $tokenData['organization_slug']; + $existingToken = $this->accessTokenRepository->selectBySlug($organizationSlug); + + $token = new AccessToken(); + $token->access_token = $tokenData['access_token']; + $token->refresh_token = $tokenData['refresh_token']; + $token->organization_slug = $organizationSlug; + $token->access_token_expires_at = (new DateTime())->add(new DateInterval('PT28M')); + $token->refresh_token_expires_at = (new DateTime())->add(new DateInterval('P28D')); + + if ($existingToken === null) { + $this->accessTokenRepository->insert($token); + } else { + $this->accessTokenRepository->update($token); + } + + return $token; + } + /** * Échange un code d'autorisation contre un token d'accès pour une organisation donnée, et stocke les tokens en base de données. * @@ -406,34 +430,31 @@ public function exchangeAuthorizationCode($code, $redirect_uri, $codeVerifier) */ private function getDonationFormOrders($organizationSlug, $donationSlug, $accessToken, $continuationToken = null) { - $curl = curl_init(); - - $url = $this->apiUrl . '/organizations/' . $organizationSlug . '/forms/donation/' . $donationSlug . '/orders?withDetails=true&sortOrder=asc'; + $query = ['withDetails' => 'true', 'sortOrder' => 'asc']; if ($continuationToken) { - $url .= '&continuationToken=' . $continuationToken; + $query['continuationToken'] = $continuationToken; } - //TODO => ici on regarde toujours l'acces token, quid du refresh/continuation? - - curl_setopt_array($curl, array( - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_ENCODING => '', - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 0, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, - CURLOPT_CUSTOMREQUEST => 'GET', - CURLOPT_HTTPHEADER => array( - 'Authorization: Bearer ' . $accessToken - ), - )); - - $response = curl_exec($curl); - curl_close($curl); - - $response_data = json_decode($response, true); - - return $response_data; + + try { + $response = $this->client->request('GET', $this->apiUrl . '/organizations/' . $organizationSlug . '/forms/donation/' . $donationSlug . '/orders', [ + 'query' => $query, + 'headers' => [ + 'Authorization' => 'Bearer ' . $accessToken, + 'accept' => 'application/json', + ], + ]); + } catch (RequestException $e) { + $this->apiLogger->error('Erreur lors de la récupération des commandes: ' . $e->getMessage()); + if ($e->hasResponse()) { + $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); + } + throw new Exception("Erreur lors de la récupération des commandes : " . $e->getMessage(), 0, $e); + } catch (GuzzleException $e) { + $this->apiLogger->error('Erreur Guzzle lors de la récupération des commandes: ' . $e->getMessage()); + throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); + } + + return json_decode($response->getBody(), true); } /** @@ -453,16 +474,11 @@ public function getAllOrders(string $organizationSlug, string $formSlug, int $cu try { $organizationAccessToken = $this->getOrganizationAccessToken($organizationSlug); } catch (Exception $e) { - http_response_code(401); - echo('Votre token d\'accès pour l\'organisation ' . $organizationSlug . ' est expiré ou invalide. Veuillez vous reconnecter pour renouveler votre token.'); - echo('Se reconnecter'); - exit; + throw new Exception('Votre token d\'accès pour l\'organisation ' . $organizationSlug . ' est expiré ou invalide. Veuillez vous reconnecter pour renouveler votre token.', 401, $e); } if (!$organizationAccessToken || !isset($organizationAccessToken->access_token)) { - http_response_code(401); - echo json_encode(['error' => 'Jeton d\'accès API non trouvé ou expiré.']); - exit; + throw new Exception('Jeton d\'accès API non trouvé ou expiré pour l\'organisation ' . $organizationSlug . '.', 401); } do { $formOrdersData = $this->getDonationFormOrders( diff --git a/src/views/event/edit.html.twig b/src/views/event/edit.html.twig index 3ec3be5..9eabde9 100644 --- a/src/views/event/edit.html.twig +++ b/src/views/event/edit.html.twig @@ -1,4 +1,3 @@ - {% block title %}Édition de l'événement{% endblock %} {% extends "layout.html.twig" %} {% block content %} @@ -7,60 +6,180 @@ Retour -
-

Widget barre de don

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-

{{ donationGoalWidget.goal / 2 }} - €

-

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} - €

-
-
-

{{ donationGoalWidget.goal / 2 }} - €

-

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} - €

+
+ + {# ── Widget barre de don ────────────────────────────────── #} +
+

+ +

+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+

{{ donationGoalWidget.goal / 2 }} €

+

{{ donationGoalWidget.text_content }}

+

{{ donationGoalWidget.goal }} €

+
+
+

{{ donationGoalWidget.goal / 2 }} €

+

{{ donationGoalWidget.text_content }}

+

{{ donationGoalWidget.goal }} €

+
+
+
+
+ URL : {{ widgetDonationGoalUrl }} +
+ +
+ +
-
-
-
- URL du widget : - {{ widgetDonationGoalUrl }} -
-
- + + {# ── Widget carte de don ────────────────────────────────── #} + {% if cardWidget %} +
+

+ +

+
+
+
+
+
+
+ + {% if cardWidget.image %} +
+ Déjà chargée : + {{ cardWidget.image }} +
+ {% endif %} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Couleurs
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {# ── Prévisualisation ── #} +
+
+
+
+
+ ✏️ {{ cardWidget.tag }} +
+
{{ cardWidget.title }}
+
{{ cardWidget.description }}
+
+
{{ (cardWidget.goal / 2) }} €
+
+
+
+
+
Objectif : {{ cardWidget.goal }} €
+
+
+ 50% collectés + 0 donateurs +
+
+
+
+
+ +
+
+ URL : {{ widgetCardUrl }} +
+ +
+
+
+
- + {% endif %} + +
{# /accordion #}
{% endblock %} {% block scripts %} diff --git a/src/views/stream/edit.html.twig b/src/views/stream/edit.html.twig index 719c1cb..da7ef75 100644 --- a/src/views/stream/edit.html.twig +++ b/src/views/stream/edit.html.twig @@ -1,4 +1,3 @@ - {% block title %}Édition du stream{% endblock %} @@ -14,122 +13,254 @@ Formulaire de don Helloasso {{ donationUrl }} -
-

Widget barre de don

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-

{{ donationGoalWidget.goal / 2 }} - €

-

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} - €

-
-
-

{{ donationGoalWidget.goal / 2 }} - €

-

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} - €

-
-
-
-
-
- URL du widget : - {{ widgetDonationGoalUrl }} -
-
- -
-
-
+
-

Widget alerte

-
-
- - {% if alertBoxWidget.image %} -
- Déjà chargé: - {{ alertBoxWidget.image }} + {# ── Widget barre de don ────────────────────────────────── #} +
+

+ +

+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+

{{ donationGoalWidget.goal / 2 }} €

+

{{ donationGoalWidget.text_content }}

+

{{ donationGoalWidget.goal }} €

+
+
+

{{ donationGoalWidget.goal / 2 }} €

+

{{ donationGoalWidget.text_content }}

+

{{ donationGoalWidget.goal }} €

+
+
+
+
+ URL : {{ widgetDonationGoalUrl }} +
+ +
+
- {% endif %} - -
-
- - -
-
- -
- C'est le message qui s'affichera lors d'un don.
- Il existe 3 paramètres: -
    -
  • {pseudo} le pseudo du donateur (anonyme si non précisé)
  • -
  • {amount} le montant du don
  • -
  • {message} si le donateur a laissé un message
  • -
- Vous pouvez ensuite formater le texte avec du html. -
-
-
-
- - {% if alertBoxWidget.sound %} -
- Déjà chargé: - {{ alertBoxWidget.sound }} + + {# ── Widget alerte ──────────────────────────────────────── #} +
+

+ +

+
+
+
+
+
+
+ + {% if alertBoxWidget.image %} +
+ Déjà chargé: + {{ alertBoxWidget.image }} +
+ {% endif %} + +
+
+ + +
+
+ + {% if alertBoxWidget.sound %} +
+ Déjà chargé: + {{ alertBoxWidget.sound }} +
+ {% endif %} + +
+
+ + +
+
+
+
+ +
+ C'est le message qui s'affichera lors d'un don.
+ Paramètres disponibles : +
    +
  • {pseudo} le pseudo du donateur
  • +
  • {amount} le montant du don
  • +
  • {message} le message du donateur
  • +
+ Vous pouvez formater le texte avec du HTML. +
+ +
+
+
+
+
+ URL : {{ widgetAlertBoxUrl }} +
+
+ + +
+
+
+
- {% endif %} - -
-
- - +
-
-
- URL du widget : - {{ widgetAlertBoxUrl }} -
-
- - + {# ── Widget carte de don ────────────────────────────────── #} + {% if cardWidget %} +
+

+ +

+
+
+
+
+
+
+ + {% if cardWidget.image %} +
+ Déjà chargée : + {{ cardWidget.image }} +
+ {% endif %} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Couleurs
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {# ── Prévisualisation ── #} +
+
+
+
+
+ ✏️ {{ cardWidget.tag }} +
+
{{ cardWidget.title }}
+
{{ cardWidget.description }}
+
+
{{ (cardWidget.goal / 2) }} €
+
+
+
+
+
Objectif : {{ cardWidget.goal }} €
+
+
+ 50% collectés + 0 donateurs +
+
+
+
+
+ +
+
+ URL : {{ widgetCardUrl }} +
+ +
+
+
+
- + {% endif %} -
+
{# /accordion #}
{% endblock %} {% block scripts %} diff --git a/src/views/widget/card.html.twig b/src/views/widget/card.html.twig new file mode 100644 index 0000000..fbca762 --- /dev/null +++ b/src/views/widget/card.html.twig @@ -0,0 +1,180 @@ +{% block title %}Card Widget{% endblock %} +{% extends "widget.html.twig" %} +{% block styles %} + +{% endblock %} +{% block content %} +
+ {% if cardWidgetPictureUrl %} +
+ {% endif %} +
+ {% if cardWidget.tag %} +
+ + + + {{ cardWidget.tag }} +
+ {% endif %} + +
{{ cardWidget.title }}
+ + {% if cardWidget.description %} +
{{ cardWidget.description }}
+ {% endif %} + +
+
{{ currentAmount / 100 }} €
+
+
+
+
+
Objectif : {{ cardWidget.goal }} €
+
+
+ {{ percentage }}% collectés + {{ donorCount }} donateurs +
+
+
+
+{% endblock %} +{% block scripts %} + + {% if stream %} + {% if viteEntries.cardStream.js %} + + {% endif %} + {% else %} + {% if viteEntries.cardEvent.js %} + + {% endif %} + {% endif %} +{% endblock %} + From 427af1b5cab22d152ef7f1692c341ffc9b689a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o?= Date: Tue, 12 May 2026 12:09:33 +0200 Subject: [PATCH 2/4] Fix and improvement --- src/Assets/js/alert.js | 38 ++++++++++++++--- src/Controllers/AdminController.php | 10 +++++ src/views/event/edit.html.twig | 42 ++++++++++++++++++- src/views/index.html.twig | 4 +- src/views/layout.html.twig | 2 +- src/views/stream/edit.html.twig | 9 +++- src/views/stream/index-admin.html.twig | 55 +++++++++++++++--------- src/views/stream/index.html.twig | 58 ++++++++++++++++++-------- src/views/widget.html.twig | 2 +- 9 files changed, 172 insertions(+), 48 deletions(-) diff --git a/src/Assets/js/alert.js b/src/Assets/js/alert.js index faeeafe..f11e72d 100644 --- a/src/Assets/js/alert.js +++ b/src/Assets/js/alert.js @@ -1,6 +1,21 @@ var alertQueue = []; var isAlertActive = false; var isFetching = false; +var audioUnlocked = false; + +// Pré-déverrouiller l'audio pour les navigateurs/OBS +(function unlockAudio() { + var ctx = new (window.AudioContext || window.webkitAudioContext)(); + if (ctx.state === 'suspended') { + ctx.resume().then(function() { audioUnlocked = true; }); + } else { + audioUnlocked = true; + } + // Créer un son silencieux pour débloquer la lecture audio + var silentAudio = new Audio("data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA="); + silentAudio.volume = 0; + silentAudio.play().catch(function() {}); +})(); fetchAlerts(); setInterval(fetchAlerts, 10000); @@ -57,21 +72,34 @@ function processAlertQueue() { messageTemplate.style.marginTop = '10px'; messageTemplate.classList.add('fade'); container.appendChild(messageTemplate); - var audio = new Audio(window.sound); + + // Pré-charger le son avant de l'utiliser + var audio = null; + if (window.sound && window.sound.length > 0) { + audio = new Audio(window.sound); + audio.volume = window.sound_volume || 0.5; + audio.preload = 'auto'; + audio.load(); + } setTimeout(function () { mediaElement.classList.add('show'); messageTemplate.classList.add('show'); - audio.volume = window.sound_volume; - audio.play(); + if (audio) { + audio.play().catch(function(err) { + console.warn('Impossible de jouer le son :', err); + }); + } }, 100) setTimeout(function () { mediaElement.classList.remove('show'); messageTemplate.classList.remove('show'); - audio.pause(); - audio.currentTime = 0; + if (audio) { + audio.pause(); + audio.currentTime = 0; + } container.innerHTML = ''; processAlertQueue(); diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index a74fc83..b64ffc7 100644 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -48,6 +48,8 @@ public function index(Request $request, Response $response): Response "events" => $events, "messages" => $this->messages->getMessages(), "currentUser" => $user, + "selectedEventId" => $request->getQueryParams()['eventId'] ?? null, + "openCreateStream" => isset($request->getQueryParams()['createStream']), ]; $template = $user->role === "ADMIN" ? 'stream/index-admin.html.twig' : 'stream/index.html.twig'; @@ -93,11 +95,13 @@ public function editEvent(Request $request, Response $response, array $args): Re $event = $this->eventRepository->selectByUserAndGuid($user, $args['id']); $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $event->guid); $cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $event->guid); + $streams = $this->streamRepository->selectListByEvent($event); $routeParser = RouteContext::fromRequest($request)->getRouteParser(); $data = [ "logged" => true, "event" => $event, + "streams" => $streams, "donationGoalWidget" => $donationGoalWidget, "cardWidget" => $cardWidget, "cardWidgetPictureUrl" => ($cardWidget && $cardWidget->image) ? $this->fileManager->getPictureUrl($cardWidget->image) : null, @@ -189,12 +193,18 @@ public function editStream(Request $request, Response $response, array $args): R $alertBoxWidget = $this->widgetRepository->selectAlertWidgetByGuid($guid); $cardWidget = $this->widgetRepository->selectCardWidgetByGuid($guid, null); + $parentEvent = null; + if ($charityStream->charity_event_id) { + $parentEvent = $this->eventRepository->selectByUserAndId($user, $charityStream->charity_event_id); + } + $donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/formulaires/' . $charityStream->form_slug; $routeParser = RouteContext::fromRequest($request)->getRouteParser(); $data = [ "logged" => true, "charityStream" => $charityStream, + "parentEvent" => $parentEvent, "donationGoalWidget" => $donationGoalWidget, "alertBoxWidget" => $alertBoxWidget, "alertBoxWidgetPictureUrl" => ($alertBoxWidget && $alertBoxWidget->image) ? $this->fileManager->getPictureUrl($alertBoxWidget->image) : null, diff --git a/src/views/event/edit.html.twig b/src/views/event/edit.html.twig index 9eabde9..0b4030c 100644 --- a/src/views/event/edit.html.twig +++ b/src/views/event/edit.html.twig @@ -1,4 +1,4 @@ -{% block title %}Édition de l'événement{% endblock %} +{% block title %}HelloAsso Widgets - Édition de l'événement{% endblock %} {% extends "layout.html.twig" %} {% block content %}
@@ -6,6 +6,38 @@ Retour + {# ── Streams liés à cet événement ────────────────────────── #} +
+
+
🎮 Streams rattachés
+ ➕ Créer un stream +
+
+ {% if streams is not empty %} + + + + + + + + + + {% for stream in streams %} + + + + + + {% endfor %} + +
IDTitreSlug formulaire
{{ stream.id | e }}{{ stream.title | e }}{{ stream.form_slug | e }}
+ {% else %} +

Aucun stream rattaché à cet événement.

+ {% endif %} +
+
+
{# ── Widget barre de don ────────────────────────────────── #} @@ -182,6 +214,14 @@
{# /accordion #}
{% endblock %} +{% block styles %} + +{% endblock %} {% block scripts %} {% if viteEntries.admin.js %} diff --git a/src/views/index.html.twig b/src/views/index.html.twig index 5c5d176..dd38a4d 100644 --- a/src/views/index.html.twig +++ b/src/views/index.html.twig @@ -1,9 +1,9 @@ -{% block title %}HelloAssoStream 🎮{% endblock %} +{% block title %}HelloAsso Widgets{% endblock %} {% extends "layout.html.twig" %} {% block content %}
-

HelloAssoStream 🎮

+

HelloAsso Widgets

diff --git a/src/views/layout.html.twig b/src/views/layout.html.twig index 71cc684..4adcf19 100644 --- a/src/views/layout.html.twig +++ b/src/views/layout.html.twig @@ -2,7 +2,7 @@ - {% block title %}Titre par défaut{% endblock %} + {% block title %}HelloAsso Widgets{% endblock %} diff --git a/src/views/stream/edit.html.twig b/src/views/stream/edit.html.twig index da7ef75..26aa910 100644 --- a/src/views/stream/edit.html.twig +++ b/src/views/stream/edit.html.twig @@ -1,4 +1,4 @@ -{% block title %}Édition du stream{% endblock %} +{% block title %}HelloAsso Widgets - Édition du stream{% endblock %} {% extends "layout.html.twig" %} @@ -9,6 +9,13 @@ Retour + {% if parentEvent %} +
+ 📅 Événement parent : + {{ parentEvent.title }} +
+ {% endif %} +
Formulaire de don Helloasso {{ donationUrl }} diff --git a/src/views/stream/index-admin.html.twig b/src/views/stream/index-admin.html.twig index 96da6b7..04ddc03 100644 --- a/src/views/stream/index-admin.html.twig +++ b/src/views/stream/index-admin.html.twig @@ -1,5 +1,5 @@ -{% block title %}Administration générale{% endblock %} +{% block title %}HelloAsso Widgets - Administration{% endblock %} {% extends "layout.html.twig" %} {% block content %} @@ -241,7 +241,7 @@
-

Administration générale

+

HelloAsso Widgets

@@ -261,27 +261,27 @@

Liste des Charity Events

- - +
+ - + {% for event in events %} - + - - + @@ -290,11 +290,10 @@
IdGuid Titre AdminDate de création Actions
{{ event.id | e }}{{ event.guid | e }} {{ event.title | e }} {{ event.admin | e }} - 📝 + {{ event.creation_date ? event.creation_date|date('d/m/Y H:i') : '—' }} + 📝 - +

Liste des Charity Streams

- - +
+ - @@ -304,18 +303,17 @@ {% for stream in streams %} - + - - @@ -354,6 +352,12 @@ {% endblock %} {% block styles %} {% endblock %} {% block scripts %}
IDGUID Titre Admin Slug formulaire
{{ stream.id | e }}{{ stream.guid | e }} {{ stream.title | e }} {{ stream.admin | e }} {{ stream.form_slug | e }} {{ stream.organization_slug | e }} - 📝 - 🔑 + + 📝 + 🔑
- +