- Add slug/short_id fields to database with auto-generation

- Create migration script for existing data
- Update admin interface to show only short URLs
- Implement redirect system to avoid code duplication
- Maintain backward compatibility with old URLs
This commit is contained in:
Yannick Le Duc
2025-08-26 22:28:11 +02:00
parent bd4f63b99c
commit caf0478e02
12 changed files with 1040 additions and 110 deletions

View File

@@ -3,6 +3,39 @@ import { Campaign, Proposition, Participant, Vote, ParticipantWithVoteStatus, Se
import { encryptionService } from './encryption';
import { emailService } from './email';
// Fonction utilitaire pour générer un slug côté client
function generateSlugClient(title: string): string {
// Convertir en minuscules et remplacer les caractères spéciaux
let slug = title.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.trim();
// Si le slug est vide, utiliser 'campagne'
if (!slug) {
slug = 'campagne';
}
// Ajouter un timestamp pour éviter les conflits
const timestamp = Date.now().toString().slice(-6);
return `${slug}-${timestamp}`;
}
// Fonction utilitaire pour générer un short_id côté client
function generateShortIdClient(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
// Générer un identifiant de 6 caractères
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
// Ajouter un timestamp pour éviter les conflits
const timestamp = Date.now().toString().slice(-3);
return `${result}${timestamp}`;
}
// Services pour les campagnes
export const campaignService = {
async getAll(): Promise<Campaign[]> {
@@ -17,6 +50,27 @@ export const campaignService = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async create(campaign: any): Promise<Campaign> {
// Générer automatiquement le slug si non fourni
if (!campaign.slug) {
try {
// Essayer d'utiliser la fonction PostgreSQL
const { data: slugData, error: slugError } = await supabase
.rpc('generate_slug', { title: campaign.title });
if (slugError) {
// Si la fonction n'existe pas, générer le slug côté client
console.warn('Fonction generate_slug non disponible, génération côté client:', slugError);
campaign.slug = generateSlugClient(campaign.title);
} else {
campaign.slug = slugData;
}
} catch (error) {
// Fallback vers la génération côté client
console.warn('Erreur avec generate_slug, génération côté client:', error);
campaign.slug = generateSlugClient(campaign.title);
}
}
const { data, error } = await supabase
.from('campaigns')
.insert(campaign)
@@ -29,6 +83,27 @@ export const campaignService = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async update(id: string, updates: any): Promise<Campaign> {
// Générer automatiquement le slug si le titre a changé et qu'aucun slug n'est fourni
if (updates.title && !updates.slug) {
try {
// Essayer d'utiliser la fonction PostgreSQL
const { data: slugData, error: slugError } = await supabase
.rpc('generate_slug', { title: updates.title });
if (slugError) {
// Si la fonction n'existe pas, générer le slug côté client
console.warn('Fonction generate_slug non disponible, génération côté client:', slugError);
updates.slug = generateSlugClient(updates.title);
} else {
updates.slug = slugData;
}
} catch (error) {
// Fallback vers la génération côté client
console.warn('Erreur avec generate_slug, génération côté client:', error);
updates.slug = generateSlugClient(updates.title);
}
}
const { data, error } = await supabase
.from('campaigns')
.update(updates)
@@ -77,6 +152,23 @@ export const campaignService = {
propositions: propositionsResult.count || 0,
participants: participantsResult.count || 0
};
},
// Nouvelle méthode pour récupérer une campagne par slug
async getBySlug(slug: string): Promise<Campaign | null> {
const { data, error } = await supabase
.from('campaigns')
.select('*')
.eq('slug', slug)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Aucune campagne trouvée
}
throw error;
}
return data;
}
};
@@ -160,6 +252,27 @@ export const participantService = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async create(participant: any): Promise<Participant> {
// Générer automatiquement le short_id si non fourni
if (!participant.short_id) {
try {
// Essayer d'utiliser la fonction PostgreSQL
const { data: shortIdData, error: shortIdError } = await supabase
.rpc('generate_short_id');
if (shortIdError) {
// Si la fonction n'existe pas, générer le short_id côté client
console.warn('Fonction generate_short_id non disponible, génération côté client:', shortIdError);
participant.short_id = generateShortIdClient();
} else {
participant.short_id = shortIdData;
}
} catch (error) {
// Fallback vers la génération côté client
console.warn('Erreur avec generate_short_id, génération côté client:', error);
participant.short_id = generateShortIdClient();
}
}
const { data, error } = await supabase
.from('participants')
.insert(participant)
@@ -207,6 +320,23 @@ export const participantService = {
.eq('id', id);
if (error) throw error;
},
// Nouvelle méthode pour récupérer un participant par short_id
async getByShortId(shortId: string): Promise<Participant | null> {
const { data, error } = await supabase
.from('participants')
.select('*')
.eq('short_id', shortId)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Aucun participant trouvé
}
throw error;
}
return data;
}
};