update
This commit is contained in:
357
Frontend/src/pages/admin/CookieSettingsPage.tsx
Normal file
357
Frontend/src/pages/admin/CookieSettingsPage.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Shield, SlidersHorizontal, Info, Save, Globe } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import adminPrivacyService, {
|
||||
CookieIntegrationSettings,
|
||||
CookieIntegrationSettingsResponse,
|
||||
CookiePolicySettings,
|
||||
CookiePolicySettingsResponse,
|
||||
} from '../../services/api/adminPrivacyService';
|
||||
import { Loading } from '../../components/common';
|
||||
|
||||
const CookieSettingsPage: React.FC = () => {
|
||||
const [policy, setPolicy] = useState<CookiePolicySettings>({
|
||||
analytics_enabled: true,
|
||||
marketing_enabled: true,
|
||||
preferences_enabled: true,
|
||||
});
|
||||
const [integrations, setIntegrations] = useState<CookieIntegrationSettings>({
|
||||
ga_measurement_id: '',
|
||||
fb_pixel_id: '',
|
||||
});
|
||||
const [policyMeta, setPolicyMeta] = useState<
|
||||
Pick<CookiePolicySettingsResponse, 'updated_at' | 'updated_by'> | null
|
||||
>(null);
|
||||
const [integrationMeta, setIntegrationMeta] = useState<
|
||||
Pick<CookieIntegrationSettingsResponse, 'updated_at' | 'updated_by'> | null
|
||||
>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [policyRes, integrationRes] = await Promise.all([
|
||||
adminPrivacyService.getCookiePolicy(),
|
||||
adminPrivacyService.getIntegrations(),
|
||||
]);
|
||||
setPolicy(policyRes.data);
|
||||
setPolicyMeta({
|
||||
updated_at: policyRes.updated_at,
|
||||
updated_by: policyRes.updated_by,
|
||||
});
|
||||
setIntegrations(integrationRes.data || {});
|
||||
setIntegrationMeta({
|
||||
updated_at: integrationRes.updated_at,
|
||||
updated_by: integrationRes.updated_by,
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to load cookie & integration settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadSettings();
|
||||
}, []);
|
||||
|
||||
const handleToggle = (key: keyof CookiePolicySettings) => {
|
||||
setPolicy((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const [policyRes, integrationRes] = await Promise.all([
|
||||
adminPrivacyService.updateCookiePolicy(policy),
|
||||
adminPrivacyService.updateIntegrations(integrations),
|
||||
]);
|
||||
setPolicy(policyRes.data);
|
||||
setPolicyMeta({
|
||||
updated_at: policyRes.updated_at,
|
||||
updated_by: policyRes.updated_by,
|
||||
});
|
||||
setIntegrations(integrationRes.data || {});
|
||||
setIntegrationMeta({
|
||||
updated_at: integrationRes.updated_at,
|
||||
updated_by: integrationRes.updated_by,
|
||||
});
|
||||
toast.success('Cookie policy and integrations updated successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to update cookie settings');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveIntegrations = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const integrationRes = await adminPrivacyService.updateIntegrations(
|
||||
integrations
|
||||
);
|
||||
setIntegrations(integrationRes.data || {});
|
||||
setIntegrationMeta({
|
||||
updated_at: integrationRes.updated_at,
|
||||
updated_by: integrationRes.updated_by,
|
||||
});
|
||||
toast.success('Integration IDs updated successfully');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Failed to update integration IDs');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Loading fullScreen={false} text="Loading cookie policy..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-6 h-6 text-amber-500" />
|
||||
<h1 className="enterprise-section-title">Cookie & Privacy Controls</h1>
|
||||
</div>
|
||||
<p className="enterprise-section-subtitle max-w-2xl">
|
||||
Define which cookie categories are allowed in the application. These
|
||||
settings control which types of cookies your users can consent to.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-enterprise-primary inline-flex items-center gap-2"
|
||||
>
|
||||
<Save className={`w-4 h-4 ${saving ? 'animate-pulse' : ''}`} />
|
||||
{saving ? 'Saving...' : 'Save changes'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info card */}
|
||||
<div className="enterprise-card flex gap-4 p-4 sm:p-5">
|
||||
<div className="mt-1">
|
||||
<Info className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="space-y-1 text-sm text-gray-700">
|
||||
<p className="font-semibold text-gray-900">
|
||||
How these settings affect the guest experience
|
||||
</p>
|
||||
<p>
|
||||
Disabling a category here prevents it from being offered to guests as
|
||||
part of the cookie consent flow. For example, if marketing cookies are
|
||||
disabled, the website should not load marketing pixels even if a guest
|
||||
previously opted in.
|
||||
</p>
|
||||
{policyMeta?.updated_at && (
|
||||
<p className="text-xs text-gray-500">
|
||||
Last updated on{' '}
|
||||
{new Date(policyMeta.updated_at).toLocaleString(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}{' '}
|
||||
{policyMeta.updated_by ? `by ${policyMeta.updated_by}` : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggles */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="enterprise-card p-5 space-y-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4 text-emerald-500" />
|
||||
Analytics cookies
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Anonymous traffic and performance measurement (e.g. page views,
|
||||
conversion funnels).
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggle('analytics_enabled')}
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition ${
|
||||
policy.analytics_enabled ? 'bg-emerald-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition ${
|
||||
policy.analytics_enabled ? 'translate-x-5' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
When disabled, analytics tracking scripts should not be executed,
|
||||
regardless of user consent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="enterprise-card p-5 space-y-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4 text-pink-500" />
|
||||
Marketing cookies
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Personalised offers, remarketing campaigns, and external ad
|
||||
networks.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggle('marketing_enabled')}
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition ${
|
||||
policy.marketing_enabled ? 'bg-pink-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition ${
|
||||
policy.marketing_enabled ? 'translate-x-5' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
When disabled, do not load any marketing pixels or share data with ad
|
||||
platforms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="enterprise-card p-5 space-y-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-4 h-4 text-indigo-500" />
|
||||
Preference cookies
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Remember guest choices like language, currency, and layout.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggle('preferences_enabled')}
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition ${
|
||||
policy.preferences_enabled ? 'bg-indigo-500' : 'bg-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition ${
|
||||
policy.preferences_enabled ? 'translate-x-5' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
When disabled, the application should avoid persisting non-essential
|
||||
preferences client-side.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Integration IDs */}
|
||||
<div className="enterprise-card p-5 space-y-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="w-5 h-5 text-blue-500" />
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">
|
||||
Third-party integrations (IDs only)
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Configure IDs for supported analytics and marketing platforms. The
|
||||
application will only load these when both the policy and user consent
|
||||
allow it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2 md:items-end">
|
||||
{integrationMeta?.updated_at && (
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Last changed{' '}
|
||||
{new Date(integrationMeta.updated_at).toLocaleString(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
})}{' '}
|
||||
{integrationMeta.updated_by ? `by ${integrationMeta.updated_by}` : ''}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSaveIntegrations}
|
||||
disabled={saving}
|
||||
className="btn-enterprise-secondary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
{saving ? 'Saving IDs...' : 'Save integration IDs'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-800">
|
||||
Google Analytics 4 Measurement ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={integrations.ga_measurement_id || ''}
|
||||
onChange={(e) =>
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
ga_measurement_id: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="G-XXXXXXXXXX"
|
||||
className="enterprise-input text-sm"
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Example: <code className="font-mono">G-ABCDE12345</code>. This is used to
|
||||
load GA4 via gtag.js when analytics cookies are allowed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-800">
|
||||
Meta (Facebook) Pixel ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={integrations.fb_pixel_id || ''}
|
||||
onChange={(e) =>
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
fb_pixel_id: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="123456789012345"
|
||||
className="enterprise-input text-sm"
|
||||
/>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
Numeric ID from your Meta Pixel. The application will only fire pixel
|
||||
events when marketing cookies are allowed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieSettingsPage;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user