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

@@ -32,6 +32,31 @@ export async function updateViewPreferences(updates: Partial<ViewPreferences>):
}
}
/**
* Met à jour l'image de fond
*/
export async function setBackgroundImage(backgroundImage: string | undefined): Promise<{
success: boolean;
error?: string;
}> {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return { success: false, error: 'Non authentifié' };
}
await userPreferencesService.updateViewPreferences(session.user.id, { backgroundImage });
revalidatePath('/');
return { success: true };
} catch (error) {
console.error('Erreur setBackgroundImage:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Erreur inconnue'
};
}
}
/**
* Met à jour les filtres Kanban
*/

View File

@@ -424,10 +424,45 @@
}
body {
background: var(--background);
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-geist-mono), 'Courier New', monospace;
overflow-x: hidden;
transition: background-image 0.3s ease-in-out;
}
/* Styles pour les images de fond */
body.has-background-image {
/* Assurer que le contenu reste lisible avec une image de fond */
position: relative;
}
body.has-background-image::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.1);
pointer-events: none;
z-index: -1;
}
/* Améliorer la lisibilité des cartes avec image de fond */
body.has-background-image .bg-\[var\(--card\)\] {
background-color: color-mix(in srgb, var(--card) 90%, transparent) !important;
backdrop-filter: blur(8px);
}
body.has-background-image .bg-\[var\(--card\)\]\/30 {
background-color: color-mix(in srgb, var(--card) 20%, transparent) !important;
backdrop-filter: blur(12px);
}
/* Rendre les conteneurs principaux transparents avec image de fond */
body.has-background-image .min-h-screen.bg-\[var\(--background\)\] {
background-color: transparent !important;
}
/* Scrollbar tech style */
@@ -552,3 +587,35 @@ body {
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Styles pour les sliders */
.slider::-webkit-slider-thumb {
appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: 2px solid var(--background);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.slider::-webkit-slider-thumb:hover {
background: color-mix(in srgb, var(--primary) 80%, var(--accent) 20%);
transform: scale(1.1);
}
.slider::-moz-range-thumb {
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
border: 2px solid var(--background);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.slider::-moz-range-thumb:hover {
background: color-mix(in srgb, var(--primary) 80%, var(--accent) 20%);
transform: scale(1.1);
}

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/contexts/ThemeContext";
import { BackgroundProvider } from "@/contexts/BackgroundContext";
import { JiraConfigProvider } from "@/contexts/JiraConfigContext";
import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext";
import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext";
@@ -54,7 +55,9 @@ export default async function RootLayout({
<KeyboardShortcuts />
<JiraConfigProvider config={initialPreferences?.jiraConfig || { enabled: false }}>
<UserPreferencesProvider initialPreferences={initialPreferences}>
{children}
<BackgroundProvider>
{children}
</BackgroundProvider>
</UserPreferencesProvider>
</JiraConfigProvider>
</KeyboardShortcutsProvider>

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

View File

@@ -0,0 +1,134 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { useUserPreferences } from './UserPreferencesContext';
interface BackgroundContextType {
backgroundImage: string | undefined;
setBackgroundImage: (image: string | undefined) => void;
}
const BackgroundContext = createContext<BackgroundContextType | undefined>(undefined);
interface BackgroundProviderProps {
children: ReactNode;
}
export function BackgroundProvider({ children }: BackgroundProviderProps) {
const { preferences } = useUserPreferences();
const [backgroundImage, setBackgroundImageState] = useState<string | undefined>(
preferences?.viewPreferences?.backgroundImage
);
const [backgroundBlur, setBackgroundBlurState] = useState<number>(
preferences?.viewPreferences?.backgroundBlur || 0
);
const [backgroundOpacity, setBackgroundOpacityState] = useState<number>(
preferences?.viewPreferences?.backgroundOpacity || 100
);
const [mounted, setMounted] = useState(false);
// Hydration safe initialization
useEffect(() => {
setMounted(true);
}, []);
// Sync with preferences
useEffect(() => {
if (preferences?.viewPreferences?.backgroundImage !== backgroundImage) {
setBackgroundImageState(preferences?.viewPreferences?.backgroundImage);
}
if (preferences?.viewPreferences?.backgroundBlur !== backgroundBlur) {
setBackgroundBlurState(preferences?.viewPreferences?.backgroundBlur || 0);
}
if (preferences?.viewPreferences?.backgroundOpacity !== backgroundOpacity) {
setBackgroundOpacityState(preferences?.viewPreferences?.backgroundOpacity || 100);
}
}, [preferences?.viewPreferences?.backgroundImage, preferences?.viewPreferences?.backgroundBlur, preferences?.viewPreferences?.backgroundOpacity, backgroundImage, backgroundBlur, backgroundOpacity]);
// Apply background image to document body
useEffect(() => {
if (mounted) {
const body = document.body;
// Supprimer l'ancien élément de fond s'il existe
const existingBackground = document.getElementById('custom-background');
if (existingBackground) {
existingBackground.remove();
}
if (backgroundImage) {
// Créer un élément div pour l'image de fond avec les effets
const backgroundElement = document.createElement('div');
backgroundElement.id = 'custom-background';
backgroundElement.style.position = 'fixed';
backgroundElement.style.top = '0';
backgroundElement.style.left = '0';
backgroundElement.style.width = '100%';
backgroundElement.style.height = '100%';
backgroundElement.style.zIndex = '-1';
backgroundElement.style.pointerEvents = 'none';
// Vérifier si c'est une URL d'image ou un preset
const PRESET_BACKGROUNDS = [
'theme-subtle',
'theme-primary',
'theme-accent',
'theme-success',
'theme-purple',
'theme-diagonal',
'theme-radial'
];
if (PRESET_BACKGROUNDS.includes(backgroundImage)) {
// Appliquer le preset basé sur le thème
const presetStyles = {
'theme-subtle': 'linear-gradient(135deg, var(--background) 0%, var(--card-column) 100%)',
'theme-primary': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--primary) 20%, var(--background)) 100%)',
'theme-accent': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--accent) 20%, var(--background)) 100%)',
'theme-success': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--success) 20%, var(--background)) 100%)',
'theme-purple': 'linear-gradient(135deg, var(--background) 0%, color-mix(in srgb, var(--purple) 20%, var(--background)) 100%)',
'theme-diagonal': '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%)',
'theme-radial': 'radial-gradient(circle at center, var(--background) 0%, color-mix(in srgb, var(--primary) 25%, var(--background)) 100%)'
};
backgroundElement.style.backgroundImage = presetStyles[backgroundImage as keyof typeof presetStyles];
} else {
// Appliquer l'URL d'image personnalisée
backgroundElement.style.backgroundImage = `url(${backgroundImage})`;
}
// Appliquer les propriétés communes
backgroundElement.style.backgroundSize = 'cover';
backgroundElement.style.backgroundPosition = 'center';
backgroundElement.style.backgroundRepeat = 'no-repeat';
backgroundElement.style.filter = `blur(${backgroundBlur}px)`;
backgroundElement.style.opacity = `${backgroundOpacity / 100}`;
// Ajouter l'élément au body
body.appendChild(backgroundElement);
body.classList.add('has-background-image');
} else {
// Supprimer l'image de fond
body.classList.remove('has-background-image');
}
}
}, [backgroundImage, backgroundBlur, backgroundOpacity, mounted]);
const setBackgroundImage = (image: string | undefined) => {
setBackgroundImageState(image);
};
return (
<BackgroundContext.Provider value={{ backgroundImage, setBackgroundImage }}>
{children}
</BackgroundContext.Provider>
);
}
export function useBackground() {
const context = useContext(BackgroundContext);
if (context === undefined) {
throw new Error('useBackground must be used within a BackgroundProvider');
}
return context;
}

View File

@@ -57,7 +57,10 @@ const defaultPreferences: UserPreferences = {
showFilters: true,
objectivesCollapsed: false,
theme: 'light',
fontSize: 'medium'
fontSize: 'medium',
backgroundImage: undefined,
backgroundBlur: 0,
backgroundOpacity: 100
},
columnVisibility: {
hiddenStatuses: []

View File

@@ -104,6 +104,9 @@ export interface ViewPreferences {
objectivesCollapsed: boolean;
theme: Theme;
fontSize: 'small' | 'medium' | 'large';
backgroundImage?: string;
backgroundBlur?: number;
backgroundOpacity?: number;
[key: string]:
| boolean
| 'tags'
@@ -112,6 +115,8 @@ export interface ViewPreferences {
| 'small'
| 'medium'
| 'large'
| string
| number
| undefined;
}

View File

@@ -29,7 +29,10 @@ const DEFAULT_PREFERENCES: UserPreferences = {
showFilters: true,
objectivesCollapsed: false,
theme: 'dark',
fontSize: 'medium'
fontSize: 'medium',
backgroundImage: undefined,
backgroundBlur: 0,
backgroundOpacity: 100
},
columnVisibility: {
hiddenStatuses: []