456 lines
19 KiB
TypeScript
456 lines
19 KiB
TypeScript
'use client';
|
|
import { useState, useEffect } from 'react';
|
|
import { Setting } from '@/types';
|
|
import { settingsService } from '@/lib/services';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { Label } from '@/components/ui/label';
|
|
import Navigation from '@/components/Navigation';
|
|
import AuthGuard from '@/components/AuthGuard';
|
|
import Footer from '@/components/Footer';
|
|
import SmtpSettingsForm from '@/components/SmtpSettingsForm';
|
|
import { Settings, Monitor, Save, CheckCircle, Mail, FileText, Download } from 'lucide-react';
|
|
import { ExportAnonymizationSelect, AnonymizationLevel } from '@/components/ExportAnonymizationSelect';
|
|
import { ExportFileFormatSelect, ExportFileFormat } from '@/components/ExportFileFormatSelect';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function SettingsPageContent() {
|
|
const [settings, setSettings] = useState<Setting[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [saved, setSaved] = useState(false);
|
|
const [randomizePropositions, setRandomizePropositions] = useState(false);
|
|
const [proposePageMessage, setProposePageMessage] = useState('');
|
|
const [footerMessage, setFooterMessage] = useState('');
|
|
const [exportAnonymization, setExportAnonymization] = useState<AnonymizationLevel>('full');
|
|
const [exportFileFormat, setExportFileFormat] = useState<ExportFileFormat>('ods');
|
|
|
|
// États pour la détection des modifications
|
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
const [originalValues, setOriginalValues] = useState<{
|
|
randomizePropositions: boolean;
|
|
proposePageMessage: string;
|
|
footerMessage: string;
|
|
exportAnonymization: AnonymizationLevel;
|
|
exportFileFormat: ExportFileFormat;
|
|
} | null>(null);
|
|
const [autoSaved, setAutoSaved] = useState(false);
|
|
|
|
useEffect(() => {
|
|
// Vérifier la configuration Supabase
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
|
|
// Si pas de configuration ou valeurs par défaut, rediriger vers setup
|
|
if (!supabaseUrl || !supabaseAnonKey ||
|
|
supabaseUrl === 'https://placeholder.supabase.co' ||
|
|
supabaseAnonKey === 'your-anon-key') {
|
|
console.log('🔧 Configuration Supabase manquante, redirection vers /setup');
|
|
window.location.href = '/setup';
|
|
return;
|
|
}
|
|
|
|
loadSettings();
|
|
}, []);
|
|
|
|
// Détecter les modifications
|
|
useEffect(() => {
|
|
if (!originalValues) return;
|
|
|
|
const hasChanges =
|
|
randomizePropositions !== originalValues.randomizePropositions ||
|
|
proposePageMessage !== originalValues.proposePageMessage ||
|
|
footerMessage !== originalValues.footerMessage ||
|
|
exportAnonymization !== originalValues.exportAnonymization ||
|
|
exportFileFormat !== originalValues.exportFileFormat;
|
|
|
|
setHasUnsavedChanges(hasChanges);
|
|
}, [randomizePropositions, proposePageMessage, footerMessage, exportAnonymization, exportFileFormat, originalValues]);
|
|
|
|
// Avertissement avant de quitter la page
|
|
useEffect(() => {
|
|
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
if (hasUnsavedChanges) {
|
|
e.preventDefault();
|
|
e.returnValue = 'Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?';
|
|
return e.returnValue;
|
|
}
|
|
};
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
}, [hasUnsavedChanges]);
|
|
|
|
const loadSettings = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const settingsData = await settingsService.getAll();
|
|
setSettings(settingsData);
|
|
|
|
// Charger la valeur du paramètre d'ordre aléatoire
|
|
const randomizeValue = await settingsService.getBooleanValue('randomize_propositions', true);
|
|
setRandomizePropositions(randomizeValue);
|
|
|
|
// Charger le message de la page de dépôt de propositions
|
|
const messageValue = await settingsService.getStringValue('propose_page_message', 'Partagez votre vision et proposez des projets qui feront la différence dans votre collectif. Votre voix compte pour façonner l\'avenir de votre communauté.');
|
|
setProposePageMessage(messageValue);
|
|
|
|
// Charger le message du bas de page
|
|
const footerValue = await settingsService.getStringValue('footer_message', 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)');
|
|
setFooterMessage(footerValue);
|
|
|
|
// Charger le niveau d'anonymisation des exports
|
|
const anonymizationValue = await settingsService.getStringValue('export_anonymization', 'full') as AnonymizationLevel;
|
|
setExportAnonymization(anonymizationValue);
|
|
|
|
// Charger le format de fichier d'export
|
|
const fileFormatValue = await settingsService.getStringValue('export_file_format', 'ods') as ExportFileFormat;
|
|
setExportFileFormat(fileFormatValue);
|
|
|
|
// Stocker les valeurs originales pour la détection des modifications
|
|
setOriginalValues({
|
|
randomizePropositions: randomizeValue,
|
|
proposePageMessage: messageValue,
|
|
footerMessage: footerValue,
|
|
exportAnonymization: anonymizationValue,
|
|
exportFileFormat: fileFormatValue
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des paramètres:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRandomizeChange = async (checked: boolean) => {
|
|
setRandomizePropositions(checked);
|
|
// Sauvegarde automatique pour ce paramètre
|
|
try {
|
|
await settingsService.setBooleanValue('randomize_propositions', checked);
|
|
// Mettre à jour les valeurs originales
|
|
if (originalValues) {
|
|
setOriginalValues({
|
|
...originalValues,
|
|
randomizePropositions: checked
|
|
});
|
|
}
|
|
// Afficher la confirmation de sauvegarde automatique
|
|
setAutoSaved(true);
|
|
setTimeout(() => setAutoSaved(false), 2000);
|
|
} catch (error) {
|
|
console.error('Erreur lors de la sauvegarde automatique:', error);
|
|
}
|
|
};
|
|
|
|
const handleExportAnonymizationChange = async (value: AnonymizationLevel) => {
|
|
setExportAnonymization(value);
|
|
// Sauvegarde automatique pour ce paramètre
|
|
try {
|
|
await settingsService.setStringValue('export_anonymization', value);
|
|
// Mettre à jour les valeurs originales
|
|
if (originalValues) {
|
|
setOriginalValues({
|
|
...originalValues,
|
|
exportAnonymization: value
|
|
});
|
|
}
|
|
// Afficher la confirmation de sauvegarde automatique
|
|
setAutoSaved(true);
|
|
setTimeout(() => setAutoSaved(false), 2000);
|
|
} catch (error) {
|
|
console.error('Erreur lors de la sauvegarde automatique:', error);
|
|
}
|
|
};
|
|
|
|
const handleExportFileFormatChange = async (value: ExportFileFormat) => {
|
|
setExportFileFormat(value);
|
|
// Sauvegarde automatique pour ce paramètre
|
|
try {
|
|
await settingsService.setStringValue('export_file_format', value);
|
|
// Mettre à jour les valeurs originales
|
|
if (originalValues) {
|
|
setOriginalValues({
|
|
...originalValues,
|
|
exportFileFormat: value
|
|
});
|
|
}
|
|
// Afficher la confirmation de sauvegarde automatique
|
|
setAutoSaved(true);
|
|
setTimeout(() => setAutoSaved(false), 2000);
|
|
} catch (error) {
|
|
console.error('Erreur lors de la sauvegarde automatique:', error);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
setSaving(true);
|
|
// Sauvegarder seulement les paramètres qui ne sont pas sauvegardés automatiquement
|
|
await settingsService.setStringValue('propose_page_message', proposePageMessage);
|
|
await settingsService.setStringValue('footer_message', footerMessage);
|
|
|
|
// Mettre à jour les valeurs originales
|
|
setOriginalValues({
|
|
randomizePropositions,
|
|
proposePageMessage,
|
|
footerMessage,
|
|
exportAnonymization,
|
|
exportFileFormat
|
|
});
|
|
|
|
setSaved(true);
|
|
setTimeout(() => setSaved(false), 3000); // Message plus long pour les textes
|
|
} catch (error) {
|
|
console.error('Erreur lors de la sauvegarde des paramètres:', error);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
|
<div className="container mx-auto px-4 py-8">
|
|
<Navigation showBackButton={true} backUrl="/admin" />
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900 dark:border-slate-100 mx-auto mb-4"></div>
|
|
<p className="text-slate-600 dark:text-slate-300">Chargement des paramètres...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
|
<div className="container mx-auto px-4 py-8">
|
|
<Navigation showBackButton={true} backUrl="/admin" />
|
|
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-3xl font-bold text-slate-900 dark:text-slate-100">Paramètres</h1>
|
|
{hasUnsavedChanges && (
|
|
<div className="flex items-center gap-2 px-3 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded-full text-sm font-medium">
|
|
<div className="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
|
|
Modifications non sauvegardées
|
|
</div>
|
|
)}
|
|
{autoSaved && (
|
|
<div className="flex items-center gap-2 px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-sm font-medium">
|
|
<CheckCircle className="w-4 h-4" />
|
|
Sauvegardé automatiquement
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-slate-600 dark:text-slate-300 mt-2">
|
|
{hasUnsavedChanges
|
|
? 'Vous avez des modifications non sauvegardées. N\'oubliez pas de cliquer sur "Sauvegarder".'
|
|
: 'Configurez les paramètres de l\'application'
|
|
}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
onClick={handleSave}
|
|
disabled={saving || !hasUnsavedChanges}
|
|
className={`flex items-center gap-2 ${
|
|
hasUnsavedChanges
|
|
? 'bg-orange-600 hover:bg-orange-700 text-white'
|
|
: ''
|
|
}`}
|
|
>
|
|
{saving ? (
|
|
<>
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
Sauvegarde...
|
|
</>
|
|
) : saved ? (
|
|
<>
|
|
<CheckCircle className="w-4 h-4" />
|
|
Sauvegardé
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="w-4 h-4" />
|
|
Sauvegarder
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Settings Categories */}
|
|
<div className="space-y-8">
|
|
{/* Affichage Category */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
|
|
<Monitor className="w-5 h-5 text-blue-600 dark:text-blue-300" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Affichage</CardTitle>
|
|
<CardDescription>
|
|
Paramètres d'affichage de l'interface utilisateur
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Randomize Propositions Setting */}
|
|
<div className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-800 rounded-lg">
|
|
<div className="flex-1">
|
|
<Label htmlFor="randomize-propositions" className="text-base font-medium">
|
|
Afficher les propositions dans un ordre aléatoire lors du vote
|
|
</Label>
|
|
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
|
Lorsque activé, les propositions seront affichées dans un ordre aléatoire pour chaque participant lors du vote.
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="randomize-propositions"
|
|
checked={randomizePropositions}
|
|
onCheckedChange={handleRandomizeChange}
|
|
className="ml-4"
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Textes Category */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-green-100 dark:bg-green-900 rounded-lg flex items-center justify-center">
|
|
<FileText className="w-5 h-5 text-green-600 dark:text-green-300" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Textes</CardTitle>
|
|
<CardDescription>
|
|
Personnalisez les textes affichés dans l'application
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Propose Page Message Setting */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="propose-page-message" className="text-base font-medium">
|
|
Message d'invitation - Page de dépôt de propositions
|
|
</Label>
|
|
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
|
Ce texte apparaît sous le titre de la campagne pour inviter les utilisateurs à déposer des propositions.
|
|
</p>
|
|
</div>
|
|
<div className="relative">
|
|
<textarea
|
|
id="propose-page-message"
|
|
value={proposePageMessage}
|
|
onChange={(e) => setProposePageMessage(e.target.value)}
|
|
className={`w-full min-h-[100px] p-3 border rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y ${
|
|
originalValues && proposePageMessage !== originalValues.proposePageMessage
|
|
? 'border-orange-300 dark:border-orange-600 bg-orange-50 dark:bg-orange-900/20'
|
|
: 'border-slate-200 dark:border-slate-700'
|
|
}`}
|
|
placeholder="Entrez votre message d'invitation..."
|
|
/>
|
|
{originalValues && proposePageMessage !== originalValues.proposePageMessage && (
|
|
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
|
|
<div className="w-1.5 h-1.5 bg-orange-500 rounded-full animate-pulse"></div>
|
|
Modifié
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Footer Message Setting */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="footer-message" className="text-base font-medium">
|
|
Message du bas de page
|
|
</Label>
|
|
<p className="text-sm text-slate-600 dark:text-slate-300 mt-1">
|
|
Ce texte apparaît en bas des pages publiques. Vous pouvez utiliser <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded text-xs">[texte du lien](GITURL)</code> pour insérer un lien vers le repository Git.
|
|
</p>
|
|
</div>
|
|
<div className="relative">
|
|
<textarea
|
|
id="footer-message"
|
|
value={footerMessage}
|
|
onChange={(e) => setFooterMessage(e.target.value)}
|
|
className={`w-full min-h-[80px] p-3 border rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 resize-y ${
|
|
originalValues && footerMessage !== originalValues.footerMessage
|
|
? 'border-orange-300 dark:border-orange-600 bg-orange-50 dark:bg-orange-900/20'
|
|
: 'border-slate-200 dark:border-slate-700'
|
|
}`}
|
|
placeholder="Entrez votre message de bas de page..."
|
|
/>
|
|
{originalValues && footerMessage !== originalValues.footerMessage && (
|
|
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
|
|
<div className="w-1.5 h-1.5 bg-orange-500 rounded-full animate-pulse"></div>
|
|
Modifié
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Exports Category */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 bg-purple-100 dark:bg-purple-900 rounded-lg flex items-center justify-center">
|
|
<Download className="w-5 h-5 text-purple-600 dark:text-purple-300" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Exports</CardTitle>
|
|
<CardDescription>
|
|
Paramètres de confidentialité pour les exports de données
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="p-4 bg-slate-50 dark:bg-slate-800 rounded-lg space-y-6">
|
|
<ExportAnonymizationSelect
|
|
value={exportAnonymization}
|
|
onValueChange={handleExportAnonymizationChange}
|
|
/>
|
|
<ExportFileFormatSelect
|
|
value={exportFileFormat}
|
|
onValueChange={handleExportFileFormatChange}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Email Category */}
|
|
<SmtpSettingsForm onSave={() => {
|
|
setSaved(true);
|
|
setTimeout(() => setSaved(false), 2000);
|
|
}} />
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<Footer />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SettingsPage() {
|
|
return (
|
|
<AuthGuard>
|
|
<SettingsPageContent />
|
|
</AuthGuard>
|
|
);
|
|
}
|