Files
Hotel-Booking/Frontend/src/pages/admin/CookieSettingsPage.tsx
Iliyan Angelov b818d645a9 updates
2025-12-07 20:36:17 +02:00

403 lines
16 KiB
TypeScript

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 '../../features/content/services/adminPrivacyService';
import Loading from '../../shared/components/Loading';
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-10 pb-8 animate-fade-in">
{}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 pb-6 border-b border-gray-200/60">
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="p-2.5 rounded-lg bg-gradient-to-br from-[var(--luxury-gold)]/10 to-[var(--luxury-gold)]/5 border border-[var(--luxury-gold)]/20 shadow-sm">
<Shield className="w-6 h-6 text-[var(--luxury-gold)]" />
</div>
<h1 className="enterprise-section-title">Cookie & Privacy Controls</h1>
</div>
<p className="enterprise-section-subtitle max-w-2xl text-gray-600">
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 whitespace-nowrap"
>
<Save className={`w-4 h-4 ${saving ? 'animate-pulse' : ''}`} />
{saving ? 'Saving...' : 'Save All Changes'}
</button>
</div>
{}
<div className="enterprise-card flex gap-5 p-6 bg-gradient-to-br from-blue-50/50 to-indigo-50/30 border-blue-100/60">
<div className="mt-0.5 flex-shrink-0">
<div className="p-2.5 rounded-lg bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40">
<Info className="w-5 h-5 text-blue-600" />
</div>
</div>
<div className="space-y-2.5 flex-1">
<p className="font-semibold text-gray-900 text-base">
How these settings affect the guest experience
</p>
<p className="text-sm text-gray-700 leading-relaxed">
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 && (
<div className="pt-2 border-t border-gray-200/60">
<p className="text-xs text-gray-500 font-medium">
Last updated on{' '}
<span className="text-gray-700">
{new Date(policyMeta.updated_at).toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
})}
</span>
{policyMeta.updated_by && (
<>
{' '}by <span className="text-gray-700 font-semibold">{policyMeta.updated_by}</span>
</>
)}
</p>
</div>
)}
</div>
</div>
{}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="enterprise-card p-6 space-y-4 group">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2.5">
<div className="p-1.5 rounded-md bg-emerald-50 border border-emerald-100">
<SlidersHorizontal className="w-4 h-4 text-emerald-600" />
</div>
<p className="font-bold text-gray-900 text-base">
Analytics Cookies
</p>
</div>
<p className="text-sm text-gray-600 leading-relaxed">
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-8 w-14 items-center rounded-full transition-all duration-300 shadow-lg ${
policy.analytics_enabled
? 'bg-gradient-to-r from-emerald-500 to-emerald-600 shadow-emerald-500/30'
: 'bg-gray-300 shadow-gray-300/20'
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-all duration-300 ${
policy.analytics_enabled ? 'translate-x-7' : 'translate-x-1'
}`}
/>
</button>
</div>
<div className="pt-3 border-t border-gray-100">
<p className="text-xs text-gray-500 leading-relaxed">
When disabled, analytics tracking scripts should not be executed,
regardless of user consent.
</p>
</div>
</div>
<div className="enterprise-card p-6 space-y-4 group">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2.5">
<div className="p-1.5 rounded-md bg-pink-50 border border-pink-100">
<SlidersHorizontal className="w-4 h-4 text-pink-600" />
</div>
<p className="font-bold text-gray-900 text-base">
Marketing Cookies
</p>
</div>
<p className="text-sm text-gray-600 leading-relaxed">
Personalised offers, remarketing campaigns, and external ad
networks.
</p>
</div>
<button
type="button"
onClick={() => handleToggle('marketing_enabled')}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-all duration-300 shadow-lg ${
policy.marketing_enabled
? 'bg-gradient-to-r from-pink-500 to-pink-600 shadow-pink-500/30'
: 'bg-gray-300 shadow-gray-300/20'
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-all duration-300 ${
policy.marketing_enabled ? 'translate-x-7' : 'translate-x-1'
}`}
/>
</button>
</div>
<div className="pt-3 border-t border-gray-100">
<p className="text-xs text-gray-500 leading-relaxed">
When disabled, do not load any marketing pixels or share data with ad
platforms.
</p>
</div>
</div>
<div className="enterprise-card p-6 space-y-4 group">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2.5">
<div className="p-1.5 rounded-md bg-indigo-50 border border-indigo-100">
<SlidersHorizontal className="w-4 h-4 text-indigo-600" />
</div>
<p className="font-bold text-gray-900 text-base">
Preference Cookies
</p>
</div>
<p className="text-sm text-gray-600 leading-relaxed">
Remember guest choices like language, currency, and layout.
</p>
</div>
<button
type="button"
onClick={() => handleToggle('preferences_enabled')}
className={`relative inline-flex h-8 w-14 items-center rounded-full transition-all duration-300 shadow-lg ${
policy.preferences_enabled
? 'bg-gradient-to-r from-indigo-500 to-indigo-600 shadow-indigo-500/30'
: 'bg-gray-300 shadow-gray-300/20'
}`}
>
<span
className={`inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-all duration-300 ${
policy.preferences_enabled ? 'translate-x-7' : 'translate-x-1'
}`}
/>
</button>
</div>
<div className="pt-3 border-t border-gray-100">
<p className="text-xs text-gray-500 leading-relaxed">
When disabled, the application should avoid persisting non-essential
preferences client-side.
</p>
</div>
</div>
</div>
{}
<div className="enterprise-card p-6 space-y-6">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between pb-4 border-b border-gray-200/60">
<div className="flex items-start gap-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500/10 to-indigo-500/10 border border-blue-200/40 mt-0.5">
<Globe className="w-5 h-5 text-blue-600" />
</div>
<div className="space-y-1.5">
<p className="font-bold text-gray-900 text-lg">
Third-Party Integrations
</p>
<p className="text-sm text-gray-600 leading-relaxed max-w-xl">
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-3 md:items-end md:min-w-[200px]">
{integrationMeta?.updated_at && (
<div className="text-right">
<p className="text-xs text-gray-500 font-medium">
Last changed
</p>
<p className="text-xs text-gray-700 mt-0.5">
{new Date(integrationMeta.updated_at).toLocaleString(undefined, {
dateStyle: 'medium',
timeStyle: 'short',
})}
</p>
{integrationMeta.updated_by && (
<p className="text-xs text-gray-600 mt-0.5">
by <span className="font-semibold">{integrationMeta.updated_by}</span>
</p>
)}
</div>
)}
<button
type="button"
onClick={handleSaveIntegrations}
disabled={saving}
className="btn-enterprise-secondary inline-flex items-center gap-2 whitespace-nowrap"
>
<Save className="w-3.5 h-3.5" />
{saving ? 'Saving...' : 'Save Integration IDs'}
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<label className="block text-sm font-semibold text-gray-900 tracking-wide">
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-xs text-gray-500 leading-relaxed">
Example: <code className="font-mono text-gray-700 bg-gray-50 px-1.5 py-0.5 rounded border border-gray-200">G-ABCDE12345</code>. This is used to
load GA4 via gtag.js when analytics cookies are allowed.
</p>
</div>
<div className="space-y-3">
<label className="block text-sm font-semibold text-gray-900 tracking-wide">
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-xs text-gray-500 leading-relaxed">
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;