) => void;
+ isFullscreen: boolean;
+ onToggleFullscreen: () => void;
+}
+
+export function SettingsPanel({
+ settings,
+ onUpdate,
+ isFullscreen,
+ onToggleFullscreen
+}: SettingsPanelProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const currentTheme = THEMES.find(t => t.id === settings.themeId) || THEMES[0];
+
+ const handleCopyUrl = useCallback(async () => {
+ const url = generateShareableUrl(settings);
+ try {
+ await navigator.clipboard.writeText(url);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (e) {
+ console.error('Erreur lors de la copie:', e);
+ }
+ }, [settings]);
+
+ return (
+ <>
+ {/* Bouton pour ouvrir les paramètres */}
+ setIsOpen(!isOpen)}
+ className={`
+ fixed top-4 right-4 z-50 p-3 rounded-full
+ transition-all duration-300 ease-out
+ hover:scale-110 active:scale-95
+ ${isOpen ? 'rotate-90' : ''}
+ `}
+ style={{
+ backgroundColor: `${currentTheme.primary}20`,
+ color: currentTheme.foreground,
+ border: `1px solid ${currentTheme.primary}40`,
+ }}
+ aria-label={isOpen ? 'Fermer les paramètres' : 'Ouvrir les paramètres'}
+ aria-expanded={isOpen}
+ >
+
+
+
+
+
+
+ {/* Bouton plein écran */}
+
+ {isFullscreen ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Panneau de paramètres */}
+
+
+
+ Paramètres
+
+
+ {/* Type d'horloge */}
+
+
+ Type d'horloge
+
+
+ onUpdate({ clockType: 'digital' })}
+ className={`
+ flex-1 py-3 px-4 rounded-lg font-medium
+ transition-all duration-200 min-w-[80px]
+ `}
+ style={{
+ backgroundColor: settings.clockType === 'digital'
+ ? currentTheme.primary
+ : `${currentTheme.secondary}30`,
+ color: settings.clockType === 'digital'
+ ? currentTheme.background
+ : currentTheme.foreground,
+ }}
+ aria-pressed={settings.clockType === 'digital'}
+ >
+ Numérique
+
+ onUpdate({ clockType: 'analog' })}
+ className={`
+ flex-1 py-3 px-4 rounded-lg font-medium
+ transition-all duration-200 min-w-[80px]
+ `}
+ style={{
+ backgroundColor: settings.clockType === 'analog'
+ ? currentTheme.primary
+ : `${currentTheme.secondary}30`,
+ color: settings.clockType === 'analog'
+ ? currentTheme.background
+ : currentTheme.foreground,
+ }}
+ aria-pressed={settings.clockType === 'analog'}
+ >
+ Analogique
+
+ onUpdate({ clockType: 'flip' })}
+ className={`
+ flex-1 py-3 px-4 rounded-lg font-medium
+ transition-all duration-200 min-w-[80px]
+ `}
+ style={{
+ backgroundColor: settings.clockType === 'flip'
+ ? currentTheme.primary
+ : `${currentTheme.secondary}30`,
+ color: settings.clockType === 'flip'
+ ? currentTheme.background
+ : currentTheme.foreground,
+ }}
+ aria-pressed={settings.clockType === 'flip'}
+ >
+ À bascule
+
+
+
+
+ {/* Format de l'heure */}
+
+
+
+ onUpdate({ timeFormat: '24h' })}
+ className={`
+ flex-1 py-3 px-4 rounded-lg font-medium
+ transition-all duration-200
+ `}
+ style={{
+ backgroundColor: settings.timeFormat === '24h'
+ ? currentTheme.primary
+ : `${currentTheme.secondary}30`,
+ color: settings.timeFormat === '24h'
+ ? currentTheme.background
+ : currentTheme.foreground,
+ }}
+ aria-pressed={settings.timeFormat === '24h'}
+ >
+ 24 heures
+
+ onUpdate({ timeFormat: '12h' })}
+ className={`
+ flex-1 py-3 px-4 rounded-lg font-medium
+ transition-all duration-200
+ `}
+ style={{
+ backgroundColor: settings.timeFormat === '12h'
+ ? currentTheme.primary
+ : `${currentTheme.secondary}30`,
+ color: settings.timeFormat === '12h'
+ ? currentTheme.background
+ : currentTheme.foreground,
+ }}
+ aria-pressed={settings.timeFormat === '12h'}
+ >
+ 12 heures
+
+
+
+
+ {/* Afficher les secondes */}
+
+
+ Afficher les secondes
+
+ onUpdate({ showSeconds: !settings.showSeconds })}
+ className={`
+ relative w-14 h-7 rounded-full
+ transition-colors duration-200
+ `}
+ style={{
+ backgroundColor: settings.showSeconds
+ ? currentTheme.primary
+ : `${currentTheme.secondary}50`,
+ }}
+ >
+
+
+
+
+ {/* Fuseau horaire */}
+
+
+ Fuseau horaire
+
+ onUpdate({ timezone: e.target.value })}
+ className="w-full py-3 px-4 rounded-lg transition-colors duration-200"
+ style={{
+ backgroundColor: `${currentTheme.secondary}30`,
+ color: currentTheme.foreground,
+ border: `1px solid ${currentTheme.secondary}50`,
+ }}
+ aria-label="Sélectionner un fuseau horaire"
+ >
+ {TIMEZONES.map((tz) => (
+
+ {tz.label} (UTC{tz.offset})
+
+ ))}
+
+
+
+ {/* Thème */}
+
+
+ Thème visuel
+
+
+ {THEMES.map((theme: Theme) => (
+ onUpdate({ themeId: theme.id })}
+ className={`
+ relative w-full aspect-square rounded-lg
+ transition-transform duration-200
+ hover:scale-105 active:scale-95
+ `}
+ style={{
+ backgroundColor: theme.background,
+ border: `2px solid ${theme.primary}`,
+ boxShadow: settings.themeId === theme.id
+ ? `0 0 0 2px ${currentTheme.background}, 0 0 0 4px ${theme.primary}`
+ : 'none',
+ }}
+ aria-label={`Thème ${theme.name}`}
+ aria-pressed={settings.themeId === theme.id}
+ title={theme.name}
+ >
+
+
+ ))}
+
+
+ Thème actuel : {currentTheme.name}
+
+
+
+ {/* Partager */}
+
+
+ Partager cette horloge
+
+
+ {copied ? (
+ <>
+
+
+
+ Copié !
+ >
+ ) : (
+ <>
+
+
+
+
+
+ Copier le lien
+ >
+ )}
+
+
+
+ {/* Info */}
+
+
+ Les paramètres sont automatiquement sauvegardés dans votre navigateur.
+
+
+
+
+
+ {/* Overlay */}
+ {isOpen && (
+ setIsOpen(false)}
+ aria-hidden="true"
+ />
+ )}
+ >
+ );
+}
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..f7eb8f6
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,3 @@
+export { ClockDisplay, DigitalClock, AnalogClock } from './Clock';
+export { SettingsPanel } from './SettingsPanel';
+export { ClockApp } from './ClockApp';
diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts
new file mode 100644
index 0000000..edcb71d
--- /dev/null
+++ b/src/lib/hooks.ts
@@ -0,0 +1,204 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { ClockSettings, TimeData, DEFAULT_SETTINGS } from './types';
+
+const STORAGE_KEY = 'clock-settings';
+
+// Hook personnalisé pour gérer le temps
+export function useTime(timezone: string, updateInterval: number = 100): TimeData {
+ const [time, setTime] = useState(() => getTimeInTimezone(timezone));
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTime(getTimeInTimezone(timezone));
+ }, updateInterval);
+
+ return () => clearInterval(interval);
+ }, [timezone, updateInterval]);
+
+ return time;
+}
+
+// Fonction pour obtenir l'heure dans un fuseau horaire spécifique
+export function getTimeInTimezone(timezone: string): TimeData {
+ const now = new Date();
+
+ try {
+ const options: Intl.DateTimeFormatOptions = {
+ timeZone: timezone,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ };
+
+ const formatter = new Intl.DateTimeFormat('fr-FR', options);
+ const parts = formatter.formatToParts(now);
+
+ const hours = parseInt(parts.find(p => p.type === 'hour')?.value || '0', 10);
+ const minutes = parseInt(parts.find(p => p.type === 'minute')?.value || '0', 10);
+ const seconds = parseInt(parts.find(p => p.type === 'second')?.value || '0', 10);
+ const milliseconds = now.getMilliseconds();
+
+ const period = hours >= 12 ? 'PM' : 'AM';
+
+ return {
+ hours,
+ minutes,
+ seconds,
+ milliseconds,
+ period,
+ formattedTime: `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`,
+ date: now,
+ };
+ } catch {
+ // Fallback si le fuseau horaire n'est pas valide
+ return {
+ hours: now.getHours(),
+ minutes: now.getMinutes(),
+ seconds: now.getSeconds(),
+ milliseconds: now.getMilliseconds(),
+ period: now.getHours() >= 12 ? 'PM' : 'AM',
+ formattedTime: now.toLocaleTimeString('fr-FR'),
+ date: now,
+ };
+ }
+}
+
+// Hook pour gérer les paramètres avec persistance
+export function useClockSettings(initialSettings?: Partial) {
+ const [settings, setSettings] = useState(() => {
+ return { ...DEFAULT_SETTINGS, ...initialSettings };
+ });
+ const [isLoaded, setIsLoaded] = useState(false);
+ const [hasInitialized, setHasInitialized] = useState(false);
+
+ // Charger les paramètres depuis localStorage au montage (une seule fois)
+ useEffect(() => {
+ if (typeof window !== 'undefined' && !hasInitialized) {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved) {
+ const parsed = JSON.parse(saved);
+ setSettings(prev => ({ ...prev, ...parsed, ...initialSettings }));
+ } else if (initialSettings) {
+ setSettings(prev => ({ ...prev, ...initialSettings }));
+ }
+ } catch (e) {
+ console.error('Erreur lors du chargement des paramètres:', e);
+ }
+ setHasInitialized(true);
+ setIsLoaded(true);
+ }
+ }, [initialSettings, hasInitialized]);
+
+ // Sauvegarder les paramètres dans localStorage
+ useEffect(() => {
+ if (isLoaded && typeof window !== 'undefined') {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
+ } catch (e) {
+ console.error('Erreur lors de la sauvegarde des paramètres:', e);
+ }
+ }
+ }, [settings, isLoaded]);
+
+ const updateSettings = useCallback((updates: Partial) => {
+ setSettings(prev => ({ ...prev, ...updates }));
+ }, []);
+
+ return { settings, updateSettings, isLoaded };
+}
+
+// Hook pour le mode plein écran
+export function useFullscreen() {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ useEffect(() => {
+ const handleFullscreenChange = () => {
+ setIsFullscreen(!!document.fullscreenElement);
+ };
+
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
+ return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
+ }, []);
+
+ const toggleFullscreen = useCallback(async () => {
+ try {
+ if (!document.fullscreenElement) {
+ await document.documentElement.requestFullscreen();
+ } else {
+ await document.exitFullscreen();
+ }
+ } catch (e) {
+ console.error('Erreur plein écran:', e);
+ }
+ }, []);
+
+ return { isFullscreen, toggleFullscreen };
+}
+
+// Formater l'heure selon le format choisi
+export function formatTime(
+ time: TimeData,
+ format: '12h' | '24h',
+ showSeconds: boolean
+): string {
+ let hours = time.hours;
+
+ if (format === '12h') {
+ hours = hours % 12 || 12;
+ }
+
+ const hoursStr = hours.toString().padStart(2, '0');
+ const minutesStr = time.minutes.toString().padStart(2, '0');
+ const secondsStr = time.seconds.toString().padStart(2, '0');
+
+ let result = `${hoursStr}:${minutesStr}`;
+
+ if (showSeconds) {
+ result += `:${secondsStr}`;
+ }
+
+ return result;
+}
+
+// Générer l'URL partageable avec les paramètres
+export function generateShareableUrl(settings: ClockSettings): string {
+ if (typeof window === 'undefined') return '';
+
+ const params = new URLSearchParams();
+ params.set('tz', settings.timezone);
+ params.set('type', settings.clockType);
+ params.set('format', settings.timeFormat);
+ params.set('seconds', settings.showSeconds.toString());
+ params.set('theme', settings.themeId);
+
+ return `${window.location.origin}?${params.toString()}`;
+}
+
+// Parser les paramètres depuis l'URL
+export function parseUrlParams(): Partial {
+ if (typeof window === 'undefined') return {};
+
+ const params = new URLSearchParams(window.location.search);
+ const settings: Partial = {};
+
+ const tz = params.get('tz');
+ if (tz) settings.timezone = tz;
+
+ const type = params.get('type');
+ if (type === 'digital' || type === 'analog') settings.clockType = type;
+
+ const format = params.get('format');
+ if (format === '12h' || format === '24h') settings.timeFormat = format;
+
+ const seconds = params.get('seconds');
+ if (seconds !== null) settings.showSeconds = seconds === 'true';
+
+ const theme = params.get('theme');
+ if (theme) settings.themeId = theme;
+
+ return settings;
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
new file mode 100644
index 0000000..21f7aec
--- /dev/null
+++ b/src/lib/types.ts
@@ -0,0 +1,189 @@
+// Types pour l'application d'horloge
+
+export type ClockType = 'digital' | 'analog' | 'flip';
+
+export type TimeFormat = '12h' | '24h';
+
+export interface Theme {
+ id: string;
+ name: string;
+ background: string;
+ foreground: string;
+ primary: string;
+ secondary: string;
+ accent: string;
+}
+
+export interface ClockSettings {
+ clockType: ClockType;
+ timeFormat: TimeFormat;
+ showSeconds: boolean;
+ timezone: string;
+ themeId: string;
+}
+
+export interface TimeData {
+ hours: number;
+ minutes: number;
+ seconds: number;
+ milliseconds: number;
+ period: 'AM' | 'PM';
+ formattedTime: string;
+ date: Date;
+}
+
+// Themes disponibles
+export const THEMES: Theme[] = [
+ {
+ id: 'midnight',
+ name: 'Minuit',
+ background: '#0f172a',
+ foreground: '#f8fafc',
+ primary: '#3b82f6',
+ secondary: '#64748b',
+ accent: '#8b5cf6',
+ },
+ {
+ id: 'ocean',
+ name: 'Océan',
+ background: '#0c4a6e',
+ foreground: '#f0f9ff',
+ primary: '#0ea5e9',
+ secondary: '#7dd3fc',
+ accent: '#38bdf8',
+ },
+ {
+ id: 'forest',
+ name: 'Forêt',
+ background: '#14532d',
+ foreground: '#f0fdf4',
+ primary: '#22c55e',
+ secondary: '#86efac',
+ accent: '#4ade80',
+ },
+ {
+ id: 'sunset',
+ name: 'Coucher de soleil',
+ background: '#7c2d12',
+ foreground: '#fff7ed',
+ primary: '#f97316',
+ secondary: '#fdba74',
+ accent: '#fb923c',
+ },
+ {
+ id: 'lavender',
+ name: 'Lavande',
+ background: '#4c1d95',
+ foreground: '#f5f3ff',
+ primary: '#a78bfa',
+ secondary: '#c4b5fd',
+ accent: '#8b5cf6',
+ },
+ {
+ id: 'rose',
+ name: 'Rose',
+ background: '#831843',
+ foreground: '#fdf2f8',
+ primary: '#ec4899',
+ secondary: '#f9a8d4',
+ accent: '#f472b6',
+ },
+ {
+ id: 'charcoal',
+ name: 'Charbon',
+ background: '#18181b',
+ foreground: '#fafafa',
+ primary: '#a1a1aa',
+ secondary: '#71717a',
+ accent: '#d4d4d8',
+ },
+ {
+ id: 'snow',
+ name: 'Neige',
+ background: '#f8fafc',
+ foreground: '#0f172a',
+ primary: '#1e293b',
+ secondary: '#64748b',
+ accent: '#334155',
+ },
+ {
+ id: 'amber',
+ name: 'Ambre',
+ background: '#78350f',
+ foreground: '#fffbeb',
+ primary: '#f59e0b',
+ secondary: '#fcd34d',
+ accent: '#fbbf24',
+ },
+ {
+ id: 'emerald',
+ name: 'Émeraude',
+ background: '#064e3b',
+ foreground: '#ecfdf5',
+ primary: '#10b981',
+ secondary: '#6ee7b7',
+ accent: '#34d399',
+ },
+ {
+ id: 'ruby',
+ name: 'Rubis',
+ background: '#7f1d1d',
+ foreground: '#fef2f2',
+ primary: '#ef4444',
+ secondary: '#fca5a5',
+ accent: '#f87171',
+ },
+ {
+ id: 'cyberpunk',
+ name: 'Cyberpunk',
+ background: '#0a0a0a',
+ foreground: '#00ff9f',
+ primary: '#ff00ff',
+ secondary: '#00ffff',
+ accent: '#ffff00',
+ },
+];
+
+// Liste des fuseaux horaires populaires
+export const TIMEZONES = [
+ { value: 'Europe/Paris', label: 'Paris (CET)', offset: '+1:00' },
+ { value: 'Europe/London', label: 'Londres (GMT)', offset: '+0:00' },
+ { value: 'America/New_York', label: 'New York (EST)', offset: '-5:00' },
+ { value: 'America/Los_Angeles', label: 'Los Angeles (PST)', offset: '-8:00' },
+ { value: 'America/Chicago', label: 'Chicago (CST)', offset: '-6:00' },
+ { value: 'America/Toronto', label: 'Toronto (EST)', offset: '-5:00' },
+ { value: 'America/Sao_Paulo', label: 'São Paulo (BRT)', offset: '-3:00' },
+ { value: 'America/Mexico_City', label: 'Mexico (CST)', offset: '-6:00' },
+ { value: 'Europe/Berlin', label: 'Berlin (CET)', offset: '+1:00' },
+ { value: 'Europe/Madrid', label: 'Madrid (CET)', offset: '+1:00' },
+ { value: 'Europe/Rome', label: 'Rome (CET)', offset: '+1:00' },
+ { value: 'Europe/Amsterdam', label: 'Amsterdam (CET)', offset: '+1:00' },
+ { value: 'Europe/Brussels', label: 'Bruxelles (CET)', offset: '+1:00' },
+ { value: 'Europe/Zurich', label: 'Zurich (CET)', offset: '+1:00' },
+ { value: 'Europe/Moscow', label: 'Moscou (MSK)', offset: '+3:00' },
+ { value: 'Europe/Istanbul', label: 'Istanbul (TRT)', offset: '+3:00' },
+ { value: 'Asia/Dubai', label: 'Dubaï (GST)', offset: '+4:00' },
+ { value: 'Asia/Kolkata', label: 'Mumbai (IST)', offset: '+5:30' },
+ { value: 'Asia/Bangkok', label: 'Bangkok (ICT)', offset: '+7:00' },
+ { value: 'Asia/Singapore', label: 'Singapour (SGT)', offset: '+8:00' },
+ { value: 'Asia/Hong_Kong', label: 'Hong Kong (HKT)', offset: '+8:00' },
+ { value: 'Asia/Shanghai', label: 'Shanghai (CST)', offset: '+8:00' },
+ { value: 'Asia/Tokyo', label: 'Tokyo (JST)', offset: '+9:00' },
+ { value: 'Asia/Seoul', label: 'Séoul (KST)', offset: '+9:00' },
+ { value: 'Australia/Sydney', label: 'Sydney (AEST)', offset: '+10:00' },
+ { value: 'Australia/Melbourne', label: 'Melbourne (AEST)', offset: '+10:00' },
+ { value: 'Pacific/Auckland', label: 'Auckland (NZST)', offset: '+12:00' },
+ { value: 'Pacific/Honolulu', label: 'Honolulu (HST)', offset: '-10:00' },
+ { value: 'Africa/Cairo', label: 'Le Caire (EET)', offset: '+2:00' },
+ { value: 'Africa/Johannesburg', label: 'Johannesburg (SAST)', offset: '+2:00' },
+ { value: 'UTC', label: 'UTC', offset: '+0:00' },
+];
+
+// Paramètres par défaut
+export const DEFAULT_SETTINGS: ClockSettings = {
+ clockType: 'digital',
+ timeFormat: '24h',
+ showSeconds: true,
+ timezone: 'Europe/Paris',
+ themeId: 'midnight',
+};