chore: transfer repo
This commit is contained in:
45
components/cookies/CookieBanner.tsx
Normal file
45
components/cookies/CookieBanner.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { useCookieConsent } from './CookieContext';
|
||||
|
||||
export function CookieBanner() {
|
||||
const { showBanner, acceptAll, openModal } = useCookieConsent();
|
||||
|
||||
if (!showBanner) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-30 bg-white shadow-lg border-t border-gray-200 px-4 py-3">
|
||||
<div className="container mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<p className="text-sm text-gray-700 text-center sm:text-left">
|
||||
Ova web stranica koristi kolačiće za poboljšanje korisničkog iskustva.
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="underline ml-1 hover:text-black"
|
||||
>
|
||||
Više informacija
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-row gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="px-3 whitespace-nowrap"
|
||||
onClick={openModal}
|
||||
>
|
||||
Prilagodi
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
onClick={acceptAll}
|
||||
>
|
||||
Prihvati sve
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
components/cookies/CookieCategoryCard.tsx
Normal file
36
components/cookies/CookieCategoryCard.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { Heading, Text } from '@/components/ui/Typography';
|
||||
import { ToggleSwitch } from './ToggleSwitch';
|
||||
|
||||
interface CookieCategoryCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
onChange: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function CookieCategoryCard({
|
||||
title,
|
||||
description,
|
||||
enabled,
|
||||
onChange,
|
||||
disabled = false
|
||||
}: CookieCategoryCardProps) {
|
||||
return (
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Heading level={4}>{title}</Heading>
|
||||
<ToggleSwitch
|
||||
enabled={enabled}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<Text size="sm" className="text-gray-600">
|
||||
{description}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
160
components/cookies/CookieContext.tsx
Normal file
160
components/cookies/CookieContext.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
'use client';
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
|
||||
// Define cookie categories and their default state
|
||||
export const COOKIE_CATEGORIES = {
|
||||
NECESSARY: 'necessary',
|
||||
ANALYTICS: 'analytics',
|
||||
MARKETING: 'marketing',
|
||||
PREFERENCES: 'preferences'
|
||||
} as const;
|
||||
|
||||
export type CookieCategory = typeof COOKIE_CATEGORIES[keyof typeof COOKIE_CATEGORIES];
|
||||
|
||||
export interface CookiePreferences {
|
||||
necessary: boolean; // Always true, can't be disabled
|
||||
analytics: boolean;
|
||||
marketing: boolean;
|
||||
preferences: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PREFERENCES: CookiePreferences = {
|
||||
necessary: true, // Always true
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
preferences: false
|
||||
};
|
||||
|
||||
const CONSENT_COOKIE_NAME = 'cookie-consent';
|
||||
const COOKIE_EXPIRY_DAYS = 365;
|
||||
|
||||
interface CookieContextType {
|
||||
preferences: CookiePreferences;
|
||||
hasConsent: boolean;
|
||||
showBanner: boolean;
|
||||
showModal: boolean;
|
||||
acceptAll: () => void;
|
||||
savePreferences: (newPrefs: CookiePreferences) => void;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
resetConsent: () => void;
|
||||
}
|
||||
|
||||
const CookieContext = createContext<CookieContextType | undefined>(undefined);
|
||||
|
||||
export function CookieProvider({ children }: { children: ReactNode }) {
|
||||
const [preferences, setPreferences] = useState<CookiePreferences>(DEFAULT_PREFERENCES);
|
||||
const [hasConsent, setHasConsent] = useState<boolean>(false);
|
||||
const [showBanner, setShowBanner] = useState<boolean>(false);
|
||||
const [showModal, setShowModal] = useState<boolean>(false);
|
||||
|
||||
// Load stored consent preferences on mount (client-side only)
|
||||
useEffect(() => {
|
||||
const storedConsent = Cookies.get(CONSENT_COOKIE_NAME);
|
||||
|
||||
if (storedConsent) {
|
||||
try {
|
||||
const parsedPreferences = JSON.parse(storedConsent);
|
||||
setPreferences({
|
||||
...DEFAULT_PREFERENCES,
|
||||
...parsedPreferences,
|
||||
// Ensure necessary cookies are always enabled
|
||||
necessary: true
|
||||
});
|
||||
setHasConsent(true);
|
||||
setShowBanner(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse cookie consent', error);
|
||||
setShowBanner(true);
|
||||
}
|
||||
} else {
|
||||
// No stored consent, show the banner
|
||||
setShowBanner(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const savePreferences = (newPrefs: CookiePreferences) => {
|
||||
const updatedPrefs = {
|
||||
...newPrefs,
|
||||
necessary: true
|
||||
};
|
||||
|
||||
// Update state
|
||||
setPreferences(updatedPrefs);
|
||||
setHasConsent(true);
|
||||
setShowBanner(false);
|
||||
setShowModal(false);
|
||||
|
||||
// Save to cookie
|
||||
Cookies.set(CONSENT_COOKIE_NAME, JSON.stringify(updatedPrefs), {
|
||||
expires: COOKIE_EXPIRY_DAYS,
|
||||
path: '/',
|
||||
sameSite: 'strict'
|
||||
});
|
||||
|
||||
applyPreferences(updatedPrefs);
|
||||
};
|
||||
|
||||
// Accept all cookies
|
||||
const acceptAll = () => {
|
||||
const allAccepted = {
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
preferences: true
|
||||
};
|
||||
|
||||
savePreferences(allAccepted);
|
||||
};
|
||||
|
||||
// Reset consent (for testing)
|
||||
const resetConsent = () => {
|
||||
Cookies.remove(CONSENT_COOKIE_NAME);
|
||||
setPreferences(DEFAULT_PREFERENCES);
|
||||
setHasConsent(false);
|
||||
setShowBanner(true);
|
||||
};
|
||||
|
||||
const openModal = () => setShowModal(true);
|
||||
const closeModal = () => setShowModal(false);
|
||||
|
||||
// Apply preferences based on consent
|
||||
const applyPreferences = (prefs: CookiePreferences) => {
|
||||
// Apply analytics preference (without console logs)
|
||||
if (prefs.analytics) {
|
||||
// Enable analytics tracking
|
||||
// Add analytics initialization code here if needed
|
||||
} else {
|
||||
// Disable analytics tracking
|
||||
// Add analytics disabling code here if needed
|
||||
}
|
||||
};
|
||||
|
||||
const value = {
|
||||
preferences,
|
||||
hasConsent,
|
||||
showBanner,
|
||||
showModal,
|
||||
acceptAll,
|
||||
savePreferences,
|
||||
openModal,
|
||||
closeModal,
|
||||
resetConsent
|
||||
};
|
||||
|
||||
return (
|
||||
<CookieContext.Provider value={value}>
|
||||
{children}
|
||||
</CookieContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCookieConsent() {
|
||||
const context = useContext(CookieContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useCookieConsent must be used within a CookieProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
164
components/cookies/CookieSettingsModal.tsx
Normal file
164
components/cookies/CookieSettingsModal.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Heading, Text } from '@/components/ui/Typography';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CookieCategoryCard } from './CookieCategoryCard';
|
||||
import { CookiePreferences, useCookieConsent } from './CookieContext';
|
||||
|
||||
// Cookie category descriptions
|
||||
const COOKIE_DESCRIPTIONS = {
|
||||
necessary: {
|
||||
title: 'Neophodni kolačići',
|
||||
description: 'Ovi kolačići su neophodni za funkcioniranje web stranice i ne mogu biti isključeni. Oni omogućuju osnovne funkcionalnosti poput navigacije stranice i pristupa sigurnim područjima.'
|
||||
},
|
||||
analytics: {
|
||||
title: 'Analitički kolačići',
|
||||
description: 'Ovi kolačići nam pomažu razumjeti kako posjetitelji koriste našu web stranicu, prikupljajući anonimne statističke podatke. Oni nam pomažu poboljšati korisničko iskustvo i performanse stranice.'
|
||||
},
|
||||
marketing: {
|
||||
title: 'Marketinški kolačići',
|
||||
description: 'Ovi kolačići se koriste za praćenje posjetitelja na web stranicama. Namjera je prikazati oglase koji su relevantni i privlačni za pojedinog korisnika i time vrijedniji za izdavače i vanjske oglašivače.'
|
||||
},
|
||||
preferences: {
|
||||
title: 'Kolačići za personalizaciju',
|
||||
description: 'Ovi kolačići omogućuju web stranici da zapamti izbore koje ste napravili (poput korisničkog imena, jezika ili regije) i pružaju poboljšane, personalizirane značajke.'
|
||||
}
|
||||
};
|
||||
|
||||
export function CookieSettingsModal() {
|
||||
const { preferences, showModal, savePreferences, closeModal } = useCookieConsent();
|
||||
const [localPreferences, setLocalPreferences] = useState<CookiePreferences>(preferences);
|
||||
|
||||
// Sync with parent preferences when they change
|
||||
useEffect(() => {
|
||||
setLocalPreferences(preferences);
|
||||
}, [preferences]);
|
||||
|
||||
const handleToggle = (category: keyof CookiePreferences) => {
|
||||
if (category === 'necessary') return; // Necessary cookies can't be disabled
|
||||
|
||||
setLocalPreferences(prev => ({
|
||||
...prev,
|
||||
[category]: !prev[category]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
savePreferences(localPreferences);
|
||||
};
|
||||
|
||||
if (!showModal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 z-50"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="p-4 border-b flex justify-between items-center sticky top-0 bg-white z-20">
|
||||
<Heading level={3}>Postavke kolačića</Heading>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
aria-label="Close"
|
||||
>
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<Text className="mb-6">
|
||||
Ova web stranica koristi kolačiće za poboljšanje korisničkog iskustva. Možete prilagoditi svoje postavke
|
||||
kolačića omogućavanjem ili onemogućavanjem svake kategorije. Kolačići označeni kao "Neophodni"
|
||||
su potrebni za osnovne funkcije web stranice i ne mogu biti isključeni.
|
||||
</Text>
|
||||
|
||||
{/* Cookie categories */}
|
||||
<div className="space-y-6">
|
||||
{/* Necessary cookies - always enabled */}
|
||||
<CookieCategoryCard
|
||||
title={COOKIE_DESCRIPTIONS.necessary.title}
|
||||
description={COOKIE_DESCRIPTIONS.necessary.description}
|
||||
enabled={true}
|
||||
onChange={() => {}}
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
{/* Analytics cookies */}
|
||||
<CookieCategoryCard
|
||||
title={COOKIE_DESCRIPTIONS.analytics.title}
|
||||
description={COOKIE_DESCRIPTIONS.analytics.description}
|
||||
enabled={localPreferences.analytics}
|
||||
onChange={() => handleToggle('analytics')}
|
||||
/>
|
||||
|
||||
{/* Marketing cookies */}
|
||||
<CookieCategoryCard
|
||||
title={COOKIE_DESCRIPTIONS.marketing.title}
|
||||
description={COOKIE_DESCRIPTIONS.marketing.description}
|
||||
enabled={localPreferences.marketing}
|
||||
onChange={() => handleToggle('marketing')}
|
||||
/>
|
||||
|
||||
{/* Preference cookies */}
|
||||
<CookieCategoryCard
|
||||
title={COOKIE_DESCRIPTIONS.preferences.title}
|
||||
description={COOKIE_DESCRIPTIONS.preferences.description}
|
||||
enabled={localPreferences.preferences}
|
||||
onChange={() => handleToggle('preferences')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<Heading level={4} className="mb-3">O kolačićima</Heading>
|
||||
<Text className="mb-4">
|
||||
Kolačići su male tekstualne datoteke koje web stranice postavljaju na vaš uređaj prilikom posjeta.
|
||||
Koriste se za pamćenje vaših postavki, poboljšanje funkcionalnosti i prikupljanje analitičkih podataka.
|
||||
</Text>
|
||||
|
||||
<Heading level={4} className="mb-2 mt-4">Kako koristimo kolačiće</Heading>
|
||||
<Text className="mb-4">
|
||||
Koristimo različite vrste kolačića za različite svrhe. Neki su neophodni za rad web stranice,
|
||||
dok drugi nam pomažu optimizirati sadržaj i korisničko iskustvo.
|
||||
</Text>
|
||||
|
||||
<Heading level={4} className="mb-2 mt-4">Vaša prava</Heading>
|
||||
<Text className="mb-4">
|
||||
U skladu s EU regulativom o kolačićima, omogućujemo vam upravljanje postavkama kolačića.
|
||||
Više informacija možete pronaći u našim <a href="/privacy-policy" className="underline">Pravilima privatnosti</a> i
|
||||
<a href="/terms-of-service" className="underline ml-1">Uvjetima korištenja</a>.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with buttons */}
|
||||
<div className="p-4 border-t sticky bottom-0 bg-white">
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Odustani
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Spremi postavke
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
components/cookies/ToggleSwitch.tsx
Normal file
27
components/cookies/ToggleSwitch.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
enabled: boolean;
|
||||
onChange: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ToggleSwitch({ enabled, onChange, disabled = false }: ToggleSwitchProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onChange}
|
||||
disabled={disabled}
|
||||
className={`relative inline-flex items-center ${disabled ? 'cursor-not-allowed opacity-80' : 'cursor-pointer'}`}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
>
|
||||
<div className={`w-10 h-6 rounded-full transition ${enabled ? 'bg-primary' : 'bg-gray-300'}`}></div>
|
||||
<div
|
||||
className={`absolute inset-y-0 left-0 w-6 h-6 bg-white rounded-full border border-gray-300 transform transition ${
|
||||
enabled ? 'translate-x-4' : 'translate-x-0'
|
||||
}`}
|
||||
></div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
17
components/cookies/index.tsx
Normal file
17
components/cookies/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { CookieBanner } from './CookieBanner';
|
||||
import { CookieProvider } from './CookieContext';
|
||||
import { CookieSettingsModal } from './CookieSettingsModal';
|
||||
|
||||
export function CookieConsent({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<CookieProvider>
|
||||
{children}
|
||||
<CookieBanner />
|
||||
<CookieSettingsModal />
|
||||
</CookieProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { useCookieConsent } from './CookieContext';
|
||||
Reference in New Issue
Block a user