Add all application pages and their functionalities

Add new routes and components for AI, Calendar, Media, Pages Management, Analytics, History, and Settings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ae4037a0-2a6f-4530-9bac-79b543286bda
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/397bca8c-984f-43ff-841a-10897aeb8140/ae4037a0-2a6f-4530-9bac-79b543286bda/vhNnBGJ
This commit is contained in:
michaelschal committed 2025-10-01 13:36:26 +00:00
1 parent 2f693b744a
commit 5829873f8c
10 files changed
+1091

No files matched your search

+4
View File
@@ -14,6 +14,10 @@ run = ["npm", "run", "start"]
localPort = 5000
externalPort = 80
[[ports]]
localPort = 39259
externalPort = 3000
[env]
PORT = "5000"
+16
View File
@@ -5,11 +5,27 @@ import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/not-found";
import Dashboard from "@/pages/dashboard";
import NewPost from "@/pages/new-post";
import Calendar from "@/pages/calendar";
import Media from "@/pages/media";
import PagesManagement from "@/pages/pages";
import AI from "@/pages/ai";
import Analytics from "@/pages/analytics";
import History from "@/pages/history";
import Settings from "@/pages/settings";
function Router() {
return (
<Switch>
<Route path="/" component={Dashboard} />
<Route path="/new" component={NewPost} />
<Route path="/calendar" component={Calendar} />
<Route path="/media" component={Media} />
<Route path="/pages" component={PagesManagement} />
<Route path="/ai" component={AI} />
<Route path="/analytics" component={Analytics} />
<Route path="/history" component={History} />
<Route path="/settings" component={Settings} />
<Route component={NotFound} />
</Switch>
);
+43
View File
@@ -0,0 +1,43 @@
import { useState } from "react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import AiChat from "@/components/ai-chat";
export default function AI() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Assistant IA</h1>
<p className="text-muted-foreground mt-2">
Générez du contenu personnalisé avec l'intelligence artificielle
</p>
</div>
<div className="max-w-4xl mx-auto">
<AiChat />
</div>
</div>
</main>
</div>
);
}
+110
View File
@@ -0,0 +1,110 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { TrendingUp, Users, Eye, Heart } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function Analytics() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { data: stats } = useQuery({
queryKey: ['/api/stats'],
});
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Analyses</h1>
<p className="text-muted-foreground mt-2">
Statistiques et performances de vos publications
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Publications totales
</CardTitle>
<TrendingUp className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-total-posts">0</div>
<p className="text-xs text-muted-foreground">Toutes plateformes</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Portée totale
</CardTitle>
<Users className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-total-reach">0</div>
<p className="text-xs text-muted-foreground">Personnes atteintes</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Impressions
</CardTitle>
<Eye className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-total-impressions">0</div>
<p className="text-xs text-muted-foreground">Vues totales</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Engagement
</CardTitle>
<Heart className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold" data-testid="text-total-engagement">0</div>
<p className="text-xs text-muted-foreground">Interactions totales</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Fonctionnalité à venir</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Les analyses détaillées et les graphiques de performance seront disponibles prochainement.
</p>
</CardContent>
</Card>
</div>
</main>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { useState } from "react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import CalendarView from "@/components/calendar-view";
export default function Calendar() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Calendrier</h1>
<p className="text-muted-foreground mt-2">
Visualisez et gérez vos publications planifiées
</p>
</div>
<CalendarView />
</div>
</main>
</div>
);
}
+114
View File
@@ -0,0 +1,114 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Clock, CheckCircle, XCircle, Calendar } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import type { Post } from "@shared/schema";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
export default function History() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const { data: posts = [], isLoading } = useQuery<Post[]>({
queryKey: ['/api/posts'],
});
const publishedPosts = posts.filter(p => p.status === 'published');
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Historique</h1>
<p className="text-muted-foreground mt-2">
Consultez l'historique de vos publications
</p>
</div>
{isLoading ? (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Card key={i} className="animate-pulse">
<CardHeader className="h-24 bg-muted"></CardHeader>
</Card>
))}
</div>
) : publishedPosts.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12">
<Clock className="w-12 h-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Aucune publication</h3>
<p className="text-muted-foreground text-center">
Vos publications apparaîtront ici une fois publiées
</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{publishedPosts.map((post) => (
<Card key={post.id} data-testid={`card-post-${post.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle className="text-lg mb-2">
Publication #{post.id}
</CardTitle>
<p className="text-sm text-muted-foreground line-clamp-2">
{post.content}
</p>
</div>
<Badge
variant={post.status === 'published' ? 'default' : 'secondary'}
className="ml-4"
>
{post.status === 'published' && <CheckCircle className="w-3 h-3 mr-1" />}
{post.status === 'failed' && <XCircle className="w-3 h-3 mr-1" />}
{post.status === 'published' ? 'Publié' : 'Échoué'}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{post.updatedAt
? format(new Date(post.updatedAt), "d MMM yyyy 'à' HH:mm", { locale: fr })
: 'Date inconnue'
}
</div>
{post.aiGenerated && (
<Badge variant="outline" className="text-xs">
Généré par IA
</Badge>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</main>
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { useState } from "react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import MediaUpload from "@/components/media-upload";
export default function Media() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Médiathèque</h1>
<p className="text-muted-foreground mt-2">
Gérez vos images et vidéos pour vos publications
</p>
</div>
<MediaUpload />
</div>
</main>
</div>
);
}
+288
View File
@@ -0,0 +1,288 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { useLocation } from "wouter";
import { Send, Sparkles, Image as ImageIcon, Calendar } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import type { SocialPage, Media } from "@shared/schema";
export default function NewPost() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [, navigate] = useLocation();
const { toast } = useToast();
const [productInfo, setProductInfo] = useState('');
const [selectedMedia, setSelectedMedia] = useState<string | null>(null);
const [selectedPages, setSelectedPages] = useState<string[]>([]);
const [scheduledDate, setScheduledDate] = useState('');
const { data: pages = [] } = useQuery<SocialPage[]>({
queryKey: ['/api/pages'],
});
const { data: mediaList = [] } = useQuery<Media[]>({
queryKey: ['/api/media'],
});
const generateTextMutation = useMutation({
mutationFn: (productInfo: string) =>
apiRequest('POST', '/api/ai/generate', { productInfo }),
onSuccess: (data: any) => {
toast({
title: "Texte généré",
description: `${data.variations?.length || 0} variations créées`,
});
queryClient.invalidateQueries({ queryKey: ['/api/ai/generations'] });
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de générer le texte",
variant: "destructive",
});
},
});
const createPostMutation = useMutation({
mutationFn: (data: any) =>
apiRequest('POST', '/api/posts', data),
onSuccess: () => {
toast({
title: "Publication créée",
description: "La publication a été créée avec succès",
});
navigate('/');
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de créer la publication",
variant: "destructive",
});
},
});
const handleGenerateText = () => {
if (!productInfo.trim()) {
toast({
title: "Information manquante",
description: "Veuillez saisir les informations du produit",
variant: "destructive",
});
return;
}
generateTextMutation.mutate(productInfo);
};
const handleCreatePost = () => {
if (!productInfo.trim()) {
toast({
title: "Contenu requis",
description: "Veuillez saisir le contenu de la publication",
variant: "destructive",
});
return;
}
createPostMutation.mutate({
content: productInfo,
scheduledFor: scheduledDate || undefined,
});
};
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6 max-w-5xl mx-auto">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Nouvelle publication</h1>
<p className="text-muted-foreground mt-2">
Créez et planifiez une nouvelle publication
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Contenu</CardTitle>
<CardDescription>Informations du produit ou message</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="productInfo">Informations du produit</Label>
<Textarea
id="productInfo"
value={productInfo}
onChange={(e) => setProductInfo(e.target.value)}
placeholder="Décrivez votre produit : nom, caractéristiques, prix, etc."
rows={6}
data-testid="input-product-info"
/>
</div>
<Button
onClick={handleGenerateText}
disabled={generateTextMutation.isPending}
className="w-full"
data-testid="button-generate-text"
>
<Sparkles className="w-4 h-4 mr-2" />
{generateTextMutation.isPending ? 'Génération...' : 'Générer le texte IA'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Média</CardTitle>
<CardDescription>Sélectionnez une image ou vidéo</CardDescription>
</CardHeader>
<CardContent>
{mediaList.length === 0 ? (
<div className="text-center py-8">
<ImageIcon className="w-12 h-12 mx-auto text-muted-foreground mb-2" />
<p className="text-muted-foreground">Aucun média disponible</p>
<Button
variant="link"
onClick={() => navigate('/media')}
data-testid="link-upload-media"
>
Télécharger des médias
</Button>
</div>
) : (
<div className="grid grid-cols-3 gap-2">
{mediaList.map((media) => (
<button
key={media.id}
onClick={() => setSelectedMedia(media.id)}
className={`relative aspect-square rounded-lg overflow-hidden border-2 transition-all ${
selectedMedia === media.id
? 'border-primary ring-2 ring-primary'
: 'border-transparent hover:border-muted-foreground'
}`}
data-testid={`button-select-media-${media.id}`}
>
<img
src={media.originalUrl}
alt={media.fileName}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
)}
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Pages cibles</CardTitle>
<CardDescription>Sélectionnez les pages où publier</CardDescription>
</CardHeader>
<CardContent>
{pages.length === 0 ? (
<div className="text-center py-8">
<p className="text-muted-foreground mb-2">Aucune page connectée</p>
<Button
variant="link"
onClick={() => navigate('/pages')}
data-testid="link-add-pages"
>
Ajouter des pages
</Button>
</div>
) : (
<div className="space-y-3">
{pages.map((page) => (
<div key={page.id} className="flex items-center space-x-2">
<Checkbox
id={`page-${page.id}`}
checked={selectedPages.includes(page.id)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedPages([...selectedPages, page.id]);
} else {
setSelectedPages(selectedPages.filter(id => id !== page.id));
}
}}
data-testid={`checkbox-page-${page.id}`}
/>
<label
htmlFor={`page-${page.id}`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 flex-1"
>
{page.pageName} ({page.platform})
</label>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Planification</CardTitle>
<CardDescription>Programmez la publication (optionnel)</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="scheduledDate">Date et heure</Label>
<Input
id="scheduledDate"
type="datetime-local"
value={scheduledDate}
onChange={(e) => setScheduledDate(e.target.value)}
data-testid="input-scheduled-date"
/>
<p className="text-xs text-muted-foreground">
Laissez vide pour publier immédiatement
</p>
</div>
</CardContent>
</Card>
<Button
onClick={handleCreatePost}
disabled={createPostMutation.isPending}
className="w-full"
size="lg"
data-testid="button-create-post"
>
<Send className="w-4 h-4 mr-2" />
{createPostMutation.isPending ? 'Création...' : 'Créer la publication'}
</Button>
</div>
</div>
</div>
</main>
</div>
);
}
+295
View File
@@ -0,0 +1,295 @@
import { useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Plus, Facebook, Instagram, Trash2, RefreshCw } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import type { SocialPage } from "@shared/schema";
export default function PagesManagement() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const { toast } = useToast();
const { data: pages = [], isLoading } = useQuery<SocialPage[]>({
queryKey: ['/api/pages'],
});
const deleteMutation = useMutation({
mutationFn: (pageId: string) =>
apiRequest('DELETE', `/api/pages/${pageId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/pages'] });
toast({
title: "Page supprimée",
description: "La page a été retirée avec succès",
});
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible de supprimer la page",
variant: "destructive",
});
},
});
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-foreground">Pages gérées</h1>
<p className="text-muted-foreground mt-2">
Connectez et gérez vos pages Facebook et Instagram
</p>
</div>
<AddPageDialog open={dialogOpen} onOpenChange={setDialogOpen} />
</div>
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<Card key={i} className="animate-pulse">
<CardHeader className="h-32 bg-muted"></CardHeader>
</Card>
))}
</div>
) : pages.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="w-16 h-16 bg-muted rounded-full flex items-center justify-center mb-4">
<Plus className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-semibold mb-2">Aucune page connectée</h3>
<p className="text-muted-foreground text-center mb-4">
Commencez par connecter vos pages Facebook et Instagram
</p>
<Button onClick={() => setDialogOpen(true)} data-testid="button-add-first-page">
<Plus className="w-4 h-4 mr-2" />
Ajouter une page
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{pages.map((page) => (
<Card key={page.id} data-testid={`card-page-${page.id}`}>
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
{page.platform === 'facebook' ? (
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
<Facebook className="w-6 h-6 text-white" />
</div>
) : (
<div className="w-12 h-12 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center">
<Instagram className="w-6 h-6 text-white" />
</div>
)}
<div>
<CardTitle className="text-lg">{page.pageName}</CardTitle>
<CardDescription className="capitalize">
{page.platform}
</CardDescription>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
ID: {page.pageId}
</div>
<Button
variant="ghost"
size="icon"
onClick={() => deleteMutation.mutate(page.id)}
disabled={deleteMutation.isPending}
data-testid={`button-delete-page-${page.id}`}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
</main>
</div>
);
}
function AddPageDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
const [platform, setPlatform] = useState<'facebook' | 'instagram'>('facebook');
const [pageName, setPageName] = useState('');
const [pageId, setPageId] = useState('');
const [accessToken, setAccessToken] = useState('');
const { toast } = useToast();
const addMutation = useMutation({
mutationFn: (data: any) =>
apiRequest('POST', '/api/pages', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/api/pages'] });
toast({
title: "Page ajoutée",
description: "La page a été connectée avec succès",
});
onOpenChange(false);
setPlatform('facebook');
setPageName('');
setPageId('');
setAccessToken('');
},
onError: () => {
toast({
title: "Erreur",
description: "Impossible d'ajouter la page",
variant: "destructive",
});
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
addMutation.mutate({
platform,
pageName,
pageId,
accessToken,
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button data-testid="button-add-page">
<Plus className="w-4 h-4 mr-2" />
Ajouter une page
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Ajouter une page</DialogTitle>
<DialogDescription>
Connectez une page Facebook ou Instagram pour commencer à publier
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="platform">Plateforme</Label>
<Select value={platform} onValueChange={(v) => setPlatform(v as any)}>
<SelectTrigger data-testid="select-platform">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="facebook">Facebook</SelectItem>
<SelectItem value="instagram">Instagram</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="pageName">Nom de la page</Label>
<Input
id="pageName"
value={pageName}
onChange={(e) => setPageName(e.target.value)}
placeholder="Mon entreprise"
required
data-testid="input-page-name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="pageId">ID de la page</Label>
<Input
id="pageId"
value={pageId}
onChange={(e) => setPageId(e.target.value)}
placeholder="123456789"
required
data-testid="input-page-id"
/>
</div>
<div className="space-y-2">
<Label htmlFor="accessToken">Jeton d'accès</Label>
<Input
id="accessToken"
type="password"
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
placeholder="EAAxxxxxxxxxxxxx"
required
data-testid="input-access-token"
/>
<p className="text-xs text-muted-foreground">
Obtenez votre jeton d'accès depuis{' '}
<a
href="https://developers.facebook.com/tools/explorer"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Facebook Graph API Explorer
</a>
</p>
</div>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
data-testid="button-cancel"
>
Annuler
</Button>
<Button
type="submit"
disabled={addMutation.isPending}
data-testid="button-submit-page"
>
{addMutation.isPending ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Ajout...
</>
) : (
'Ajouter'
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
+139
View File
@@ -0,0 +1,139 @@
import { useState } from "react";
import { Settings as SettingsIcon, Bell, Key, Shield } from "lucide-react";
import Sidebar from "@/components/sidebar";
import TopBar from "@/components/topbar";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export default function Settings() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [notifications, setNotifications] = useState(true);
const [autoPublish, setAutoPublish] = useState(true);
return (
<div className="flex h-screen overflow-hidden bg-background">
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
<div className={`
fixed lg:static inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
`}>
<Sidebar />
</div>
<main className="flex-1 overflow-y-auto">
<TopBar onMenuClick={() => setSidebarOpen(!sidebarOpen)} />
<div className="p-6 max-w-4xl mx-auto">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">Paramètres</h1>
<p className="text-muted-foreground mt-2">
Configurez votre application
</p>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="w-5 h-5" />
Notifications
</CardTitle>
<CardDescription>
Gérez vos préférences de notification
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="notifications">Notifications activées</Label>
<p className="text-sm text-muted-foreground">
Recevez des notifications pour les publications
</p>
</div>
<Switch
id="notifications"
checked={notifications}
onCheckedChange={setNotifications}
data-testid="switch-notifications"
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="autoPublish">Publication automatique</Label>
<p className="text-sm text-muted-foreground">
Publier automatiquement à l'heure programmée
</p>
</div>
<Switch
id="autoPublish"
checked={autoPublish}
onCheckedChange={setAutoPublish}
data-testid="switch-auto-publish"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="w-5 h-5" />
Clés API
</CardTitle>
<CardDescription>
Configurez vos clés API pour les intégrations
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="openrouterKey">Clé API OpenRouter</Label>
<Input
id="openrouterKey"
type="password"
placeholder="sk-or-v1-..."
data-testid="input-openrouter-key"
/>
<p className="text-xs text-muted-foreground">
Utilisée pour la génération de texte IA
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="w-5 h-5" />
Sécurité
</CardTitle>
<CardDescription>
Paramètres de sécurité et confidentialité
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Les paramètres de sécurité avancés seront disponibles prochainement.
</p>
</CardContent>
</Card>
<div className="flex justify-end">
<Button data-testid="button-save-settings">
Enregistrer les modifications
</Button>
</div>
</div>
</div>
</main>
</div>
);
}