diff --git a/hifi-src/src/components/CookieConsentBanner.jsx b/hifi-src/src/components/CookieConsentBanner.jsx index af10d78..22512ef 100644 --- a/hifi-src/src/components/CookieConsentBanner.jsx +++ b/hifi-src/src/components/CookieConsentBanner.jsx @@ -6,6 +6,7 @@ import { useLanguage } from '../context/LanguageContext.jsx'; function SettingsModal({ onClose }) { const { consent, saveCustom } = useCookieConsent(); const [external, setExternal] = useState(consent.external); + const [analytics, setAnalytics] = useState(consent.analytics); const { t } = useLanguage(); return ( @@ -49,6 +50,20 @@ function SettingsModal({ onClose }) { className="mt-1 h-4 w-4 shrink-0" /> +
+
+

{t('cookieConsent.analyticsTitle')}

+

+ {t('cookieConsent.analyticsDesc')} +

+
+ setAnalytics(e.target.checked)} + className="mt-1 h-4 w-4 shrink-0" + /> +
diff --git a/hifi-src/src/context/CookieConsentContext.jsx b/hifi-src/src/context/CookieConsentContext.jsx index fced880..0c11dbf 100644 --- a/hifi-src/src/context/CookieConsentContext.jsx +++ b/hifi-src/src/context/CookieConsentContext.jsx @@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea const STORAGE_KEY = 'hifiplanet-cookie-consent'; -const defaultConsent = { necessary: true, external: false }; +const defaultConsent = { necessary: true, external: false, analytics: false }; const CookieConsentContext = createContext({ consent: defaultConsent, @@ -25,7 +25,7 @@ export function CookieConsentProvider({ children }) { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); - setConsent({ necessary: true, external: !!parsed.external }); + setConsent({ necessary: true, external: !!parsed.external, analytics: !!parsed.analytics }); setDecided(true); } } catch { @@ -40,9 +40,12 @@ export function CookieConsentProvider({ children }) { localStorage.setItem(STORAGE_KEY, JSON.stringify({ ...next, decidedAt: new Date().toISOString() })); }, []); - const acceptAll = useCallback(() => persist({ necessary: true, external: true }), [persist]); - const rejectNonEssential = useCallback(() => persist({ necessary: true, external: false }), [persist]); - const saveCustom = useCallback((partial) => persist({ necessary: true, external: !!partial.external }), [persist]); + const acceptAll = useCallback(() => persist({ necessary: true, external: true, analytics: true }), [persist]); + const rejectNonEssential = useCallback(() => persist({ necessary: true, external: false, analytics: false }), [persist]); + const saveCustom = useCallback( + (partial) => persist({ necessary: true, external: !!partial.external, analytics: !!partial.analytics }), + [persist] + ); const openSettings = useCallback(() => setSettingsOpen(true), []); const closeSettings = useCallback(() => setSettingsOpen(false), []); diff --git a/hifi-src/src/context/SiteSettingsContext.jsx b/hifi-src/src/context/SiteSettingsContext.jsx index 4558411..7030f70 100644 --- a/hifi-src/src/context/SiteSettingsContext.jsx +++ b/hifi-src/src/context/SiteSettingsContext.jsx @@ -7,6 +7,7 @@ const SiteSettingsContext = createContext({ whatsapp: null, contact_email: 'info@hifi-planet-amorbach.de', hero_image_path: null, + ga_measurement_id: null, }); export const SiteSettingsProvider = SiteSettingsContext.Provider; diff --git a/hifi-src/src/i18n/de.js b/hifi-src/src/i18n/de.js index 8e650d1..d6b8ab3 100644 --- a/hifi-src/src/i18n/de.js +++ b/hifi-src/src/i18n/de.js @@ -36,7 +36,7 @@ export default { cookieSettings: 'Cookie-Einstellungen', }, cookieConsent: { - bannerText: 'Wir verwenden nur technisch notwendige Cookies. Für Google Maps und YouTube-Videos benötigen wir zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden.', + bannerText: 'Wir verwenden nur technisch notwendige Cookies. Für Google Maps, YouTube-Videos und Statistik-Cookies benötigen wir zusätzlich deine Zustimmung, da dabei Daten an Google übertragen werden.', moreInfo: 'Mehr dazu in unserer', privacyPolicy: 'Datenschutzerklärung', settings: 'Einstellungen', @@ -48,6 +48,8 @@ export default { necessaryDesc: 'Für den Betrieb der Website erforderlich (z. B. Login-Session, Theme- und Cookie-Einstellung). Kann nicht deaktiviert werden.', externalTitle: 'Externe Medien', externalDesc: 'Google Maps und YouTube-Videos. Beim Laden werden Daten (u. a. deine IP-Adresse) an Google übertragen.', + analyticsTitle: 'Statistik', + analyticsDesc: 'Google Analytics hilft uns zu verstehen, wie die Website genutzt wird. Dabei werden Daten an Google übertragen.', cancel: 'Abbrechen', saveSelection: 'Auswahl speichern', }, diff --git a/hifi-src/src/i18n/en.js b/hifi-src/src/i18n/en.js index 83ad0f2..eda24b0 100644 --- a/hifi-src/src/i18n/en.js +++ b/hifi-src/src/i18n/en.js @@ -36,7 +36,7 @@ export default { cookieSettings: 'Cookie settings', }, cookieConsent: { - bannerText: 'We only use technically necessary cookies. For Google Maps and YouTube videos we need your additional consent, since data is transferred to Google in that case.', + bannerText: 'We only use technically necessary cookies. For Google Maps, YouTube videos and analytics cookies we need your additional consent, since data is transferred to Google in that case.', moreInfo: 'More details in our', privacyPolicy: 'privacy policy', settings: 'Settings', @@ -48,6 +48,8 @@ export default { necessaryDesc: 'Required to operate the website (e.g. login session, theme and cookie preference). Cannot be disabled.', externalTitle: 'External media', externalDesc: 'Google Maps and YouTube videos. Loading them transfers data (including your IP address) to Google.', + analyticsTitle: 'Analytics', + analyticsDesc: 'Google Analytics helps us understand how the website is used. This transfers data to Google.', cancel: 'Cancel', saveSelection: 'Save selection', }, diff --git a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx index 98f7334..d0cb6ea 100644 --- a/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx +++ b/hifi-src/src/pages/admin/settings/WebsiteSettings.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { api } from '../../../api/client.js'; import ImageUploadField from '../../../components/ImageUploadField.jsx'; -const emptyForm = { phone: '', whatsapp: '', contact_email: '', hero_image_path: '' }; +const emptyForm = { phone: '', whatsapp: '', contact_email: '', hero_image_path: '', ga_measurement_id: '' }; export default function WebsiteSettings() { const [form, setForm] = useState(emptyForm); @@ -20,6 +20,7 @@ export default function WebsiteSettings() { whatsapp: res.whatsapp || '', contact_email: res.contact_email || '', hero_image_path: res.hero_image_path || '', + ga_measurement_id: res.ga_measurement_id || '', }) ) .finally(() => setLoading(false)); @@ -103,6 +104,23 @@ export default function WebsiteSettings() { +
+

Google Analytics

+

+ Wird erst geladen, nachdem ein Besucher der Statistik-Kategorie im Cookie-Banner zugestimmt hat. Leer + lassen, um Google Analytics zu deaktivieren. +

+
+ + update('ga_measurement_id', e.target.value)} + placeholder="G-XXXXXXXXXX" + className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900" + /> +
+
+ {error &&

{error}

} {saved &&

Gespeichert.

} diff --git a/hifi-src/src/pages/public/Datenschutz.jsx b/hifi-src/src/pages/public/Datenschutz.jsx index 76009b1..fb3a0da 100644 --- a/hifi-src/src/pages/public/Datenschutz.jsx +++ b/hifi-src/src/pages/public/Datenschutz.jsx @@ -93,9 +93,10 @@ export default function Datenschutz() {

Für diese technisch notwendigen Cookies ist gemäß § 25 Abs. 2 Nr. 2 TTDSG keine Einwilligung erforderlich. Rechtsgrundlage ist unser berechtigtes Interesse am technischen Betrieb der Website (Art. - 6 Abs. 1 lit. f DSGVO). Für alle darüber hinausgehenden Inhalte – aktuell die eingebundenen Google - Maps- und YouTube-Elemente – fragen wir Sie beim ersten Besuch aktiv nach Ihrer Einwilligung. Sie - können Ihre Auswahl jederzeit über den Link „Cookie-Einstellungen" im Footer ändern. + 6 Abs. 1 lit. f DSGVO). Für alle darüber hinausgehenden Inhalte und Dienste – aktuell die eingebundenen + Google Maps- und YouTube-Elemente sowie Google Analytics – fragen wir Sie beim ersten Besuch aktiv nach + Ihrer Einwilligung. Sie können Ihre Auswahl jederzeit über den Link „Cookie-Einstellungen" im Footer + ändern.

@@ -145,7 +146,31 @@ export default function Datenschutz() {
-

8. Empfänger der Daten

+

8. Google Analytics

+

+ Sofern Sie der Statistik-Kategorie im Cookie-Banner zugestimmt haben, setzen wir „Google Analytics" ein, + einen Webanalysedienst der Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irland. Google + Analytics verwendet Cookies und ähnliche Technologien, die eine Analyse der Benutzung dieser Website + durch Sie ermöglichen. Die dabei erzeugten Informationen (u. a. Ihre IP-Adresse, die vor der Verarbeitung + durch die Aktivierung der IP-Anonymisierung gekürzt wird) werden an einen Server von Google übertragen + und dort gespeichert; eine Verarbeitung kann dabei auch auf Servern außerhalb der EU/des EWR erfolgen. + Die Verarbeitung erfolgt ausschließlich auf Grundlage Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO), + die Sie jederzeit über die Cookie-Einstellungen mit Wirkung für die Zukunft widerrufen können. Weitere + Informationen finden Sie in der{' '} + + Datenschutzerklärung von Google + + . +

+
+ +
+

9. Empfänger der Daten

Ihre Daten werden grundsätzlich nur innerhalb unseres Unternehmens verarbeitet. Eine Weitergabe an Dritte erfolgt nur, soweit dies zur Bearbeitung Ihrer Anfrage erforderlich ist (z. B. an unseren @@ -155,7 +180,7 @@ export default function Datenschutz() {

-

9. Speicherdauer

+

10. Speicherdauer

Sofern in dieser Erklärung keine speziellere Speicherdauer genannt wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die Datenspeicherung entfällt. Gesetzliche @@ -165,7 +190,7 @@ export default function Datenschutz() {

-

10. SSL-/TLS-Verschlüsselung

+

11. SSL-/TLS-Verschlüsselung

Diese Seite nutzt aus Sicherheitsgründen eine SSL-/TLS-Verschlüsselung zur Übertragung Ihrer Daten. Eine verschlüsselte Verbindung erkennen Sie an dem Schloss-Symbol in der Adresszeile Ihres Browsers. @@ -173,7 +198,7 @@ export default function Datenschutz() {

-

11. Änderung dieser Datenschutzerklärung

+

12. Änderung dieser Datenschutzerklärung

Wir passen diese Datenschutzerklärung an, sobald sich die von uns eingesetzten Datenverarbeitungen ändern. Es gilt jeweils die zum Zeitpunkt Ihres Besuchs aktuelle, auf dieser Seite veröffentlichte diff --git a/hifi/api/database/migration_analytics.sql b/hifi/api/database/migration_analytics.sql new file mode 100644 index 0000000..3ce9664 --- /dev/null +++ b/hifi/api/database/migration_analytics.sql @@ -0,0 +1,6 @@ +-- Google Analytics Measurement ID admin-konfigurierbar machen. Additiv, nichts wird geloescht. +-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren" +-- unter Admin-Panel -> Einstellungen -> Datenbank. + +ALTER TABLE app_settings + ADD COLUMN IF NOT EXISTS ga_measurement_id VARCHAR(20) NULL; diff --git a/hifi/api/database/schema.sql b/hifi/api/database/schema.sql index 1d254af..c55da29 100644 --- a/hifi/api/database/schema.sql +++ b/hifi/api/database/schema.sql @@ -24,6 +24,7 @@ CREATE TABLE app_settings ( whatsapp VARCHAR(50) NULL, contact_email VARCHAR(150) NULL, hero_image_path VARCHAR(255) NULL, + ga_measurement_id VARCHAR(20) NULL, mail_host VARCHAR(255) NULL, mail_port INT NULL, mail_username VARCHAR(255) NULL, diff --git a/hifi/api/src/Controllers/SiteSettingsController.php b/hifi/api/src/Controllers/SiteSettingsController.php index c90fc6f..961377d 100644 --- a/hifi/api/src/Controllers/SiteSettingsController.php +++ b/hifi/api/src/Controllers/SiteSettingsController.php @@ -14,13 +14,14 @@ class SiteSettingsController 'whatsapp' => null, 'contact_email' => 'info@hifi-planet-amorbach.de', 'hero_image_path' => null, + 'ga_measurement_id' => null, ]; public static function show(): void { try { $stmt = Database::connection()->query( - 'SELECT phone, whatsapp, contact_email, hero_image_path FROM app_settings WHERE id = 1' + 'SELECT phone, whatsapp, contact_email, hero_image_path, ga_measurement_id FROM app_settings WHERE id = 1' ); $row = $stmt->fetch(); } catch (\Throwable $e) { @@ -43,13 +44,14 @@ class SiteSettingsController $db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)'); $stmt = $db->prepare( - 'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ? WHERE id = 1' + 'UPDATE app_settings SET phone = ?, whatsapp = ?, contact_email = ?, hero_image_path = ?, ga_measurement_id = ? WHERE id = 1' ); $stmt->execute([ trim($body['phone'] ?? '') ?: null, trim($body['whatsapp'] ?? '') ?: null, trim($body['contact_email'] ?? '') ?: null, trim($body['hero_image_path'] ?? '') ?: null, + trim($body['ga_measurement_id'] ?? '') ?: null, ]); self::show(); diff --git a/hifi/api/src/Support/Schema.php b/hifi/api/src/Support/Schema.php index c24ecae..b8499f9 100644 --- a/hifi/api/src/Support/Schema.php +++ b/hifi/api/src/Support/Schema.php @@ -35,6 +35,7 @@ class Schema whatsapp VARCHAR(50) NULL, contact_email VARCHAR(150) NULL, hero_image_path VARCHAR(255) NULL, + ga_measurement_id VARCHAR(20) NULL, mail_host VARCHAR(255) NULL, mail_port INT NULL, mail_username VARCHAR(255) NULL, @@ -242,6 +243,7 @@ class Schema 'whatsapp' => 'VARCHAR(50) NULL', 'contact_email' => 'VARCHAR(150) NULL', 'hero_image_path' => 'VARCHAR(255) NULL', + 'ga_measurement_id' => 'VARCHAR(20) NULL', 'mail_host' => 'VARCHAR(255) NULL', 'mail_port' => 'INT NULL', 'mail_username' => 'VARCHAR(255) NULL',