chore: transfer repo

This commit is contained in:
Danijel
2026-01-19 20:21:14 +01:00
commit 7d2fb0c737
213 changed files with 18085 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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;
}

View 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>
</>
);
}

View 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>
);
}

View 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';