Improve GDPR implementation
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Suspense } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { BrowserRouter, useNavigate } from 'react-router-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { ErrorBoundary } from './components/layout/ErrorBoundary';
|
||||
@@ -11,6 +11,7 @@ import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { SSEProvider } from './contexts/SSEContext';
|
||||
import GlobalSubscriptionHandler from './components/auth/GlobalSubscriptionHandler';
|
||||
import { CookieBanner } from './components/ui/CookieConsent';
|
||||
import i18n from './i18n';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -24,6 +25,30 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
});
|
||||
|
||||
function AppContent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<LoadingSpinner overlay />}>
|
||||
<AppRouter />
|
||||
<GlobalSubscriptionHandler />
|
||||
<CookieBanner onPreferencesClick={() => navigate('/cookie-preferences')} />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#363636',
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -37,22 +62,9 @@ function App() {
|
||||
>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<SSEProvider>
|
||||
<Suspense fallback={<LoadingSpinner overlay />}>
|
||||
<AppRouter />
|
||||
<GlobalSubscriptionHandler />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#363636',
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</SSEProvider>
|
||||
<SSEProvider>
|
||||
<AppContent />
|
||||
</SSEProvider>
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
|
||||
88
frontend/src/api/services/consent.ts
Normal file
88
frontend/src/api/services/consent.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
// ================================================================
|
||||
// frontend/src/api/services/consent.ts
|
||||
// ================================================================
|
||||
/**
|
||||
* Consent Service - GDPR Compliance
|
||||
*
|
||||
* Backend API: services/auth/app/api/consent.py
|
||||
*
|
||||
* Last Updated: 2025-10-16
|
||||
*/
|
||||
import { apiClient } from '../client';
|
||||
|
||||
export interface ConsentRequest {
|
||||
terms_accepted: boolean;
|
||||
privacy_accepted: boolean;
|
||||
marketing_consent?: boolean;
|
||||
analytics_consent?: boolean;
|
||||
consent_method: 'registration' | 'settings' | 'cookie_banner';
|
||||
consent_version?: string;
|
||||
}
|
||||
|
||||
export interface ConsentResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
terms_accepted: boolean;
|
||||
privacy_accepted: boolean;
|
||||
marketing_consent: boolean;
|
||||
analytics_consent: boolean;
|
||||
consent_version: string;
|
||||
consent_method: string;
|
||||
consented_at: string;
|
||||
withdrawn_at: string | null;
|
||||
}
|
||||
|
||||
export interface ConsentHistoryResponse {
|
||||
id: string;
|
||||
user_id: string;
|
||||
action: string;
|
||||
consent_snapshot: Record<string, any>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export class ConsentService {
|
||||
private readonly baseUrl = '/auth';
|
||||
|
||||
/**
|
||||
* Record user consent for data processing
|
||||
* GDPR Article 7 - Conditions for consent
|
||||
*/
|
||||
async recordConsent(consentData: ConsentRequest): Promise<ConsentResponse> {
|
||||
return apiClient.post<ConsentResponse>(`${this.baseUrl}/consent`, consentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current active consent for user
|
||||
*/
|
||||
async getCurrentConsent(): Promise<ConsentResponse | null> {
|
||||
return apiClient.get<ConsentResponse | null>(`${this.baseUrl}/consent/current`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete consent history for user
|
||||
* GDPR Article 7(1) - Demonstrating consent
|
||||
*/
|
||||
async getConsentHistory(): Promise<ConsentHistoryResponse[]> {
|
||||
return apiClient.get<ConsentHistoryResponse[]>(`${this.baseUrl}/consent/history`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user consent preferences
|
||||
* GDPR Article 7(3) - Withdrawal of consent
|
||||
*/
|
||||
async updateConsent(consentData: ConsentRequest): Promise<ConsentResponse> {
|
||||
return apiClient.put<ConsentResponse>(`${this.baseUrl}/consent`, consentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw all consent
|
||||
* GDPR Article 7(3) - Right to withdraw consent
|
||||
*/
|
||||
async withdrawConsent(): Promise<{ message: string; withdrawn_count: number }> {
|
||||
return apiClient.post<{ message: string; withdrawn_count: number }>(
|
||||
`${this.baseUrl}/consent/withdraw`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const consentService = new ConsentService();
|
||||
@@ -299,6 +299,53 @@ export class SubscriptionService {
|
||||
doesAnalyticsLevelMeetMinimum(level: AnalyticsLevel, minimumRequired: AnalyticsLevel): boolean {
|
||||
return ANALYTICS_HIERARCHY[level] >= ANALYTICS_HIERARCHY[minimumRequired];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel subscription - Downgrade to read-only mode
|
||||
*/
|
||||
async cancelSubscription(tenantId: string, reason?: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
status: string;
|
||||
cancellation_effective_date: string;
|
||||
days_remaining: number;
|
||||
read_only_mode_starts: string;
|
||||
}> {
|
||||
return apiClient.post('/subscriptions/cancel', {
|
||||
tenant_id: tenantId,
|
||||
reason: reason || ''
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactivate a cancelled or inactive subscription
|
||||
*/
|
||||
async reactivateSubscription(tenantId: string, plan: string = 'starter'): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
status: string;
|
||||
plan: string;
|
||||
next_billing_date: string | null;
|
||||
}> {
|
||||
return apiClient.post('/subscriptions/reactivate', {
|
||||
tenant_id: tenantId,
|
||||
plan
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription status including read-only mode info
|
||||
*/
|
||||
async getSubscriptionStatus(tenantId: string): Promise<{
|
||||
tenant_id: string;
|
||||
status: string;
|
||||
plan: string;
|
||||
is_read_only: boolean;
|
||||
cancellation_effective_date: string | null;
|
||||
days_until_inactive: number | null;
|
||||
}> {
|
||||
return apiClient.get(`/subscriptions/${tenantId}/status`);
|
||||
}
|
||||
}
|
||||
|
||||
export const subscriptionService = new SubscriptionService();
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
/**
|
||||
* User registration request
|
||||
* Backend: services/auth/app/schemas/auth.py:15-24 (UserRegistration)
|
||||
* Backend: services/auth/app/schemas/auth.py:15-29 (UserRegistration)
|
||||
*/
|
||||
export interface UserRegistration {
|
||||
email: string; // EmailStr - validated email format
|
||||
@@ -29,6 +29,11 @@ export interface UserRegistration {
|
||||
subscription_plan?: string | null; // Default: "starter", options: starter, professional, enterprise
|
||||
use_trial?: boolean | null; // Default: false - Whether to use trial period
|
||||
payment_method_id?: string | null; // Stripe payment method ID
|
||||
// GDPR Consent fields
|
||||
terms_accepted?: boolean; // Default: true - Accept terms of service
|
||||
privacy_accepted?: boolean; // Default: true - Accept privacy policy
|
||||
marketing_consent?: boolean; // Default: false - Consent to marketing communications
|
||||
analytics_consent?: boolean; // Default: false - Consent to analytics cookies
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,8 @@ interface SimpleUserRegistration {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
acceptTerms: boolean;
|
||||
marketingConsent: boolean;
|
||||
analyticsConsent: boolean;
|
||||
}
|
||||
|
||||
// Define the steps for the registration process
|
||||
@@ -41,7 +43,9 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
acceptTerms: false
|
||||
acceptTerms: false,
|
||||
marketingConsent: false,
|
||||
analyticsConsent: false
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Partial<SimpleUserRegistration>>({});
|
||||
@@ -135,10 +139,15 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
subscription_plan: selectedPlan,
|
||||
use_trial: useTrial,
|
||||
payment_method_id: paymentMethodId,
|
||||
// Include consent data
|
||||
terms_accepted: formData.acceptTerms,
|
||||
privacy_accepted: formData.acceptTerms,
|
||||
marketing_consent: formData.marketingConsent,
|
||||
analytics_consent: formData.analyticsConsent,
|
||||
};
|
||||
|
||||
|
||||
await register(registrationData);
|
||||
|
||||
|
||||
showSuccessToast(t('auth:register.registering', '¡Bienvenido! Tu cuenta ha sido creada correctamente.'), {
|
||||
title: t('auth:alerts.success_create', 'Cuenta creada exitosamente')
|
||||
});
|
||||
@@ -407,15 +416,47 @@ export const RegisterForm: React.FC<RegisterFormProps> = ({
|
||||
/>
|
||||
<label htmlFor="acceptTerms" className="text-sm text-text-secondary cursor-pointer">
|
||||
Acepto los{' '}
|
||||
<a href="#" className="text-color-primary hover:text-color-primary-dark underline">
|
||||
<a href="/terms" target="_blank" rel="noopener noreferrer" className="text-color-primary hover:text-color-primary-dark underline">
|
||||
términos y condiciones
|
||||
</a>{' '}
|
||||
de uso
|
||||
y la{' '}
|
||||
<a href="/privacy" target="_blank" rel="noopener noreferrer" className="text-color-primary hover:text-color-primary-dark underline">
|
||||
política de privacidad
|
||||
</a>{' '}
|
||||
<span className="text-color-error">*</span>
|
||||
</label>
|
||||
</div>
|
||||
{errors.acceptTerms && (
|
||||
<p className="text-color-error text-sm ml-7">{errors.acceptTerms}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="marketingConsent"
|
||||
checked={formData.marketingConsent}
|
||||
onChange={handleInputChange('marketingConsent')}
|
||||
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<label htmlFor="marketingConsent" className="text-sm text-text-secondary cursor-pointer">
|
||||
Deseo recibir comunicaciones de marketing y promociones
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="analyticsConsent"
|
||||
checked={formData.analyticsConsent}
|
||||
onChange={handleInputChange('analyticsConsent')}
|
||||
className="mt-1 h-4 w-4 rounded border-border-primary text-color-primary focus:ring-color-primary focus:ring-offset-0"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<label htmlFor="analyticsConsent" className="text-sm text-text-secondary cursor-pointer">
|
||||
Acepto el uso de cookies analíticas para mejorar la experiencia
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
|
||||
167
frontend/src/components/ui/CookieConsent/CookieBanner.tsx
Normal file
167
frontend/src/components/ui/CookieConsent/CookieBanner.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '../Button';
|
||||
import { X, Cookie, Settings } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { CookiePreferences, getCookieConsent, saveCookieConsent } from './cookieUtils';
|
||||
|
||||
interface CookieBannerProps {
|
||||
onPreferencesClick?: () => void;
|
||||
}
|
||||
|
||||
export const CookieBanner: React.FC<CookieBannerProps> = ({ onPreferencesClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const consent = getCookieConsent();
|
||||
if (!consent) {
|
||||
setIsVisible(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
const preferences: CookiePreferences = {
|
||||
essential: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
preferences: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
};
|
||||
saveCookieConsent(preferences);
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleAcceptEssential = () => {
|
||||
const preferences: CookiePreferences = {
|
||||
essential: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
preferences: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
};
|
||||
saveCookieConsent(preferences);
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleCustomize = () => {
|
||||
if (onPreferencesClick) {
|
||||
onPreferencesClick();
|
||||
}
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg transition-all duration-300',
|
||||
isMinimized ? 'h-16' : 'h-auto'
|
||||
)}
|
||||
role="dialog"
|
||||
aria-label={t('common:cookie.banner_title', 'Cookie Consent')}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
{isMinimized ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cookie className="w-5 h-5 text-amber-600" />
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{t('common:cookie.minimized_message', 'We use cookies to enhance your experience.')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setIsMinimized(false)}
|
||||
>
|
||||
{t('common:cookie.show_details', 'Show Details')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<Cookie className="w-6 h-6 text-amber-600 mt-1 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{t('common:cookie.banner_title', 'We Value Your Privacy')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
{t(
|
||||
'common:cookie.banner_description',
|
||||
'We use cookies and similar technologies to provide, protect, and improve our services. Some cookies are essential for the site to function, while others help us understand how you use our services and provide personalized features.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
{t(
|
||||
'common:cookie.banner_description_2',
|
||||
'By clicking "Accept All", you consent to our use of all cookies. You can customize your preferences or accept only essential cookies.'
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<a
|
||||
href="/cookies"
|
||||
className="text-sm text-primary-600 hover:text-primary-700 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('common:cookie.learn_more', 'Learn more about cookies')}
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="/privacy"
|
||||
className="text-sm text-primary-600 hover:text-primary-700 underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('common:cookie.privacy_policy', 'Privacy Policy')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={t('common:cookie.minimize', 'Minimize')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||
<Button
|
||||
onClick={handleAcceptAll}
|
||||
variant="primary"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
{t('common:cookie.accept_all', 'Accept All')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAcceptEssential}
|
||||
variant="outline"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
{t('common:cookie.accept_essential', 'Essential Only')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCustomize}
|
||||
variant="ghost"
|
||||
className="flex-1 sm:flex-none"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
{t('common:cookie.customize', 'Customize')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieBanner;
|
||||
101
frontend/src/components/ui/CookieConsent/cookieUtils.ts
Normal file
101
frontend/src/components/ui/CookieConsent/cookieUtils.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
export interface CookiePreferences {
|
||||
essential: boolean;
|
||||
analytics: boolean;
|
||||
marketing: boolean;
|
||||
preferences: boolean;
|
||||
timestamp: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const COOKIE_CONSENT_KEY = 'bakery_cookie_consent';
|
||||
const COOKIE_CONSENT_VERSION = '1.0';
|
||||
|
||||
export const getCookieConsent = (): CookiePreferences | null => {
|
||||
try {
|
||||
const stored = localStorage.getItem(COOKIE_CONSENT_KEY);
|
||||
if (!stored) return null;
|
||||
|
||||
const consent = JSON.parse(stored) as CookiePreferences;
|
||||
|
||||
if (consent.version !== COOKIE_CONSENT_VERSION) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return consent;
|
||||
} catch (error) {
|
||||
console.error('Error reading cookie consent:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const saveCookieConsent = (preferences: CookiePreferences): void => {
|
||||
try {
|
||||
localStorage.setItem(COOKIE_CONSENT_KEY, JSON.stringify(preferences));
|
||||
|
||||
initializeAnalytics(preferences);
|
||||
initializeMarketing(preferences);
|
||||
} catch (error) {
|
||||
console.error('Error saving cookie consent:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const clearCookieConsent = (): void => {
|
||||
try {
|
||||
localStorage.removeItem(COOKIE_CONSENT_KEY);
|
||||
} catch (error) {
|
||||
console.error('Error clearing cookie consent:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const hasConsent = (category: keyof Omit<CookiePreferences, 'timestamp' | 'version'>): boolean => {
|
||||
const consent = getCookieConsent();
|
||||
if (!consent) return false;
|
||||
return consent[category];
|
||||
};
|
||||
|
||||
const initializeAnalytics = (preferences: CookiePreferences): void => {
|
||||
if (preferences.analytics) {
|
||||
console.log('Analytics cookies enabled');
|
||||
} else {
|
||||
console.log('Analytics cookies disabled');
|
||||
}
|
||||
};
|
||||
|
||||
const initializeMarketing = (preferences: CookiePreferences): void => {
|
||||
if (preferences.marketing) {
|
||||
console.log('Marketing cookies enabled');
|
||||
} else {
|
||||
console.log('Marketing cookies disabled');
|
||||
}
|
||||
};
|
||||
|
||||
export const getCookieCategories = () => [
|
||||
{
|
||||
id: 'essential',
|
||||
name: 'Essential Cookies',
|
||||
description: 'Required for the website to function. These cookies enable core functionality such as security, authentication, and session management. They cannot be disabled.',
|
||||
required: true,
|
||||
examples: ['Session tokens', 'Authentication cookies', 'Security tokens']
|
||||
},
|
||||
{
|
||||
id: 'preferences',
|
||||
name: 'Preference Cookies',
|
||||
description: 'Allow the website to remember information that changes the way the website behaves or looks, such as your preferred language or region.',
|
||||
required: false,
|
||||
examples: ['Language preference', 'Theme preference', 'Region settings']
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
name: 'Analytics Cookies',
|
||||
description: 'Help us understand how visitors interact with our website by collecting and reporting information anonymously.',
|
||||
required: false,
|
||||
examples: ['Google Analytics', 'Page views', 'User behavior tracking']
|
||||
},
|
||||
{
|
||||
id: 'marketing',
|
||||
name: 'Marketing Cookies',
|
||||
description: 'Used to track visitors across websites to display relevant advertisements and marketing campaigns.',
|
||||
required: false,
|
||||
examples: ['Advertising cookies', 'Retargeting pixels', 'Social media cookies']
|
||||
}
|
||||
];
|
||||
3
frontend/src/components/ui/CookieConsent/index.ts
Normal file
3
frontend/src/components/ui/CookieConsent/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CookieBanner } from './CookieBanner';
|
||||
export { getCookieConsent, saveCookieConsent, clearCookieConsent, hasConsent, getCookieCategories } from './cookieUtils';
|
||||
export type { CookiePreferences } from './cookieUtils';
|
||||
547
frontend/src/pages/app/settings/privacy/PrivacySettingsPage.tsx
Normal file
547
frontend/src/pages/app/settings/privacy/PrivacySettingsPage.tsx
Normal file
@@ -0,0 +1,547 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Shield,
|
||||
Download,
|
||||
Trash2,
|
||||
FileText,
|
||||
Cookie,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
ExternalLink,
|
||||
Lock,
|
||||
Eye
|
||||
} from 'lucide-react';
|
||||
import { Button, Card, Input } from '../../../../components/ui';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { useAuthUser, useAuthStore, useAuthActions } from '../../../../stores/auth.store';
|
||||
import { useCurrentTenant } from '../../../../stores';
|
||||
import { subscriptionService } from '../../../../api';
|
||||
|
||||
export const PrivacySettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { success, error: showError } = useToast();
|
||||
const user = useAuthUser();
|
||||
const token = useAuthStore((state) => state.token);
|
||||
const { logout } = useAuthActions();
|
||||
const currentTenant = useCurrentTenant();
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [deleteConfirmEmail, setDeleteConfirmEmail] = useState('');
|
||||
const [deletePassword, setDeletePassword] = useState('');
|
||||
const [deleteReason, setDeleteReason] = useState('');
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [showExportPreview, setShowExportPreview] = useState(false);
|
||||
const [exportPreview, setExportPreview] = useState<any>(null);
|
||||
const [subscriptionStatus, setSubscriptionStatus] = useState<any>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const loadSubscriptionStatus = async () => {
|
||||
const tenantId = currentTenant?.id || user?.tenant_id;
|
||||
if (tenantId) {
|
||||
try {
|
||||
const status = await subscriptionService.getSubscriptionStatus(tenantId);
|
||||
setSubscriptionStatus(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscription status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSubscriptionStatus();
|
||||
}, [currentTenant, user]);
|
||||
|
||||
const handleDataExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/users/me/export', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to export data');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `my_data_export_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
success(
|
||||
t('settings:privacy.export_success', 'Your data has been exported successfully'),
|
||||
{ title: t('settings:privacy.export_complete', 'Export Complete') }
|
||||
);
|
||||
} catch (err) {
|
||||
showError(
|
||||
t('settings:privacy.export_error', 'Failed to export your data. Please try again.'),
|
||||
{ title: t('common:error', 'Error') }
|
||||
);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewExportPreview = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/v1/users/me/export/summary', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch preview');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setExportPreview(data);
|
||||
setShowExportPreview(true);
|
||||
} catch (err) {
|
||||
showError(
|
||||
t('settings:privacy.preview_error', 'Failed to load preview'),
|
||||
{ title: t('common:error', 'Error') }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAccountDeletion = async () => {
|
||||
if (deleteConfirmEmail.toLowerCase() !== user?.email?.toLowerCase()) {
|
||||
showError(
|
||||
t('settings:privacy.email_mismatch', 'Email does not match your account email'),
|
||||
{ title: t('common:error', 'Error') }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deletePassword) {
|
||||
showError(
|
||||
t('settings:privacy.password_required', 'Password is required'),
|
||||
{ title: t('common:error', 'Error') }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const response = await fetch('/api/v1/users/me/delete/request', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirm_email: deleteConfirmEmail,
|
||||
password: deletePassword,
|
||||
reason: deleteReason
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || 'Failed to delete account');
|
||||
}
|
||||
|
||||
success(
|
||||
t('settings:privacy.delete_success', 'Your account has been deleted. You will be logged out.'),
|
||||
{ title: t('settings:privacy.account_deleted', 'Account Deleted') }
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
logout();
|
||||
navigate('/');
|
||||
}, 2000);
|
||||
} catch (err: any) {
|
||||
showError(
|
||||
err.message || t('settings:privacy.delete_error', 'Failed to delete account. Please try again.'),
|
||||
{ title: t('common:error', 'Error') }
|
||||
);
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Shield className="w-8 h-8 text-primary-600" />
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{t('settings:privacy.title', 'Privacy & Data')}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t('settings:privacy.subtitle', 'Manage your data and privacy settings')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GDPR Rights Information */}
|
||||
<Card className="p-6 bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{t('settings:privacy.gdpr_rights_title', 'Your Data Rights')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t(
|
||||
'settings:privacy.gdpr_rights_description',
|
||||
'Under GDPR, you have the right to access, export, and delete your personal data. These tools help you exercise those rights.'
|
||||
)}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<a
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline inline-flex items-center gap-1"
|
||||
>
|
||||
{t('settings:privacy.privacy_policy', 'Privacy Policy')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
<span className="text-gray-400">•</span>
|
||||
<a
|
||||
href="/terms"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300 underline inline-flex items-center gap-1"
|
||||
>
|
||||
{t('settings:privacy.terms', 'Terms of Service')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Cookie Preferences */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<Cookie className="w-5 h-5 text-amber-600 mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{t('settings:privacy.cookie_preferences', 'Cookie Preferences')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
{t(
|
||||
'settings:privacy.cookie_description',
|
||||
'Manage which cookies and tracking technologies we can use on your browser.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => navigate('/cookie-preferences')}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Cookie className="w-4 h-4 mr-2" />
|
||||
{t('settings:privacy.manage_cookies', 'Manage Cookies')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Data Export - Article 15 (Right to Access) & Article 20 (Data Portability) */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<Download className="w-5 h-5 text-green-600 mt-1 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{t('settings:privacy.export_data', 'Export Your Data')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{t(
|
||||
'settings:privacy.export_description',
|
||||
'Download a copy of all your personal data in machine-readable JSON format. This includes your profile, account activity, and all data we have about you.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 mb-4">
|
||||
<strong>GDPR Rights:</strong> Article 15 (Right to Access) & Article 20 (Data Portability)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showExportPreview && exportPreview && (
|
||||
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||
<h4 className="font-semibold text-sm text-gray-900 dark:text-white mb-3">
|
||||
{t('settings:privacy.export_preview', 'What will be exported:')}
|
||||
</h4>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Personal data</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{exportPreview.data_counts?.active_sessions || 0} active sessions
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{exportPreview.data_counts?.consent_changes || 0} consent records
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{exportPreview.data_counts?.audit_logs || 0} audit logs
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleViewExportPreview}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
{t('settings:privacy.preview_export', 'Preview')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleDataExport}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={isExporting}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting
|
||||
? t('settings:privacy.exporting', 'Exporting...')
|
||||
: t('settings:privacy.export_button', 'Export My Data')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Account Deletion - Article 17 (Right to Erasure) */}
|
||||
<Card className="p-6 border-red-200 dark:border-red-800">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600 mt-1 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{t('settings:privacy.delete_account', 'Delete Account')}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{t(
|
||||
'settings:privacy.delete_description',
|
||||
'Permanently delete your account and all associated data. This action cannot be undone.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 mb-4">
|
||||
<strong>GDPR Right:</strong> Article 17 (Right to Erasure / "Right to be Forgotten")
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-red-900 dark:text-red-100">
|
||||
<p className="font-semibold mb-2">
|
||||
{t('settings:privacy.delete_warning_title', 'What will be deleted:')}
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1 text-xs">
|
||||
<li>Your account and login credentials</li>
|
||||
<li>All personal information (name, email, phone)</li>
|
||||
<li>All active sessions and devices</li>
|
||||
<li>Consent records and preferences</li>
|
||||
<li>Security logs and login history</li>
|
||||
</ul>
|
||||
<p className="mt-3 font-semibold mb-1">
|
||||
{t('settings:privacy.delete_retained_title', 'What will be retained:')}
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1 text-xs">
|
||||
<li>Audit logs (anonymized after 1 year - legal requirement)</li>
|
||||
<li>Financial records (anonymized for 7 years - tax law)</li>
|
||||
<li>Aggregated analytics (no personal identifiers)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-300 text-red-600 hover:bg-red-50 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
{t('settings:privacy.delete_button', 'Delete My Account')}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Additional Resources */}
|
||||
<Card className="p-6">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||||
{t('settings:privacy.resources_title', 'Privacy Resources')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<a
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<FileText className="w-5 h-5 text-gray-600" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('settings:privacy.privacy_policy', 'Privacy Policy')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{t('settings:privacy.privacy_policy_description', 'How we handle your data')}
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/cookies"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<Cookie className="w-5 h-5 text-gray-600" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{t('settings:privacy.cookie_policy', 'Cookie Policy')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{t('settings:privacy.cookie_policy_description', 'About cookies we use')}
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-gray-400" />
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Delete Account Modal */}
|
||||
{showDeleteModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<Card className="max-w-md w-full p-6 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600 flex-shrink-0" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('settings:privacy.delete_confirm_title', 'Delete Account?')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{t(
|
||||
'settings:privacy.delete_confirm_description',
|
||||
'This action is permanent and cannot be undone. All your data will be deleted immediately.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{subscriptionStatus && subscriptionStatus.status === 'active' && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-yellow-900 dark:text-yellow-100 mb-1">
|
||||
Active Subscription Detected
|
||||
</p>
|
||||
<p className="text-yellow-800 dark:text-yellow-200 mb-2">
|
||||
You have an active {subscriptionStatus.plan} subscription. Deleting your account will:
|
||||
</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-yellow-800 dark:text-yellow-200">
|
||||
<li>Cancel your subscription immediately</li>
|
||||
<li>No refund for remaining time</li>
|
||||
<li>Permanently delete all data</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label={t('settings:privacy.confirm_email_label', 'Confirm your email')}
|
||||
type="email"
|
||||
placeholder={user?.email || ''}
|
||||
value={deleteConfirmEmail}
|
||||
onChange={(e) => setDeleteConfirmEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('settings:privacy.password_label', 'Enter your password')}
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
required
|
||||
leftIcon={<Lock className="w-4 h-4" />}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{t('settings:privacy.delete_reason_label', 'Reason for leaving (optional)')}
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||
rows={3}
|
||||
placeholder={t(
|
||||
'settings:privacy.delete_reason_placeholder',
|
||||
'Help us improve by telling us why...'
|
||||
)}
|
||||
value={deleteReason}
|
||||
onChange={(e) => setDeleteReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
⚠️ {t('settings:privacy.delete_final_warning', 'This will permanently delete your account and all data. This action cannot be reversed.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-6">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowDeleteModal(false);
|
||||
setDeleteConfirmEmail('');
|
||||
setDeletePassword('');
|
||||
setDeleteReason('');
|
||||
}}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{t('common:actions.cancel', 'Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAccountDeletion}
|
||||
variant="primary"
|
||||
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
|
||||
disabled={isDeleting || !deleteConfirmEmail || !deletePassword}
|
||||
>
|
||||
{isDeleting
|
||||
? t('settings:privacy.deleting', 'Deleting...')
|
||||
: t('settings:privacy.delete_permanently', 'Delete Permanently')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacySettingsPage;
|
||||
2
frontend/src/pages/app/settings/privacy/index.ts
Normal file
2
frontend/src/pages/app/settings/privacy/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as PrivacySettingsPage } from './PrivacySettingsPage';
|
||||
export { default } from './PrivacySettingsPage';
|
||||
@@ -182,11 +182,21 @@ const SubscriptionPage: React.FC = () => {
|
||||
try {
|
||||
setCancelling(true);
|
||||
|
||||
// In a real implementation, this would call an API endpoint to cancel the subscription
|
||||
// const result = await subscriptionService.cancelSubscription(tenantId);
|
||||
|
||||
// For now, we'll simulate the cancellation
|
||||
addToast('Tu suscripción ha sido cancelada', { type: 'success' });
|
||||
const result = await subscriptionService.cancelSubscription(tenantId, 'User requested cancellation');
|
||||
|
||||
if (result.success) {
|
||||
const daysRemaining = result.days_remaining;
|
||||
const effectiveDate = new Date(result.cancellation_effective_date).toLocaleDateString('es-ES', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
addToast(
|
||||
`Suscripción cancelada. Acceso de solo lectura a partir del ${effectiveDate} (${daysRemaining} días restantes)`,
|
||||
{ type: 'success' }
|
||||
);
|
||||
}
|
||||
|
||||
await loadSubscriptionData();
|
||||
setCancellationDialogOpen(false);
|
||||
|
||||
438
frontend/src/pages/public/CookiePolicyPage.tsx
Normal file
438
frontend/src/pages/public/CookiePolicyPage.tsx
Normal file
@@ -0,0 +1,438 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Cookie, Calendar, Settings, Shield, BarChart3, Target } from 'lucide-react';
|
||||
import { Card, Button } from '../../components/ui';
|
||||
|
||||
export const CookiePolicyPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const lastUpdated = '2025-10-15';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Cookie className="w-16 h-16 text-amber-600" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('legal:cookie.title', 'Cookie Policy')}
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
{t('legal:cookie.last_updated', 'Last updated')}: {lastUpdated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<Card className="p-6 bg-primary-50 dark:bg-primary-900/20 border-primary-200 dark:border-primary-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<Settings className="w-6 h-6 text-primary-600 mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Manage Your Cookie Preferences
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
You can control which cookies we use by visiting our Cookie Preferences page. You can
|
||||
change your preferences at any time.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => navigate('/cookie-preferences')}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Manage Cookie Preferences
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 prose prose-gray dark:prose-invert max-w-none">
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
1. {t('legal:cookie.section_1_title', 'What Are Cookies?')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Cookies are small text files that are stored on your computer or mobile device when you
|
||||
visit a website. They are widely used to make websites work more efficiently and provide
|
||||
information to website owners.
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Cookies can be "persistent" or "session" cookies. Persistent cookies remain on your
|
||||
device after you close your browser, while session cookies are deleted when you close
|
||||
your browser.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
2. {t('legal:cookie.section_2_title', 'How We Use Cookies')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We use cookies for several reasons:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-2">
|
||||
<li>To enable essential functionality (login, session management)</li>
|
||||
<li>To remember your preferences and settings</li>
|
||||
<li>To analyze how you use our platform (analytics)</li>
|
||||
<li>To provide personalized content and features</li>
|
||||
<li>To deliver relevant advertisements (if applicable)</li>
|
||||
<li>To improve our services and user experience</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
3. {t('legal:cookie.section_3_title', 'Types of Cookies We Use')}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="border-l-4 border-green-500 pl-4 bg-green-50 dark:bg-green-900/20 p-4 rounded-r-lg">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<Shield className="w-6 h-6 text-green-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
3.1 Essential Cookies (Required)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
These cookies are necessary for the website to function and cannot be switched off.
|
||||
They are usually set in response to actions you take such as logging in or filling
|
||||
in forms.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-9">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-green-100 dark:bg-green-900/30">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold">Cookie Name</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Purpose</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-green-200 dark:divide-green-800">
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">session_token</td>
|
||||
<td className="px-3 py-2">Maintains user session</td>
|
||||
<td className="px-3 py-2">Session</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">auth_token</td>
|
||||
<td className="px-3 py-2">Authentication</td>
|
||||
<td className="px-3 py-2">7 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">csrf_token</td>
|
||||
<td className="px-3 py-2">Security protection</td>
|
||||
<td className="px-3 py-2">Session</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">bakery_cookie_consent</td>
|
||||
<td className="px-3 py-2">Stores cookie preferences</td>
|
||||
<td className="px-3 py-2">1 year</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-blue-500 pl-4 bg-blue-50 dark:bg-blue-900/20 p-4 rounded-r-lg">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<Settings className="w-6 h-6 text-blue-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
3.2 Preference Cookies (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
These cookies allow us to remember your preferences and settings, such as language,
|
||||
region, and theme preferences.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-9">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-blue-100 dark:bg-blue-900/30">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold">Cookie Name</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Purpose</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-blue-200 dark:divide-blue-800">
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">user_language</td>
|
||||
<td className="px-3 py-2">Language preference</td>
|
||||
<td className="px-3 py-2">1 year</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">theme_preference</td>
|
||||
<td className="px-3 py-2">Dark/Light mode</td>
|
||||
<td className="px-3 py-2">1 year</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">timezone</td>
|
||||
<td className="px-3 py-2">Timezone setting</td>
|
||||
<td className="px-3 py-2">1 year</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-purple-500 pl-4 bg-purple-50 dark:bg-purple-900/20 p-4 rounded-r-lg">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<BarChart3 className="w-6 h-6 text-purple-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
3.3 Analytics Cookies (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
These cookies help us understand how visitors interact with our website by
|
||||
collecting and reporting information anonymously. We use this data to improve our
|
||||
services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-9">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-purple-100 dark:bg-purple-900/30">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold">Cookie Name</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Purpose</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-purple-200 dark:divide-purple-800">
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">_ga</td>
|
||||
<td className="px-3 py-2">Google Analytics - User ID</td>
|
||||
<td className="px-3 py-2">2 years</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">_gid</td>
|
||||
<td className="px-3 py-2">Google Analytics - Session</td>
|
||||
<td className="px-3 py-2">24 hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">_gat</td>
|
||||
<td className="px-3 py-2">Google Analytics - Throttling</td>
|
||||
<td className="px-3 py-2">1 minute</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">analytics_session</td>
|
||||
<td className="px-3 py-2">Internal analytics session</td>
|
||||
<td className="px-3 py-2">30 minutes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-orange-500 pl-4 bg-orange-50 dark:bg-orange-900/20 p-4 rounded-r-lg">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<Target className="w-6 h-6 text-orange-600 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
|
||||
3.4 Marketing Cookies (Optional)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
These cookies track your visits to our website and other websites to show you
|
||||
personalized advertisements. They may be set by us or by third-party advertising
|
||||
partners.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-9">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-orange-100 dark:bg-orange-900/30">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-semibold">Cookie Name</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Purpose</th>
|
||||
<th className="px-3 py-2 text-left font-semibold">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-orange-200 dark:divide-orange-800">
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">marketing_id</td>
|
||||
<td className="px-3 py-2">Marketing campaign tracking</td>
|
||||
<td className="px-3 py-2">90 days</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">ad_preferences</td>
|
||||
<td className="px-3 py-2">Ad personalization</td>
|
||||
<td className="px-3 py-2">1 year</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="px-3 py-2 font-mono text-xs">conversion_track</td>
|
||||
<td className="px-3 py-2">Conversion tracking</td>
|
||||
<td className="px-3 py-2">30 days</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
4. {t('legal:cookie.section_4_title', 'Third-Party Cookies')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Some cookies are set by third-party services that appear on our pages:
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Third-Party Services:
|
||||
</h4>
|
||||
<ul className="space-y-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="font-semibold min-w-[120px]">Google Analytics:</span>
|
||||
<span>Analytics and performance monitoring</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="font-semibold min-w-[120px]">Stripe:</span>
|
||||
<span>Payment processing (sets cookies on their domain)</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="font-semibold min-w-[120px]">CDN Services:</span>
|
||||
<span>Content delivery and performance optimization</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
These third parties have their own privacy policies and cookie policies. We recommend
|
||||
reviewing them:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<li>
|
||||
<a
|
||||
href="https://policies.google.com/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 underline"
|
||||
>
|
||||
Google Privacy Policy
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://stripe.com/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-700 underline"
|
||||
>
|
||||
Stripe Privacy Policy
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
5. {t('legal:cookie.section_5_title', 'How to Control Cookies')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
5.1 Our Cookie Preference Tool
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
The easiest way to manage cookies on our website is through our Cookie Preferences page.
|
||||
You can:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Accept all cookies</li>
|
||||
<li>Accept only essential cookies</li>
|
||||
<li>Customize your preferences by category</li>
|
||||
<li>Change your preferences at any time</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
5.2 Browser Settings
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Most browsers allow you to control cookies through their settings:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<li>
|
||||
<strong>Chrome:</strong> Settings → Privacy and security → Cookies and other site data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Firefox:</strong> Settings → Privacy & Security → Cookies and Site Data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Safari:</strong> Preferences → Privacy → Cookies and website data
|
||||
</li>
|
||||
<li>
|
||||
<strong>Edge:</strong> Settings → Cookies and site permissions → Cookies and site data
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<strong>Note:</strong> Blocking all cookies may prevent you from using certain features
|
||||
of our website, such as staying logged in or saving your preferences.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
6. {t('legal:cookie.section_6_title', 'Do Not Track Signals')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Some browsers include a "Do Not Track" (DNT) feature. Currently, there is no industry
|
||||
standard for how websites should respond to DNT signals. We respect your cookie preferences
|
||||
set through our Cookie Preference tool and browser settings.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
7. {t('legal:cookie.section_7_title', 'Updates to This Policy')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
We may update this Cookie Policy from time to time to reflect changes in our practices or
|
||||
for legal, operational, or regulatory reasons. The "Last Updated" date at the top of this
|
||||
page indicates when this policy was last revised.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
8. {t('legal:cookie.section_8_title', 'Contact Us')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
If you have questions about our use of cookies, please contact us:
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<p className="font-semibold">Panadería IA</p>
|
||||
<p>Email: privacy@panaderia-ia.com</p>
|
||||
<p>Website: https://panaderia-ia.com</p>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8 flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button onClick={() => navigate('/cookie-preferences')} variant="primary">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Manage Cookie Preferences
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/')} variant="outline">
|
||||
Back to Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookiePolicyPage;
|
||||
234
frontend/src/pages/public/CookiePreferencesPage.tsx
Normal file
234
frontend/src/pages/public/CookiePreferencesPage.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Card } from '../../components/ui';
|
||||
import { Cookie, CheckCircle, XCircle, Shield, BarChart3, Target, Settings as SettingsIcon } from 'lucide-react';
|
||||
import {
|
||||
getCookieConsent,
|
||||
saveCookieConsent,
|
||||
getCookieCategories,
|
||||
CookiePreferences
|
||||
} from '../../components/ui/CookieConsent';
|
||||
import { useToast } from '../../hooks/ui/useToast';
|
||||
|
||||
export const CookiePreferencesPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { success } = useToast();
|
||||
|
||||
const [preferences, setPreferences] = useState<CookiePreferences>({
|
||||
essential: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
preferences: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const existingConsent = getCookieConsent();
|
||||
if (existingConsent) {
|
||||
setPreferences(existingConsent);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggle = (category: keyof Omit<CookiePreferences, 'timestamp' | 'version'>) => {
|
||||
if (category === 'essential') return;
|
||||
|
||||
setPreferences(prev => ({
|
||||
...prev,
|
||||
[category]: !prev[category]
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const updatedPreferences: CookiePreferences = {
|
||||
...preferences,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
saveCookieConsent(updatedPreferences);
|
||||
success(
|
||||
t('common:cookie.preferences_saved', 'Your cookie preferences have been saved successfully.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
};
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
const allEnabled: CookiePreferences = {
|
||||
essential: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
preferences: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
saveCookieConsent(allEnabled);
|
||||
setPreferences(allEnabled);
|
||||
success(
|
||||
t('common:cookie.all_accepted', 'All cookies have been accepted.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
};
|
||||
|
||||
const handleRejectAll = () => {
|
||||
const essentialOnly: CookiePreferences = {
|
||||
essential: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
preferences: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
saveCookieConsent(essentialOnly);
|
||||
setPreferences(essentialOnly);
|
||||
success(
|
||||
t('common:cookie.only_essential', 'Only essential cookies are enabled.'),
|
||||
{ title: t('common:cookie.success', 'Preferences Saved') }
|
||||
);
|
||||
};
|
||||
|
||||
const categories = getCookieCategories();
|
||||
|
||||
const getCategoryIcon = (categoryId: string) => {
|
||||
switch (categoryId) {
|
||||
case 'essential':
|
||||
return <Shield className="w-5 h-5 text-green-600" />;
|
||||
case 'preferences':
|
||||
return <SettingsIcon className="w-5 h-5 text-blue-600" />;
|
||||
case 'analytics':
|
||||
return <BarChart3 className="w-5 h-5 text-purple-600" />;
|
||||
case 'marketing':
|
||||
return <Target className="w-5 h-5 text-orange-600" />;
|
||||
default:
|
||||
return <Cookie className="w-5 h-5 text-gray-600" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Cookie className="w-16 h-16 text-amber-600" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('common:cookie.preferences_title', 'Cookie Preferences')}
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
{t(
|
||||
'common:cookie.preferences_description',
|
||||
'Manage your cookie preferences. You can enable or disable different types of cookies below. Note that blocking some types of cookies may impact your experience on our website.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
{categories.map((category) => {
|
||||
const isEnabled = preferences[category.id as keyof Omit<CookiePreferences, 'timestamp' | 'version'>];
|
||||
|
||||
return (
|
||||
<Card key={category.id} className="p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
<div className="mt-1">{getCategoryIcon(category.id)}</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{category.name}
|
||||
</h3>
|
||||
{category.required && (
|
||||
<span className="px-2 py-1 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100 text-xs font-medium rounded">
|
||||
{t('common:cookie.required', 'Required')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
{category.description}
|
||||
</p>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500">
|
||||
<span className="font-medium">{t('common:cookie.examples', 'Examples')}: </span>
|
||||
{category.examples.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleToggle(category.id as keyof Omit<CookiePreferences, 'timestamp' | 'version'>)}
|
||||
disabled={category.required}
|
||||
className={`flex-shrink-0 w-14 h-8 rounded-full transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 ${
|
||||
isEnabled
|
||||
? 'bg-primary-600'
|
||||
: 'bg-gray-300 dark:bg-gray-600'
|
||||
} ${category.required ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
aria-label={`Toggle ${category.name}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block w-6 h-6 mt-1 bg-white rounded-full shadow transform transition-transform duration-200 ease-in-out ${
|
||||
isEnabled ? 'translate-x-7' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<Shield className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p className="font-medium mb-1">
|
||||
{t('common:cookie.info_title', 'About Cookie Management')}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'common:cookie.info_description',
|
||||
'Essential cookies are always enabled to ensure the basic functionality of our website. You can choose to disable other cookie categories, but this may affect certain features. For more information, please read our '
|
||||
)}
|
||||
<a href="/cookies" className="underline font-medium hover:text-blue-900 dark:hover:text-blue-100">
|
||||
{t('common:cookie.cookie_policy', 'Cookie Policy')}
|
||||
</a>
|
||||
{' '}
|
||||
{t('common:cookie.and', 'and')}
|
||||
{' '}
|
||||
<a href="/privacy" className="underline font-medium hover:text-blue-900 dark:hover:text-blue-100">
|
||||
{t('common:cookie.privacy_policy', 'Privacy Policy')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<Button onClick={handleAcceptAll} variant="primary" className="flex-1">
|
||||
{t('common:cookie.accept_all', 'Accept All')}
|
||||
</Button>
|
||||
<Button onClick={handleSave} variant="outline" className="flex-1">
|
||||
{t('common:cookie.save_preferences', 'Save Preferences')}
|
||||
</Button>
|
||||
<Button onClick={handleRejectAll} variant="ghost" className="flex-1">
|
||||
{t('common:cookie.reject_all', 'Reject All Non-Essential')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white underline"
|
||||
>
|
||||
{t('common:actions.go_back', 'Go Back')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookiePreferencesPage;
|
||||
464
frontend/src/pages/public/PrivacyPolicyPage.tsx
Normal file
464
frontend/src/pages/public/PrivacyPolicyPage.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Shield, Mail, FileText, Calendar } from 'lucide-react';
|
||||
import { Card } from '../../components/ui';
|
||||
|
||||
export const PrivacyPolicyPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const lastUpdated = '2025-10-15';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<Shield className="w-16 h-16 text-primary-600" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('legal:privacy.title', 'Privacy Policy')}
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
{t('legal:privacy.last_updated', 'Last updated')}: {lastUpdated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 prose prose-gray dark:prose-invert max-w-none">
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
1. {t('legal:privacy.section_1_title', 'Data Controller')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
The data controller responsible for your personal data is:
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg mb-4">
|
||||
<p className="font-semibold">Panadería IA</p>
|
||||
<p>Email: privacy@panaderia-ia.com</p>
|
||||
<p>Website: https://panaderia-ia.com</p>
|
||||
</div>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
If you have any questions about this Privacy Policy or our data processing practices,
|
||||
please contact us at the above email address.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
2. {t('legal:privacy.section_2_title', 'Personal Data We Collect')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We collect and process the following categories of personal data:
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
2.1 Account Information
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Full name</li>
|
||||
<li>Email address</li>
|
||||
<li>Phone number</li>
|
||||
<li>Password (encrypted)</li>
|
||||
<li>Account creation date</li>
|
||||
<li>Last login information</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
2.2 Business Information
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Business name (bakery name)</li>
|
||||
<li>Business type</li>
|
||||
<li>Business address</li>
|
||||
<li>Tax identification number</li>
|
||||
<li>Business license information</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
2.3 Usage Data
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>IP address</li>
|
||||
<li>Browser type and version</li>
|
||||
<li>Device information</li>
|
||||
<li>Pages visited and features used</li>
|
||||
<li>Time and date of access</li>
|
||||
<li>Referring website addresses</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
2.4 Customer Data (If Applicable)
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Customer names and contact information</li>
|
||||
<li>Order history and preferences</li>
|
||||
<li>Delivery addresses</li>
|
||||
<li>Payment information (processed by Stripe, not stored by us)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
3. {t('legal:privacy.section_3_title', 'Legal Basis for Processing')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We process your personal data based on the following legal grounds under GDPR Article 6:
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Contract Performance (Art. 6(1)(b))
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Processing necessary to provide our services, manage your account, and fulfill our
|
||||
contractual obligations to you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Consent (Art. 6(1)(a))
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
For marketing communications, analytics cookies, and other optional data processing
|
||||
where you have provided explicit consent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Legitimate Interests (Art. 6(1)(f))
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
For improving our services, security purposes, and fraud prevention, where our
|
||||
legitimate interests do not override your rights.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Legal Obligation (Art. 6(1)(c))
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
For compliance with legal obligations such as tax, accounting, and regulatory
|
||||
requirements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
4. {t('legal:privacy.section_4_title', 'How We Use Your Data')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We use your personal data for the following purposes:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-2">
|
||||
<li>To provide, operate, and maintain our bakery management platform</li>
|
||||
<li>To manage your account and provide customer support</li>
|
||||
<li>To process transactions and send you related information</li>
|
||||
<li>To send administrative information, updates, and security alerts</li>
|
||||
<li>To improve and personalize your experience on our platform</li>
|
||||
<li>To monitor and analyze usage trends and activities</li>
|
||||
<li>To detect, prevent, and address technical issues and fraud</li>
|
||||
<li>To send marketing communications (with your consent)</li>
|
||||
<li>To comply with legal obligations and enforce our terms</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
5. {t('legal:privacy.section_5_title', 'Data Sharing and Third Parties')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We may share your personal data with the following third parties:
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
5.1 Service Providers
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li><strong>Stripe:</strong> Payment processing (PCI-DSS compliant)</li>
|
||||
<li><strong>Clouding.io:</strong> Cloud infrastructure hosting in the EU</li>
|
||||
<li><strong>Email service providers:</strong> For transactional and marketing emails</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
5.2 Data Processing Agreements
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
All third-party service providers are bound by Data Processing Agreements (DPAs) that
|
||||
ensure GDPR compliance and protect your data rights.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
5.3 Legal Disclosures
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We may disclose your data if required by law, legal process, litigation, or government
|
||||
authorities, or to protect our rights, property, or safety.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
6. {t('legal:privacy.section_6_title', 'Data Retention')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We retain your personal data for as long as necessary to fulfill the purposes outlined
|
||||
in this Privacy Policy, unless a longer retention period is required by law.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg mb-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Retention Periods:
|
||||
</h4>
|
||||
<ul className="list-disc pl-6 text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li><strong>Account data:</strong> Duration of account + 30 days after deletion request</li>
|
||||
<li><strong>Transaction records:</strong> 7 years (legal/tax requirements)</li>
|
||||
<li><strong>Audit logs:</strong> 1 year (anonymized after)</li>
|
||||
<li><strong>Marketing data:</strong> Until consent withdrawn + 30 days</li>
|
||||
<li><strong>Session data:</strong> 90 days</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
7. {t('legal:privacy.section_7_title', 'Your Rights Under GDPR')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You have the following rights regarding your personal data:
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Access (Art. 15)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Request a copy of your personal data in a structured, commonly used format.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Rectification (Art. 16)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Request correction of inaccurate or incomplete personal data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Erasure (Art. 17)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Request deletion of your personal data ("right to be forgotten").
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Restrict Processing (Art. 18)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Request limitation of processing in certain circumstances.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Data Portability (Art. 20)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Receive your data in a portable format and transfer it to another controller.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Object (Art. 21)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Object to processing based on legitimate interests or for direct marketing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Withdraw Consent (Art. 7)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Withdraw consent at any time without affecting lawfulness of prior processing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-l-4 border-primary-500 pl-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
Right to Lodge a Complaint (Art. 77)
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
File a complaint with your local data protection authority.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mt-4">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
How to Exercise Your Rights
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-2">
|
||||
You can exercise most of your rights directly from your account settings:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 text-sm text-gray-700 dark:text-gray-300">
|
||||
<li>Download your data from Settings → Privacy → Export Data</li>
|
||||
<li>Delete your account from Settings → Privacy → Delete Account</li>
|
||||
<li>Manage consent from Settings → Privacy → Consent Preferences</li>
|
||||
</ul>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mt-2">
|
||||
For other requests, contact: <strong>privacy@panaderia-ia.com</strong>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
8. {t('legal:privacy.section_8_title', 'Data Security')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We implement appropriate technical and organizational measures to protect your personal
|
||||
data:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-2">
|
||||
<li>Encryption in transit (TLS 1.2+) and at rest</li>
|
||||
<li>Password hashing using bcrypt algorithm</li>
|
||||
<li>Multi-factor authentication options</li>
|
||||
<li>Regular security audits and penetration testing</li>
|
||||
<li>Access controls and role-based permissions</li>
|
||||
<li>Comprehensive audit logging of all data access</li>
|
||||
<li>Regular backups with encryption</li>
|
||||
<li>EU-based data centers (clouding.io)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
9. {t('legal:privacy.section_9_title', 'International Data Transfers')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Your data is primarily stored and processed in the European Union. If we transfer data
|
||||
outside the EU, we ensure appropriate safeguards are in place:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Standard Contractual Clauses (SCCs) approved by the European Commission</li>
|
||||
<li>Adequacy decisions for the receiving country</li>
|
||||
<li>Binding Corporate Rules where applicable</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
10. {t('legal:privacy.section_10_title', 'Cookies and Tracking')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We use cookies and similar tracking technologies. For detailed information, please see
|
||||
our <a href="/cookies" className="text-primary-600 hover:text-primary-700 underline">Cookie Policy</a>.
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
You can manage your cookie preferences at any time from the{' '}
|
||||
<a href="/cookie-preferences" className="text-primary-600 hover:text-primary-700 underline">
|
||||
Cookie Preferences page
|
||||
</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
11. {t('legal:privacy.section_11_title', 'Children\'s Privacy')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Our services are not intended for individuals under 16 years of age. We do not knowingly
|
||||
collect personal data from children. If you become aware that a child has provided us
|
||||
with personal data, please contact us immediately.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
12. {t('legal:privacy.section_12_title', 'Changes to This Policy')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We may update this Privacy Policy from time to time. We will notify you of any material
|
||||
changes by:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300">
|
||||
<li>Posting the updated policy on our website</li>
|
||||
<li>Updating the "Last Updated" date</li>
|
||||
<li>Sending you an email notification (for significant changes)</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Your continued use of our services after changes constitutes acceptance of the updated
|
||||
policy.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
13. {t('legal:privacy.section_13_title', 'Contact Us')}
|
||||
</h2>
|
||||
<div className="bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-lg p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<Mail className="w-6 h-6 text-primary-600 mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Privacy Questions or Concerns?
|
||||
</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
If you have any questions about this Privacy Policy or our data practices, or if
|
||||
you wish to exercise your rights, please contact us:
|
||||
</p>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<p><strong>Email:</strong> privacy@panaderia-ia.com</p>
|
||||
<p><strong>Response Time:</strong> Within 30 days of receipt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
14. {t('legal:privacy.section_14_title', 'Supervisory Authority')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You have the right to lodge a complaint with a supervisory authority, in particular in
|
||||
the EU Member State of your habitual residence, place of work, or place of the alleged
|
||||
infringement.
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
For Spain: <strong>Agencia Española de Protección de Datos (AEPD)</strong><br />
|
||||
Website: www.aepd.es
|
||||
</p>
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<a
|
||||
href="/"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 underline"
|
||||
>
|
||||
{t('common:actions.back_home', 'Back to Home')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivacyPolicyPage;
|
||||
421
frontend/src/pages/public/TermsOfServicePage.tsx
Normal file
421
frontend/src/pages/public/TermsOfServicePage.tsx
Normal file
@@ -0,0 +1,421 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FileText, Calendar, AlertTriangle } from 'lucide-react';
|
||||
import { Card } from '../../components/ui';
|
||||
|
||||
export const TermsOfServicePage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const lastUpdated = '2025-10-15';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-12 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex justify-center mb-4">
|
||||
<FileText className="w-16 h-16 text-primary-600" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{t('legal:terms.title', 'Terms of Service')}
|
||||
</h1>
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>
|
||||
{t('legal:terms.last_updated', 'Last updated')}: {lastUpdated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="p-8 prose prose-gray dark:prose-invert max-w-none">
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
1. {t('legal:terms.section_1_title', 'Agreement to Terms')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
By accessing or using Panadería IA ("the Platform", "our Service"), you agree to be
|
||||
bound by these Terms of Service ("Terms"). If you do not agree to these Terms, do not
|
||||
use the Platform.
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
These Terms constitute a legally binding agreement between you ("User", "you", "your")
|
||||
and Panadería IA regarding your use of the Platform.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
2. {t('legal:terms.section_2_title', 'Description of Service')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Panadería IA provides a comprehensive bakery management platform that includes:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Inventory management and tracking</li>
|
||||
<li>Production planning and scheduling</li>
|
||||
<li>Sales forecasting and analytics</li>
|
||||
<li>Customer and order management</li>
|
||||
<li>Recipe management and costing</li>
|
||||
<li>Supplier and procurement management</li>
|
||||
<li>Point of sale integration</li>
|
||||
<li>AI-powered insights and recommendations</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
We reserve the right to modify, suspend, or discontinue any part of the Service at any
|
||||
time with or without notice.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
3. {t('legal:terms.section_3_title', 'User Accounts')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
3.1 Account Registration
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
To use the Platform, you must:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Be at least 16 years of age</li>
|
||||
<li>Provide accurate, current, and complete information</li>
|
||||
<li>Maintain and promptly update your account information</li>
|
||||
<li>Maintain the security of your password</li>
|
||||
<li>Accept responsibility for all activities under your account</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
3.2 Account Security
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You are responsible for:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Safeguarding your password and account credentials</li>
|
||||
<li>Notifying us immediately of any unauthorized access</li>
|
||||
<li>Ensuring that you log out from your account at the end of each session</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
3.3 Account Termination
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
We reserve the right to suspend or terminate your account if you violate these Terms or
|
||||
engage in fraudulent, abusive, or illegal activity.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
4. {t('legal:terms.section_4_title', 'Subscription and Payment')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
4.1 Subscription Plans
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
The Platform offers various subscription plans with different features and pricing. You
|
||||
can view current plans and pricing on our website.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
4.2 Payment Terms
|
||||
</h3>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Subscription fees are billed in advance on a monthly or annual basis</li>
|
||||
<li>All fees are non-refundable except as required by law</li>
|
||||
<li>You authorize us to charge your payment method for all fees</li>
|
||||
<li>Prices may change with 30 days' notice</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
4.3 Free Trial
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We may offer a free trial period. At the end of the trial, your subscription will
|
||||
automatically convert to a paid plan unless you cancel before the trial ends.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
4.4 Cancellation
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
You may cancel your subscription at any time from your account settings. Cancellation
|
||||
takes effect at the end of the current billing period.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
5. {t('legal:terms.section_5_title', 'User Conduct and Prohibited Activities')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You agree not to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-2">
|
||||
<li>Violate any applicable laws or regulations</li>
|
||||
<li>Infringe on intellectual property rights of others</li>
|
||||
<li>Upload viruses, malware, or other malicious code</li>
|
||||
<li>Attempt to gain unauthorized access to the Platform or related systems</li>
|
||||
<li>Interfere with or disrupt the Platform's operation</li>
|
||||
<li>Use the Platform for any illegal or unauthorized purpose</li>
|
||||
<li>Impersonate any person or entity</li>
|
||||
<li>Collect or harvest user data without permission</li>
|
||||
<li>Reverse engineer, decompile, or disassemble the Platform</li>
|
||||
<li>Use automated systems (bots, scrapers) without authorization</li>
|
||||
<li>Resell or redistribute the Service without permission</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
6. {t('legal:terms.section_6_title', 'Intellectual Property')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
6.1 Platform Ownership
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
The Platform and all content, features, and functionality are owned by Panadería IA and
|
||||
are protected by international copyright, trademark, and other intellectual property laws.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
6.2 User Content
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
You retain all rights to the data and content you upload to the Platform ("User Content").
|
||||
By uploading User Content, you grant us a limited license to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Store, process, and display your User Content to provide the Service</li>
|
||||
<li>Create derivative works for analytics and AI features (anonymized)</li>
|
||||
<li>Make backups of your data for disaster recovery</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
6.3 Trademarks
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
"Panadería IA" and our logo are trademarks. You may not use our trademarks without
|
||||
prior written consent.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
7. {t('legal:terms.section_7_title', 'Data Privacy and Protection')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Your use of the Platform is also governed by our{' '}
|
||||
<a href="/privacy" className="text-primary-600 hover:text-primary-700 underline">
|
||||
Privacy Policy
|
||||
</a>
|
||||
, which is incorporated into these Terms by reference.
|
||||
</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We comply with the General Data Protection Regulation (GDPR) and other applicable data
|
||||
protection laws. Key points:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>We are the data controller for your account data</li>
|
||||
<li>You are the data controller for customer data you input</li>
|
||||
<li>We act as a data processor for your customer data</li>
|
||||
<li>We have Data Processing Agreements with all sub-processors</li>
|
||||
<li>Data is stored in EU data centers</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
8. {t('legal:terms.section_8_title', 'Service Availability and Support')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
8.1 Availability
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We strive to maintain 99.9% uptime but do not guarantee uninterrupted access. The
|
||||
Platform may be unavailable during:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Scheduled maintenance (with advance notice)</li>
|
||||
<li>Emergency maintenance</li>
|
||||
<li>Force majeure events</li>
|
||||
<li>Third-party service failures</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
8.2 Customer Support
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Support is provided based on your subscription plan. Response times and support channels
|
||||
vary by plan tier.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
9. {t('legal:terms.section_9_title', 'Disclaimers and Limitations of Liability')}
|
||||
</h2>
|
||||
|
||||
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 mb-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<strong>IMPORTANT LEGAL NOTICE</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
9.1 "AS IS" Disclaimer
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
THE PLATFORM IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
9.2 Limitation of Liability
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY LAW, PANADERÍA IA SHALL NOT BE LIABLE FOR:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Indirect, incidental, special, consequential, or punitive damages</li>
|
||||
<li>Loss of profits, revenue, data, or business opportunities</li>
|
||||
<li>Service interruptions or data loss</li>
|
||||
<li>Actions of third parties</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
OUR TOTAL LIABILITY SHALL NOT EXCEED THE AMOUNT YOU PAID IN THE 12 MONTHS PRECEDING
|
||||
THE CLAIM.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
10. {t('legal:terms.section_10_title', 'Indemnification')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
You agree to indemnify and hold harmless Panadería IA from any claims, damages, losses,
|
||||
liabilities, and expenses (including legal fees) arising from:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Your use of the Platform</li>
|
||||
<li>Your violation of these Terms</li>
|
||||
<li>Your violation of any law or rights of third parties</li>
|
||||
<li>Your User Content</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
11. {t('legal:terms.section_11_title', 'Modifications to Terms')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
We reserve the right to modify these Terms at any time. Changes will be effective:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>Immediately upon posting for non-material changes</li>
|
||||
<li>30 days after notification for material changes</li>
|
||||
</ul>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Continued use of the Platform after changes constitutes acceptance. If you do not agree,
|
||||
you must stop using the Platform and cancel your subscription.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
12. {t('legal:terms.section_12_title', 'Governing Law and Dispute Resolution')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
12.1 Governing Law
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
These Terms are governed by the laws of Spain and the European Union, without regard to
|
||||
conflict of law principles.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
12.2 Dispute Resolution
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
In the event of a dispute:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<li>First attempt to resolve through good faith negotiations</li>
|
||||
<li>If unresolved, disputes shall be submitted to the courts of Spain</li>
|
||||
<li>EU consumers retain rights under EU consumer protection laws</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
13. {t('legal:terms.section_13_title', 'Miscellaneous')}
|
||||
</h2>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
13.1 Entire Agreement
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
These Terms, together with our Privacy Policy and Cookie Policy, constitute the entire
|
||||
agreement between you and Panadería IA.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
13.2 Severability
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
If any provision is found unenforceable, the remaining provisions continue in full force.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
13.3 Waiver
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Failure to enforce any right or provision does not constitute a waiver of such right.
|
||||
</p>
|
||||
|
||||
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
13.4 Assignment
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
You may not assign these Terms without our consent. We may assign these Terms without
|
||||
restriction.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
14. {t('legal:terms.section_14_title', 'Contact Information')}
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
||||
For questions about these Terms, contact us at:
|
||||
</p>
|
||||
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<p className="font-semibold">Panadería IA</p>
|
||||
<p>Email: legal@panaderia-ia.com</p>
|
||||
<p>Website: https://panaderia-ia.com</p>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<a
|
||||
href="/"
|
||||
className="text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 underline"
|
||||
>
|
||||
{t('common:actions.back_home', 'Back to Home')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfServicePage;
|
||||
@@ -1,4 +1,8 @@
|
||||
export { default as LandingPage } from './LandingPage';
|
||||
export { default as LoginPage } from './LoginPage';
|
||||
export { default as RegisterPage } from './RegisterPage';
|
||||
export { default as DemoPage } from './DemoPage';
|
||||
export { default as DemoPage } from './DemoPage';
|
||||
export { default as PrivacyPolicyPage } from './PrivacyPolicyPage';
|
||||
export { default as TermsOfServicePage } from './TermsOfServicePage';
|
||||
export { default as CookiePolicyPage } from './CookiePolicyPage';
|
||||
export { default as CookiePreferencesPage } from './CookiePreferencesPage';
|
||||
@@ -10,6 +10,10 @@ const LoginPage = React.lazy(() => import('../pages/public/LoginPage'));
|
||||
const RegisterPage = React.lazy(() => import('../pages/public/RegisterPage'));
|
||||
const DemoPage = React.lazy(() => import('../pages/public/DemoPage'));
|
||||
const DemoSetupPage = React.lazy(() => import('../pages/public/DemoSetupPage'));
|
||||
const PrivacyPolicyPage = React.lazy(() => import('../pages/public/PrivacyPolicyPage'));
|
||||
const TermsOfServicePage = React.lazy(() => import('../pages/public/TermsOfServicePage'));
|
||||
const CookiePolicyPage = React.lazy(() => import('../pages/public/CookiePolicyPage'));
|
||||
const CookiePreferencesPage = React.lazy(() => import('../pages/public/CookiePreferencesPage'));
|
||||
const DashboardPage = React.lazy(() => import('../pages/app/DashboardPage'));
|
||||
|
||||
// Operations pages
|
||||
@@ -36,6 +40,7 @@ const PerformanceAnalyticsPage = React.lazy(() => import('../pages/app/analytics
|
||||
const PersonalInfoPage = React.lazy(() => import('../pages/app/settings/personal-info/PersonalInfoPage'));
|
||||
const CommunicationPreferencesPage = React.lazy(() => import('../pages/app/settings/communication-preferences/CommunicationPreferencesPage'));
|
||||
const SubscriptionPage = React.lazy(() => import('../pages/app/settings/subscription/SubscriptionPage'));
|
||||
const PrivacySettingsPage = React.lazy(() => import('../pages/app/settings/privacy/PrivacySettingsPage'));
|
||||
const InformationPage = React.lazy(() => import('../pages/app/database/information/InformationPage'));
|
||||
const TeamPage = React.lazy(() => import('../pages/app/settings/team/TeamPage'));
|
||||
const OrganizationsPage = React.lazy(() => import('../pages/app/settings/organizations/OrganizationsPage'));
|
||||
@@ -63,6 +68,12 @@ export const AppRouter: React.FC = () => {
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/demo" element={<DemoPage />} />
|
||||
<Route path="/demo/setup" element={<DemoSetupPage />} />
|
||||
|
||||
{/* Legal & Privacy Routes - Public */}
|
||||
<Route path="/privacy" element={<PrivacyPolicyPage />} />
|
||||
<Route path="/terms" element={<TermsOfServicePage />} />
|
||||
<Route path="/cookies" element={<CookiePolicyPage />} />
|
||||
<Route path="/cookie-preferences" element={<CookiePreferencesPage />} />
|
||||
|
||||
{/* Protected Routes with AppShell Layout */}
|
||||
<Route
|
||||
@@ -334,6 +345,16 @@ export const AppRouter: React.FC = () => {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/app/settings/privacy"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<PrivacySettingsPage />
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Data Routes */}
|
||||
<Route
|
||||
|
||||
@@ -133,6 +133,7 @@ export const ROUTES = {
|
||||
SETTINGS_COMMUNICATION: '/app/settings/communication-preferences',
|
||||
SETTINGS_SUBSCRIPTION: '/app/settings/subscription',
|
||||
SETTINGS_ORGANIZATIONS: '/app/settings/organizations',
|
||||
SETTINGS_PRIVACY: '/app/settings/privacy',
|
||||
SETTINGS_TENANT: '/settings/tenant',
|
||||
SETTINGS_USERS: '/settings/users',
|
||||
SETTINGS_PERMISSIONS: '/settings/permissions',
|
||||
@@ -141,6 +142,12 @@ export const ROUTES = {
|
||||
SETTINGS_BAKERY_CONFIG: '/app/database/information',
|
||||
SETTINGS_TEAM: '/app/database/team',
|
||||
QUALITY_TEMPLATES: '/app/database/quality-templates',
|
||||
|
||||
// Legal & Privacy Pages
|
||||
PRIVACY_POLICY: '/privacy',
|
||||
TERMS_OF_SERVICE: '/terms',
|
||||
COOKIE_POLICY: '/cookies',
|
||||
COOKIE_PREFERENCES: '/cookie-preferences',
|
||||
|
||||
// Reports
|
||||
REPORTS: '/reports',
|
||||
@@ -520,6 +527,16 @@ export const routesConfig: RouteConfig[] = [
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
{
|
||||
path: '/app/settings/privacy',
|
||||
name: 'Privacy',
|
||||
component: 'PrivacySettingsPage',
|
||||
title: 'Privacidad',
|
||||
icon: 'settings',
|
||||
requiresAuth: true,
|
||||
showInNavigation: true,
|
||||
showInBreadcrumbs: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user