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:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
349
src/components/settings/BackgroundImageSelector.tsx
Normal file
349
src/components/settings/BackgroundImageSelector.tsx
Normal 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'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'URL d'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'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'effet de l'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'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'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'interface.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user