feat: implement personalized background image feature

- Added functionality for users to select and customize background images in settings, including predefined options and URL uploads.
- Updated `ViewPreferences` to store background image settings and modified `userPreferencesService` to handle updates.
- Enhanced global styles for improved readability with background images, including blur and transparency effects.
- Integrated `BackgroundImageSelector` component into settings for intuitive user experience.
- Refactored `Card` components across the app to use a new 'glass' variant for better aesthetics.
This commit is contained in:
Julien Froidefond
2025-10-01 22:15:11 +02:00
parent 988ffbf774
commit e73e46893f
16 changed files with 648 additions and 23 deletions

View File

@@ -103,7 +103,7 @@ export function DailySection({
onDragEnd={handleDragEnd}
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
>
<Card className="p-0 flex flex-col h-[80vh] sm:h-[600px]">
<Card variant="glass" className="p-0 flex flex-col h-[80vh] sm:h-[600px]">
{/* Header */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">

View File

@@ -121,7 +121,7 @@ export function PendingTasksSection({
const pendingCount = pendingTasks.length;
return (
<Card className="mt-6">
<Card variant="glass" className="mt-6">
<CardHeader>
<div className="flex items-center justify-between">
<button

View File

@@ -0,0 +1,349 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Card, CardContent } from '@/components/ui/Card';
import { useUserPreferences } from '@/contexts/UserPreferencesContext';
// Images de fond prédéfinies basées sur le thème actuel
const PRESET_BACKGROUNDS = [
{
id: 'none',
name: 'Aucune',
description: 'Fond uni par défaut',
preview: 'linear-gradient(135deg, var(--background) 0%, var(--card) 100%)'
},
{
id: 'theme-subtle',
name: 'Dégradé subtil',
description: 'Dégradé doux avec les couleurs du thème',
preview: 'linear-gradient(135deg, var(--background) 0%, var(--card-column) 100%)'
},
{
id: 'theme-primary',
name: 'Dégradé primaire',
description: 'Dégradé avec la couleur primaire du thème',
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 100%)'
},
{
id: 'theme-accent',
name: 'Dégradé accent',
description: 'Dégradé avec la couleur d\'accent du thème',
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--accent) 20%, var(--background)) 100%)'
},
{
id: 'theme-success',
name: 'Dégradé succès',
description: 'Dégradé avec la couleur de succès du thème',
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--success) 20%, var(--background)) 100%)'
},
{
id: 'theme-purple',
name: 'Dégradé violet',
description: 'Dégradé avec la couleur violette du thème',
preview: 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--purple) 20%, var(--background)) 100%)'
},
{
id: 'theme-diagonal',
name: 'Dégradé diagonal',
description: 'Dégradé diagonal avec plusieurs couleurs du thème',
preview: 'linear-gradient(45deg, var(--background) 0%, color-mix(in srgb, var(--primary) 15%, var(--background)) 50%, color-mix(in srgb, var(--accent) 15%, var(--background)) 100%)'
},
{
id: 'theme-radial',
name: 'Dégradé radial',
description: 'Dégradé radial centré avec les couleurs du thème',
preview: 'radial-gradient(circle at center, var(--background) 0%, color-mix(in srgb, var(--primary) 25%, var(--background)) 100%)'
}
];
export function BackgroundImageSelector() {
const { preferences, updateViewPreferences } = useUserPreferences();
const [customUrl, setCustomUrl] = useState('');
const [showCustomInput, setShowCustomInput] = useState(false);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const currentBackground = preferences?.viewPreferences?.backgroundImage;
const backgroundBlur = preferences?.viewPreferences?.backgroundBlur || 0;
const backgroundOpacity = preferences?.viewPreferences?.backgroundOpacity || 100;
const handlePresetSelect = (presetId: string) => {
const backgroundImage = presetId === 'none' ? undefined : presetId;
updateViewPreferences({ backgroundImage });
};
const handleCustomUrlSubmit = () => {
if (!customUrl.trim()) return;
updateViewPreferences({ backgroundImage: customUrl.trim() });
setCustomUrl('');
setShowCustomInput(false);
};
const handleRemoveCustom = () => {
updateViewPreferences({ backgroundImage: undefined });
};
const handleBlurChange = (blur: number) => {
updateViewPreferences({ backgroundBlur: blur });
};
const handleOpacityChange = (opacity: number) => {
updateViewPreferences({ backgroundOpacity: opacity });
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-mono font-semibold text-[var(--foreground)]">Image de fond</h3>
<p className="text-sm text-[var(--muted-foreground)] mt-1">
Personnalisez l&apos;arrière-plan de toutes les pages
</p>
</div>
<div className="text-sm text-[var(--muted-foreground)]">
{currentBackground ? (
<span className="font-medium text-[var(--primary)]">
{PRESET_BACKGROUNDS.find(p => p.id === currentBackground)?.name || 'Personnalisé'}
</span>
) : (
<span className="font-medium text-[var(--muted-foreground)]">Aucune</span>
)}
</div>
</div>
{/* Aperçu actuel */}
<Card>
<CardContent className="p-4">
<div className="text-sm text-[var(--muted-foreground)] mb-3">Aperçu actuel :</div>
<div
className="w-full h-24 rounded-lg border border-[var(--border)]/30 overflow-hidden"
style={{
backgroundImage: currentBackground && !PRESET_BACKGROUNDS.find(p => p.id === currentBackground)
? `url(${currentBackground})`
: PRESET_BACKGROUNDS.find(p => p.id === currentBackground)?.preview || 'var(--background)',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
/>
</CardContent>
</Card>
{/* Images prédéfinies */}
<Card variant="glass">
<CardContent className="p-4">
<div className="text-sm text-[var(--muted-foreground)] mb-4">Images prédéfinies :</div>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{PRESET_BACKGROUNDS.map((preset) => (
<Button
key={preset.id}
onClick={() => handlePresetSelect(preset.id)}
variant={currentBackground === preset.id ? 'selected' : 'secondary'}
className="p-3 h-auto text-left justify-start"
>
<div className="flex items-start gap-3">
{/* Aperçu */}
<div
className="w-12 h-8 rounded border border-[var(--border)]/30 flex-shrink-0"
style={{ background: preset.preview }}
/>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--foreground)] mb-1">
{preset.name}
</div>
<div className="text-xs text-[var(--muted-foreground)] leading-relaxed">
{preset.description}
</div>
{currentBackground === preset.id && (
<div className="mt-1 text-xs text-[var(--primary)] font-medium">
Sélectionné
</div>
)}
</div>
</div>
</Button>
))}
</div>
</CardContent>
</Card>
{/* URL personnalisée */}
<Card>
<CardContent className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-[var(--foreground)]">URL personnalisée</h4>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Ajoutez votre propre image de fond
</p>
</div>
<Button
onClick={() => setShowCustomInput(!showCustomInput)}
variant="secondary"
size="sm"
>
{showCustomInput ? 'Masquer' : 'Ajouter'}
</Button>
</div>
{showCustomInput && (
<div className="space-y-3">
<div className="flex gap-2">
<Input
type="url"
placeholder="https://example.com/image.jpg"
value={customUrl}
onChange={(e) => setCustomUrl(e.target.value)}
className="flex-1"
/>
<Button
onClick={handleCustomUrlSubmit}
disabled={!customUrl.trim()}
className="px-4"
>
Appliquer
</Button>
</div>
<p className="text-xs text-[var(--muted-foreground)]">
Entrez l&apos;URL d&apos;une image (JPG, PNG, GIF, WebP)
</p>
</div>
)}
{/* Bouton pour supprimer l'image personnalisée */}
{currentBackground && !PRESET_BACKGROUNDS.find(p => p.id === currentBackground) && (
<Button
onClick={handleRemoveCustom}
variant="destructive"
className="w-full"
>
Supprimer l&apos;image personnalisée
</Button>
)}
</div>
</CardContent>
</Card>
{/* Options avancées */}
{currentBackground && (
<Card>
<CardContent className="p-4">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-[var(--foreground)]">Options avancées</h4>
<p className="text-xs text-[var(--muted-foreground)] mt-1">
Personnalisez l&apos;effet de l&apos;image de fond
</p>
</div>
<Button
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
variant="secondary"
size="sm"
>
{showAdvancedOptions ? 'Masquer' : 'Afficher'}
</Button>
</div>
{showAdvancedOptions && (
<div className="space-y-4">
{/* Contrôle du blur */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm text-[var(--foreground)]">
Flou d&apos;arrière-plan
</label>
<span className="text-xs text-[var(--muted-foreground)]">
{backgroundBlur}px
</span>
</div>
<input
type="range"
min="0"
max="20"
step="1"
value={backgroundBlur}
onChange={(e) => handleBlurChange(Number(e.target.value))}
className="w-full h-2 bg-[var(--border)] rounded-lg appearance-none cursor-pointer slider"
/>
<div className="flex justify-between text-xs text-[var(--muted-foreground)]">
<span>Aucun</span>
<span>Très flou</span>
</div>
</div>
{/* Contrôle de l'opacité */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm text-[var(--foreground)]">
Opacité de l&apos;image
</label>
<span className="text-xs text-[var(--muted-foreground)]">
{backgroundOpacity}%
</span>
</div>
<input
type="range"
min="10"
max="100"
step="5"
value={backgroundOpacity}
onChange={(e) => handleOpacityChange(Number(e.target.value))}
className="w-full h-2 bg-[var(--border)] rounded-lg appearance-none cursor-pointer slider"
/>
<div className="flex justify-between text-xs text-[var(--muted-foreground)]">
<span>Très transparent</span>
<span>Opaque</span>
</div>
</div>
{/* Aperçu en temps réel */}
<div className="space-y-2">
<label className="text-sm text-[var(--foreground)]">
Aperçu avec les effets appliqués :
</label>
<div
className="w-full h-16 rounded-lg border border-[var(--border)]/30 overflow-hidden"
style={{
backgroundImage: currentBackground && !PRESET_BACKGROUNDS.find(p => p.id === currentBackground)
? `url(${currentBackground})`
: PRESET_BACKGROUNDS.find(p => p.id === currentBackground)?.preview || 'var(--background)',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
filter: `blur(${backgroundBlur}px)`,
opacity: backgroundOpacity / 100
}}
/>
</div>
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Note sur les performances */}
<Card>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="text-lg">💡</div>
<div>
<p className="text-sm text-[var(--warning)] font-medium mb-1">
Conseil pour les performances
</p>
<p className="text-xs text-[var(--muted-foreground)]">
Utilisez des images optimisées pour de meilleures performances.
Les images trop lourdes peuvent ralentir l&apos;interface.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -6,6 +6,7 @@ import { Header } from '@/components/ui/Header';
import { Card, CardContent } from '@/components/ui/Card';
import { TagsManagement } from './tags/TagsManagement';
import { ThemeSelector } from '@/components/ThemeSelector';
import { BackgroundImageSelector } from './BackgroundImageSelector';
import Link from 'next/link';
interface GeneralSettingsPageClientProps {
@@ -49,13 +50,22 @@ export function GeneralSettingsPageClient({ initialTags }: GeneralSettingsPageCl
<div className="space-y-8">
{/* Sélection de thème */}
<div className="bg-[var(--card)]/30 border border-[var(--border)]/50 rounded-lg p-6 backdrop-blur-sm">
<ThemeSelector />
</div>
<Card variant="glass">
<CardContent padding="lg">
<ThemeSelector />
</CardContent>
</Card>
{/* Sélection d'image de fond */}
<Card variant="glass">
<CardContent padding="lg">
<BackgroundImageSelector />
</CardContent>
</Card>
{/* UI Showcase */}
<Card>
<CardContent className="p-6">
<Card variant="glass">
<CardContent padding="lg">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-[var(--foreground)] mb-2">
@@ -83,7 +93,7 @@ export function GeneralSettingsPageClient({ initialTags }: GeneralSettingsPageCl
/>
{/* Note développement futur */}
<Card>
<Card variant="glass">
<CardContent className="p-4">
<div className="p-4 bg-[var(--warning)]/10 border border-[var(--warning)]/20 rounded">
<p className="text-sm text-[var(--warning)] font-medium mb-2">

View File

@@ -1,6 +1,7 @@
'use client';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Tag } from '@/lib/types';
interface TagsGridProps {
@@ -41,16 +42,17 @@ export function TagsGrid({
const usage = tag.usage || 0;
const isUnused = usage === 0;
return (
<div
<Card
key={tag.id}
className={`p-3 rounded-lg border transition-all hover:shadow-sm ${
className={`transition-all hover:shadow-sm ${
isUnused
? 'border-[var(--destructive)]/30 bg-[var(--destructive)]/5 hover:border-[var(--destructive)]/50'
: 'border-[var(--border)] hover:border-[var(--primary)]/50'
: 'hover:border-[var(--primary)]/50'
}`}
>
{/* Header du tag */}
<div className="flex items-center justify-between mb-2">
<div className="p-3">
{/* Header du tag */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
@@ -119,7 +121,8 @@ export function TagsGrid({
</div>
)}
</div>
</div>
</div>
</Card>
);
})}
</div>

View File

@@ -95,7 +95,7 @@ export function TagsManagement({ tags, onRefreshTags, onDeleteTag }: TagsManagem
return (
<>
<Card>
<Card variant="glass">
<CardHeader>
<div className="flex items-center justify-between">
<div>

View File

@@ -111,7 +111,7 @@ export function Calendar({
const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
return (
<Card className={`p-4 ${className}`}>
<Card variant="glass" className={`p-4 ${className}`}>
{/* Header avec navigation */}
<div className="flex items-center justify-between mb-4">
<Button

View File

@@ -2,7 +2,7 @@ import { HTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/utils';
interface CardProps extends HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'elevated' | 'bordered' | 'column';
variant?: 'default' | 'elevated' | 'bordered' | 'column' | 'glass';
shadow?: 'none' | 'sm' | 'md' | 'lg';
border?: 'none' | 'default' | 'primary' | 'accent';
background?: 'default' | 'column' | 'muted';
@@ -35,7 +35,8 @@ const Card = forwardRef<HTMLDivElement, CardProps>(
default: '',
elevated: 'shadow-lg',
bordered: 'border-[var(--primary)]/30 shadow-lg',
column: 'bg-[var(--card-column)] shadow-lg'
column: 'bg-[var(--card-column)] shadow-lg',
glass: 'bg-[var(--card)]/30 border border-[var(--border)]/50 backdrop-blur-sm'
};
// Appliquer le variant si spécifié, sinon utiliser les props individuelles