j'ai oublié de supprimé les fichiers code morts

This commit is contained in:
Movix 2026-05-10 16:39:13 +02:00
parent 4bba4cab40
commit e57910d274
10 changed files with 0 additions and 2389 deletions

View file

@ -1,252 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { verifyAdminCode, isAdminAuthenticated, logoutAdmin } from '../services/adminService';
import { Lock, Unlock, LogOut } from 'lucide-react';
import { checkDiscordMembership } from '../utils/discord';
import { DISCORD_CONFIG } from '../config/discord';
interface AdminLoginProps {
onAdminStatusChange?: (isAdmin: boolean) => void;
}
const AdminLogin: React.FC<AdminLoginProps> = ({ onAdminStatusChange }) => {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [adminCode, setAdminCode] = useState('');
const [isAdmin, setIsAdmin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [error, setError] = useState('');
// Check if user is already authenticated as admin
useEffect(() => {
// Ignorer la vérification si on est en train de se déconnecter
if (isLoggingOut) return;
const checkAdminStatus = async () => {
console.log("Vérification du statut admin au chargement");
// Check if authenticated via Discord
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
if (isDiscordAuth) {
try {
const discordUser = JSON.parse(localStorage.getItem('discord_user') || '{}');
// Si l'utilisateur est déjà identifié comme admin via Discord, conserver son statut
if (discordUser.isAdmin) {
console.log("User is already admin via Discord role (cached)");
setIsAdmin(true);
if (onAdminStatusChange) {
onAdminStatusChange(true);
}
return;
}
// Check if we need to refresh the role info
const lastCheck = parseInt(localStorage.getItem('discord_last_check') || '0');
const now = Date.now();
const needsRefresh = now - lastCheck > (DISCORD_CONFIG.CACHE_DURATION * 1000);
if (needsRefresh) {
console.log("Refreshing Discord roles...");
const accessToken = localStorage.getItem('discord_token');
if (accessToken) {
const membershipData = await checkDiscordMembership(accessToken);
// Ne pas modifier l'état si on est rate limited et qu'on n'a pas de données valides
if (membershipData.isRateLimited && !membershipData.isAdmin) {
console.log("Rate limited, preserving current admin status");
return;
}
const isDiscordAdmin = membershipData.isAdmin;
// Update the user info in localStorage
const updatedUser = {
...discordUser,
roles: membershipData.roles,
isAdmin: isDiscordAdmin
};
localStorage.setItem('discord_user', JSON.stringify(updatedUser));
localStorage.setItem('discord_last_check', now.toString());
if (isDiscordAdmin) {
console.log("User is admin via Discord role (fresh check)");
setIsAdmin(true);
if (onAdminStatusChange) {
onAdminStatusChange(true);
}
return;
}
}
}
} catch (error) {
console.error("Error checking Discord admin status:", error);
// En cas d'erreur, on conserve le statut admin actuel si l'utilisateur l'était déjà
if (isAdmin) {
console.log("Error during Discord check, preserving admin status");
return;
}
}
}
// Fallback to traditional admin authentication
const adminStatus = await isAdminAuthenticated();
console.log(`Statut admin traditionnel: ${adminStatus}`);
setIsAdmin(adminStatus);
if (onAdminStatusChange) {
onAdminStatusChange(adminStatus);
}
};
checkAdminStatus();
}, [onAdminStatusChange, isLoggingOut, isAdmin]);
const handleAdminLogin = async () => {
if (!adminCode.trim()) {
setError(t('admin.enterAdminCode'));
return;
}
setIsLoading(true);
setError('');
try {
console.log(`Tentative de connexion avec le code: ${adminCode}`);
const isValid = await verifyAdminCode(adminCode);
console.log(`Résultat de la vérification: ${isValid}`);
if (isValid) {
setIsAdmin(true);
setIsOpen(false);
setAdminCode('');
if (onAdminStatusChange) {
onAdminStatusChange(true);
}
} else {
setError(t('admin.invalidAdminCode'));
}
} catch (err) {
console.error('Erreur complète:', err);
setError(t('admin.codeVerificationError'));
} finally {
setIsLoading(false);
}
};
const handleLogout = async () => {
setIsLoggingOut(true);
try {
await logoutAdmin();
// Attendre un court instant pour s'assurer que le localStorage est bien mis à jour
setTimeout(() => {
setIsAdmin(false);
if (onAdminStatusChange) {
onAdminStatusChange(false);
}
setIsLoggingOut(false);
}, 100);
} catch (error) {
console.error('Erreur lors de la déconnexion:', error);
setIsLoggingOut(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleAdminLogin();
}
};
// Vérifier si on doit afficher le bouton Admin basé sur les rôles Discord
const shouldShowAdminButton = () => {
// Si déjà authentifié comme admin, on affiche toujours le bouton
if (isAdmin) return true;
// Vérifier l'authentification Discord
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
if (isDiscordAuth) {
try {
const discordUser = JSON.parse(localStorage.getItem('discord_user') || '{}');
return discordUser.isAdmin || false;
} catch {
return false;
}
}
// Si l'utilisateur n'est pas connecté via Discord, ne pas afficher le bouton
return false;
};
// Si on ne doit pas afficher le bouton, retourner null
if (!shouldShowAdminButton()) {
return null;
}
return (
<div className="relative">
{isAdmin ? (
<button
onClick={handleLogout}
disabled={isLoggingOut}
className="flex items-center space-x-1 bg-green-600 hover:bg-green-700 px-3 py-1 rounded-md text-white text-sm transition-colors duration-200 disabled:opacity-70"
>
{isLoggingOut ? (
<>
<div className="animate-spin h-3 w-3 border-2 border-white border-t-transparent rounded-full mr-1"></div>
<span>{t('admin.loggingOut')}</span>
</>
) : (
<>
<Unlock size={14} />
<span>{t('admin.title')}</span>
<LogOut size={14} />
</>
)}
</button>
) : (
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center space-x-1 bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded-md text-white text-sm transition-colors duration-200"
>
<Lock size={14} />
<span>{t('admin.title')}</span>
</button>
)}
{isOpen && !isAdmin && (
<div className="absolute top-10 right-0 mt-2 bg-gray-800 border border-gray-700 rounded-md shadow-lg p-4 w-64 z-50">
<h3 className="text-white font-medium mb-2">{t('admin.adminLogin')}</h3>
<div className="space-y-3">
<div>
<input
type="password"
value={adminCode}
onChange={(e) => setAdminCode(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={t('admin.adminCode')}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-md text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
</div>
{error && <p className="text-red-500 text-xs">{error}</p>}
<button
onClick={handleAdminLogin}
disabled={isLoading || !adminCode.trim()}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 disabled:opacity-70 px-3 py-2 rounded-md text-white transition-colors duration-200 flex items-center justify-center"
>
{isLoading ? (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
) : (
t('admin.verify')
)}
</button>
</div>
</div>
)}
</div>
);
};
export default AdminLogin;

View file

@ -1,557 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Comment, COMMENT_LENGTH_LIMITS } from '../types/Comment';
import { ThumbsUp, ThumbsDown, Reply, Trash2, Send, Clock } from 'lucide-react';
import { likeComment, dislikeComment, deleteComment, addComment, reactWithEmoji } from '../services/commentService';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
import EmojiPicker from './EmojiPicker';
import ReactionBar from './ReactionBar';
import { addReplyNotification } from '../services/notificationService';
import ReactMarkdown from 'react-markdown';
import { safeRemarkGfm } from '../utils/markdownPlugins';
import remarkEmoji from 'remark-emoji';
import MarkdownToolbar from './MarkdownToolbar';
// Définition de la limite de caractères pour les réponses
const MAX_REPLY_LENGTH = COMMENT_LENGTH_LIMITS.REPLY;
const markdownComponents = {
p: ({ children }: any) => <p className="mb-1 last:mb-0">{children}</p>,
strong: ({ children }: any) => <strong className="font-bold text-white">{children}</strong>,
em: ({ children }: any) => <em className="italic">{children}</em>,
code: ({ children, className }: any) => {
const isBlock = className?.includes('language-');
return isBlock ? (
<pre className="bg-gray-900/50 rounded p-2 my-1 overflow-x-auto text-xs">
<code className={className}>{children}</code>
</pre>
) : (
<code className="bg-gray-900/50 text-blue-300 px-1 py-0.5 rounded text-[0.85em]">{children}</code>
);
},
pre: ({ children }: any) => <>{children}</>,
a: ({ href, children }: any) => (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:underline break-all">
{children}
</a>
),
ul: ({ children }: any) => <ul className="list-disc list-inside ml-2 my-1">{children}</ul>,
ol: ({ children }: any) => <ol className="list-decimal list-inside ml-2 my-1">{children}</ol>,
blockquote: ({ children }: any) => (
<blockquote className="border-l-2 border-gray-500 pl-2 my-1 text-gray-400 italic">{children}</blockquote>
),
del: ({ children }: any) => <del className="line-through text-gray-500">{children}</del>,
// Bloquer les images et headings dans les commentaires
img: () => null,
h1: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
h2: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
h3: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
h4: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
h5: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
h6: ({ children }: any) => <p className="font-bold text-white mb-1">{children}</p>,
};
const remarkPlugins = safeRemarkGfm ? [safeRemarkGfm, remarkEmoji] : [remarkEmoji];
const REPLY_COOLDOWN_TIME = 15; // Cooldown plus court pour les réponses (15 secondes)
interface CommentItemProps {
comment: Comment;
currentUserId: string | null;
contentId: string;
contentType: 'movie' | 'series';
refreshComments: () => void;
isAdmin?: boolean;
}
const CommentItem: React.FC<CommentItemProps> = ({
comment,
currentUserId,
contentId,
contentType,
refreshComments,
isAdmin = false
}) => {
const { t } = useTranslation();
const [isReplying, setIsReplying] = useState(false);
const [replyContent, setReplyContent] = useState('');
const [isExpanded, setIsExpanded] = useState(false);
const [likeAnimation, setLikeAnimation] = useState(false);
const [dislikeAnimation, setDislikeAnimation] = useState(false);
const [replyCooldown, setReplyCooldown] = useState(0);
const [isLoadingReply, setIsLoadingReply] = useState(false);
const replyTextareaRef = useRef<HTMLTextAreaElement>(null);
// Gérer le cooldown des réponses
useEffect(() => {
if (replyCooldown <= 0) return;
const timer = setInterval(() => {
setReplyCooldown(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [replyCooldown]);
// Vérification du cooldown au chargement
useEffect(() => {
const lastReplyTime = localStorage.getItem('lastReplyTime');
if (lastReplyTime && !isAdmin) {
const timeElapsed = Math.floor((Date.now() - parseInt(lastReplyTime)) / 1000);
const remainingTime = REPLY_COOLDOWN_TIME - timeElapsed;
if (remainingTime > 0) {
setReplyCooldown(remainingTime);
}
}
}, [isAdmin]);
const startReplyCooldown = () => {
// Les admins n'ont pas de cooldown
if (isAdmin) return;
setReplyCooldown(REPLY_COOLDOWN_TIME);
localStorage.setItem('lastReplyTime', Date.now().toString());
};
const handleLike = async () => {
if (!currentUserId) return;
try {
// Animation effect
setLikeAnimation(true);
setTimeout(() => setLikeAnimation(false), 500);
await likeComment(comment.id, currentUserId);
refreshComments();
} catch (error) {
console.error('Error liking comment:', error);
}
};
const handleDislike = async () => {
if (!currentUserId) return;
try {
// Animation effect
setDislikeAnimation(true);
setTimeout(() => setDislikeAnimation(false), 500);
await dislikeComment(comment.id, currentUserId);
refreshComments();
} catch (error) {
console.error('Error disliking comment:', error);
}
};
const handleDelete = async () => {
if (window.confirm(t('comments.deleteConfirm'))) {
try {
await deleteComment(comment.id, currentUserId || undefined, isAdmin);
refreshComments();
} catch (error) {
console.error('Error deleting comment:', error);
}
}
};
const handleReply = async () => {
if (!currentUserId || !replyContent.trim() || (replyCooldown > 0 && !isAdmin) || isLoadingReply) return;
try {
setIsLoadingReply(true);
// Get user details from localStorage
let username = t('comments.defaultUser');
let userAvatar = 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
const isGoogleAuth = localStorage.getItem('google_auth') === 'true';
if (isDiscordAuth) {
const userInfoStr = localStorage.getItem('discord_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
username = userInfo.username;
userAvatar = userInfo.avatar;
}
} else if (isGoogleAuth) {
const userInfoStr = localStorage.getItem('google_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
username = userInfo.name;
userAvatar = userInfo.picture;
}
}
// Ajouter le commentaire
const replyId = await addComment(
contentId,
contentType,
currentUserId,
username,
userAvatar,
replyContent,
comment.id,
isAdmin // Transmettre le statut admin
);
// Démarrer le cooldown pour les réponses si pas admin
startReplyCooldown();
setReplyContent('');
setIsReplying(false);
// Mettre à jour les commentaires
refreshComments();
// Ajouter une notification
await addReplyNotification(
comment.id,
replyId,
replyContent,
currentUserId,
username
);
} catch (error) {
console.error('Error adding reply:', error);
} finally {
setIsLoadingReply(false);
}
};
const handleEmojiInsert = (emoji: string) => {
setReplyContent(prev => {
if (prev.length + emoji.length <= MAX_REPLY_LENGTH) {
return prev + emoji;
}
return prev;
});
};
const handleReaction = async (emoji: string) => {
if (!currentUserId) return;
try {
await reactWithEmoji(comment.id, emoji, currentUserId);
refreshComments();
} catch (error) {
console.error('Error reacting to comment:', error);
}
};
const formatDate = (date: Date) => {
try {
// Format différent pour les petits écrans (détecté via CSS media query)
const isMobile = window.innerWidth < 640;
if (isMobile) {
return format(date, 'dd/MM/yy HH:mm', { locale: fr });
}
return format(date, 'dd MMMM yyyy à HH:mm', { locale: fr });
} catch (error) {
return t('comments.unknownDate');
}
};
const formatCooldownTime = (seconds: number) => {
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
};
const isLikedByCurrentUser = currentUserId && comment.likedBy && comment.likedBy.includes(currentUserId);
const isDislikedByCurrentUser = currentUserId && comment.dislikedBy && comment.dislikedBy.includes(currentUserId);
// Un utilisateur peut supprimer son propre commentaire ou si c'est un admin
const canDeleteComment = (currentUserId && currentUserId === comment.userId) || isAdmin;
return (
<div className="bg-gray-800 p-4 rounded-lg mb-3">
<div className="flex items-start space-x-3">
<img
src={comment.userAvatar || 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp'}
alt={comment.username}
className="w-10 h-10 rounded-full object-cover shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between flex-wrap">
<div className="flex items-center">
<h3 className="font-medium text-white mr-2">{comment.username}</h3>
{comment.isAdmin && (
<span className="bg-red-600 text-white text-xs px-2 py-0.5 rounded-full mr-2 font-medium">
ADMIN
</span>
)}
</div>
<span className="text-xs text-gray-400">{formatDate(comment.createdAt)}</span>
</div>
<div className="mt-1 text-gray-300 break-words max-w-full overflow-hidden text-sm sm:text-base overflow-x-hidden prose-invert">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{comment.content}
</ReactMarkdown>
</div>
<ReactionBar
reactions={comment.reactions || []}
onReactionClick={handleReaction}
currentUserId={currentUserId}
/>
<div className="flex items-center mt-3 space-x-2 sm:space-x-4 flex-wrap">
<button
onClick={handleLike}
className={`flex items-center transition-all duration-300 ${
isLikedByCurrentUser
? 'text-blue-500'
: 'text-gray-400 hover:text-blue-500'
} ${likeAnimation ? 'animate-like' : ''} text-xs sm:text-sm`}
>
<ThumbsUp
size={16}
className={`mr-1 transform transition-transform duration-300 ${likeAnimation ? 'scale-150' : ''} ${isLikedByCurrentUser ? 'fill-current' : ''}`}
/>
<span className={`${likeAnimation ? 'animate-bounce' : ''}`}>{comment.likes}</span>
</button>
<button
onClick={handleDislike}
className={`flex items-center transition-all duration-300 ${
isDislikedByCurrentUser
? 'text-red-500'
: 'text-gray-400 hover:text-red-500'
} ${dislikeAnimation ? 'animate-dislike' : ''} text-xs sm:text-sm`}
>
<ThumbsDown
size={16}
className={`mr-1 transform transition-transform duration-300 ${dislikeAnimation ? 'scale-150' : ''} ${isDislikedByCurrentUser ? 'fill-current' : ''}`}
/>
<span className={`${dislikeAnimation ? 'animate-bounce' : ''}`}>{comment.dislikes || 0}</span>
</button>
{currentUserId && (
<button
onClick={() => setIsReplying(!isReplying)}
disabled={replyCooldown > 0}
className={`flex items-center text-gray-400 hover:text-blue-500 transition hover:scale-105 ${replyCooldown > 0 ? 'opacity-50 cursor-not-allowed' : ''} text-xs sm:text-sm`}
>
<Reply size={16} className="mr-1" />
<span>{replyCooldown > 0 ? `(${formatCooldownTime(replyCooldown)})` : t('comments.reply')}</span>
</button>
)}
{canDeleteComment && (
<button
onClick={handleDelete}
className="flex items-center text-gray-400 hover:text-red-500 transition hover:scale-105 text-xs sm:text-sm"
>
<Trash2 size={16} className="mr-1" />
<span>{t('common.delete')}</span>
</button>
)}
</div>
{isReplying && (
<div className="mt-3 ml-10">
<div className="relative bg-gray-700 rounded-lg">
<textarea
ref={replyTextareaRef}
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder={replyCooldown > 0 && !isAdmin ? t('comments.replyCooldownMessage', { time: formatCooldownTime(replyCooldown) }) : t('comments.writeReply')}
disabled={replyCooldown > 0 && !isAdmin}
className={`w-full bg-gray-700 text-white p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none min-h-[80px] ${replyCooldown > 0 && !isAdmin ? 'opacity-70 cursor-not-allowed' : ''}`}
maxLength={MAX_REPLY_LENGTH}
/>
<div className="absolute right-2 bottom-2 flex items-center space-x-2">
<EmojiPicker onEmojiSelect={handleEmojiInsert} />
</div>
</div>
<MarkdownToolbar
textareaRef={replyTextareaRef}
value={replyContent}
onChange={setReplyContent}
maxLength={MAX_REPLY_LENGTH}
/>
<div className="flex justify-between mt-2">
{replyCooldown > 0 && !isAdmin && (
<div className="flex items-center text-yellow-500 text-sm">
<Clock size={14} className="mr-1" />
<span>{t('comments.waitLabel')} {formatCooldownTime(replyCooldown)}</span>
</div>
)}
<div className="flex items-center space-x-2 ml-auto">
<span className={`text-xs ${replyContent.length >= MAX_REPLY_LENGTH ? 'text-red-500' : 'text-gray-400'}`}>
{replyContent.length}/{MAX_REPLY_LENGTH}
</span>
<button
onClick={handleReply}
disabled={replyCooldown > 0 && !isAdmin || !replyContent.trim() || isLoadingReply}
className={`bg-blue-600 text-white px-3 py-1.5 rounded-lg flex items-center space-x-1 text-sm ${
(replyCooldown > 0 && !isAdmin) || !replyContent.trim() || isLoadingReply ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-700'
}`}
>
<Send size={14} />
<span>{t('comments.reply')}</span>
</button>
<button
onClick={() => setIsReplying(false)}
className="bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm hover:bg-gray-700"
>
{t('common.cancel')}
</button>
</div>
</div>
</div>
)}
{comment.replies && comment.replies.length > 0 && (
<div className="mt-4">
{!isExpanded && (
<button
onClick={() => setIsExpanded(true)}
className="text-blue-500 text-sm hover:underline"
>
{t('comments.viewReplies', { count: comment.replies.length })}
</button>
)}
{isExpanded && (
<>
<button
onClick={() => setIsExpanded(false)}
className="text-blue-500 text-sm mb-2 hover:underline"
>
{t('comments.hideReplies')}
</button>
<div className="pl-4 border-l-2 border-gray-700">
{comment.replies.map(reply => {
const isReplyLikedByCurrentUser = currentUserId && reply.likedBy && reply.likedBy.includes(currentUserId);
const isReplyDislikedByCurrentUser = currentUserId && reply.dislikedBy && reply.dislikedBy.includes(currentUserId);
return (
<div key={reply.id} className="mt-3">
<div className="flex items-start space-x-3">
<img
src={reply.userAvatar || 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp'}
alt={reply.username}
className="w-8 h-8 rounded-full object-cover shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between flex-wrap">
<div className="flex items-center">
<h3 className="font-medium text-white text-sm mr-2">{reply.username}</h3>
{reply.isAdmin && (
<span className="bg-red-600 text-white text-xs px-1.5 py-0.5 rounded-full mr-2 font-medium text-[10px]">
ADMIN
</span>
)}
</div>
<span className="text-xs text-gray-400 inline-block">{formatDate(reply.createdAt)}</span>
</div>
<div className="mt-1 text-gray-300 break-words max-w-full overflow-hidden text-xs sm:text-sm overflow-x-hidden prose-invert">
<ReactMarkdown remarkPlugins={remarkPlugins} components={markdownComponents}>
{reply.content}
</ReactMarkdown>
</div>
<ReactionBar
reactions={reply.reactions || []}
onReactionClick={async (emoji) => {
if (!currentUserId) return;
await reactWithEmoji(reply.id, emoji, currentUserId);
refreshComments();
}}
currentUserId={currentUserId}
/>
<div className="flex items-center mt-2 space-x-2 sm:space-x-4 flex-wrap">
<button
onClick={async () => {
if (!currentUserId) return;
// Animation for replies
const button = document.getElementById(`like-${reply.id}`);
if (button) {
button.classList.add('scale-150');
setTimeout(() => button.classList.remove('scale-150'), 300);
}
await likeComment(reply.id, currentUserId);
refreshComments();
}}
className={`flex items-center transition-all duration-300 ${
isReplyLikedByCurrentUser
? 'text-blue-500'
: 'text-gray-400 hover:text-blue-500'
} transition text-xs`}
>
<ThumbsUp
id={`like-${reply.id}`}
size={14}
className={`mr-1 transform transition-transform duration-300 ${isReplyLikedByCurrentUser ? 'fill-current' : ''}`}
/>
<span>{reply.likes}</span>
</button>
<button
onClick={async () => {
if (!currentUserId) return;
// Animation for replies
const button = document.getElementById(`dislike-${reply.id}`);
if (button) {
button.classList.add('scale-150');
setTimeout(() => button.classList.remove('scale-150'), 300);
}
await dislikeComment(reply.id, currentUserId);
refreshComments();
}}
className={`flex items-center transition-all duration-300 ${
isReplyDislikedByCurrentUser
? 'text-red-500'
: 'text-gray-400 hover:text-red-500'
} transition text-xs`}
>
<ThumbsDown
id={`dislike-${reply.id}`}
size={14}
className={`mr-1 transform transition-transform duration-300 ${isReplyDislikedByCurrentUser ? 'fill-current' : ''}`}
/>
<span>{reply.dislikes || 0}</span>
</button>
{/* Bouton de suppression pour les réponses - visible si l'utilisateur est l'auteur de la réponse ou un admin */}
{(currentUserId && (currentUserId === reply.userId || isAdmin)) && (
<button
onClick={async () => {
if (window.confirm(t('comments.deleteReplyConfirm'))) {
await deleteComment(reply.id, currentUserId || undefined, isAdmin);
refreshComments();
}
}}
className="flex items-center text-gray-400 hover:text-red-500 transition hover:scale-105 text-xs"
>
<Trash2 size={14} className="mr-1" />
<span>{t('common.delete')}</span>
</button>
)}
</div>
</div>
</div>
</div>
);
})}
</div>
</>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default CommentItem;

View file

@ -1,423 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { getComments, addComment } from '../services/commentService';
import { Comment, COMMENT_LENGTH_LIMITS } from '../types/Comment';
import CommentItem from './CommentItem';
import { Send, Clock } from 'lucide-react';
import EmojiPicker from './EmojiPicker';
import AdminLogin from './AdminLogin';
import { isUserVip } from '../utils/authUtils';
interface CommentSectionProps {
contentId: string;
contentType: 'movie' | 'series';
}
const MAX_COMMENT_LENGTH = COMMENT_LENGTH_LIMITS.COMMENT;
const COOLDOWN_TIME = 30; // Cooldown en secondes
const CommentSection: React.FC<CommentSectionProps> = ({ contentId, contentType }) => {
const { t } = useTranslation();
const [comments, setComments] = useState<Comment[]>([]);
const [newComment, setNewComment] = useState('');
const [loading, setLoading] = useState(true);
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const [cooldownRemaining, setCooldownRemaining] = useState(0);
const [isLoadingAddComment, setIsLoadingAddComment] = useState(false);
// Fonction pour vérifier l'authentification - memoized pour éviter les recalculs inutiles
const checkAuthStatus = useCallback(() => {
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
const isGoogleAuth = localStorage.getItem('google_auth') === 'true';
const isVipUser = isUserVip();
const isBip39Auth = localStorage.getItem('bip39_auth') === 'true';
// VIP via access_code
let isVipAuth = false;
let vipUser = null;
// BIP39 authentication
let isBip39User = false;
let bip39User = null;
const authStr = localStorage.getItem('auth');
if (authStr) {
try {
const authObj = JSON.parse(authStr);
if (authObj && authObj.userProfile) {
if (authObj.userProfile.provider === 'access_code') {
isVipAuth = true;
vipUser = authObj.userProfile;
} else if (authObj.userProfile.provider === 'bip39') {
isBip39User = true;
bip39User = authObj.userProfile;
}
}
} catch (error) {
console.error('Error parsing auth data:', error);
}
}
setIsAuthenticated(isDiscordAuth || isGoogleAuth || isVipAuth || isVipUser || isBip39Auth || isBip39User);
// Prioritize Discord, then Google, then BIP39, then VIP for user identity
if (isDiscordAuth) {
try {
const userInfoStr = localStorage.getItem('discord_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
setCurrentUserId(userInfo?.id || 'discord_user');
}
} catch (error) {
console.error('Error parsing Discord user data:', error);
}
} else if (isGoogleAuth) {
try {
const userInfoStr = localStorage.getItem('google_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
setCurrentUserId(userInfo?.id || 'google_user');
}
} catch (error) {
console.error('Error parsing Google user data:', error);
}
} else if (isBip39Auth || (isBip39User && bip39User)) {
setCurrentUserId(bip39User?.id || 'bip39_user');
} else if (isVipAuth && vipUser) {
setCurrentUserId(vipUser.id || 'vip_user');
} else if (isVipUser) {
// Fallback for VIP without specific auth
const guestId = localStorage.getItem('guest_uuid') || 'anonymous_vip';
setCurrentUserId(guestId);
}
}, []);
// Chargement initial des commentaires
useEffect(() => {
const fetchInitialData = async () => {
setLoading(true);
try {
const fetchedComments = await getComments(contentId, contentType);
setComments(fetchedComments);
} catch (error) {
console.error('Error loading comments:', error);
} finally {
setLoading(false);
}
};
fetchInitialData();
checkAuthStatus();
}, [contentId, contentType, checkAuthStatus]);
// Gérer le cooldown
useEffect(() => {
if (cooldownRemaining <= 0) return;
const timer = setInterval(() => {
setCooldownRemaining(prev => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [cooldownRemaining]);
// Charge les commentaires de manière optimisée
const refreshComments = useCallback(async () => {
try {
const fetchedComments = await getComments(contentId, contentType);
setComments(fetchedComments);
} catch (error) {
console.error('Error refreshing comments:', error);
}
}, [contentId, contentType]);
const startCooldown = () => {
// Les admins n'ont pas de cooldown
if (isAdmin) return;
setCooldownRemaining(COOLDOWN_TIME);
localStorage.setItem('lastCommentTime', Date.now().toString());
};
// Vérification du cooldown au chargement
useEffect(() => {
const lastCommentTime = localStorage.getItem('lastCommentTime');
if (lastCommentTime && !isAdmin) {
const timeElapsed = Math.floor((Date.now() - parseInt(lastCommentTime)) / 1000);
const remainingTime = COOLDOWN_TIME - timeElapsed;
if (remainingTime > 0) {
setCooldownRemaining(remainingTime);
}
}
}, [isAdmin]);
const handleAdminStatusChange = (status: boolean) => {
setIsAdmin(status);
// Si l'utilisateur est devenu admin, on annule le cooldown
if (status) {
setCooldownRemaining(0);
}
};
const handleAddComment = async () => {
if (!isAuthenticated || !newComment.trim() || (cooldownRemaining > 0 && !isAdmin) || isLoadingAddComment) return;
try {
setIsLoadingAddComment(true);
// Get user details from localStorage
let username = t('comments.defaultUser');
let userAvatar = 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
let userId = currentUserId || 'anonymous_user';
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
const isGoogleAuth = localStorage.getItem('google_auth') === 'true';
const isVipUser = isUserVip();
const isBip39Auth = localStorage.getItem('bip39_auth') === 'true';
// VIP via access_code and BIP39 authentication
let isVipAuth = false;
let vipUser = null;
let isBip39User = false;
let bip39User = null;
const authStr = localStorage.getItem('auth');
if (authStr) {
try {
const authObj = JSON.parse(authStr);
if (authObj && authObj.userProfile) {
if (authObj.userProfile.provider === 'access_code') {
isVipAuth = true;
vipUser = authObj.userProfile;
} else if (authObj.userProfile.provider === 'bip39') {
isBip39User = true;
bip39User = authObj.userProfile;
}
}
} catch (error) {
console.error('Error parsing auth data:', error);
}
}
if (isDiscordAuth) {
try {
const userInfoStr = localStorage.getItem('discord_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
if (userInfo) {
username = userInfo.username || 'Discord User';
userAvatar = (typeof userInfo.avatar === 'string' && userInfo.avatar.trim() !== '') ? userInfo.avatar : userAvatar;
userId = userInfo.id || userId;
}
}
} catch (error) {
console.error('Error parsing Discord user data:', error);
}
} else if (isGoogleAuth) {
try {
const userInfoStr = localStorage.getItem('google_user');
if (userInfoStr) {
const userInfo = JSON.parse(userInfoStr);
if (userInfo) {
username = userInfo.name || 'Google User';
userAvatar = (typeof userInfo.picture === 'string' && userInfo.picture.trim() !== '') ? userInfo.picture : userAvatar;
userId = userInfo.id || userId;
}
}
} catch (error) {
console.error('Error parsing Google user data:', error);
}
} else if (isBip39Auth || (isBip39User && bip39User)) {
username = bip39User?.username || t('comments.defaultUserBip39');
userAvatar = (typeof bip39User?.avatar === 'string' && bip39User.avatar.trim() !== '') ? bip39User.avatar : userAvatar;
userId = bip39User?.id || 'bip39_user';
} else if (isVipAuth && vipUser) {
username = vipUser.username || 'VIP User';
userAvatar = (typeof vipUser.avatar === 'string' && vipUser.avatar.trim() !== '') ? vipUser.avatar : userAvatar;
userId = vipUser.id || 'vip_user';
} else if (isVipUser) {
username = 'VIP User';
userId = localStorage.getItem('guest_uuid') || 'anonymous_vip';
}
// Ajouter le commentaire
const commentId = await addComment(
contentId,
contentType,
userId,
username,
userAvatar,
newComment,
undefined, // parentId
isAdmin // Indiquer si l'utilisateur est admin
);
// Démarrer le cooldown si pas admin
startCooldown();
// Ajouter optimistiquement le commentaire au state sans recharger
const newCommentObject: Comment = {
id: commentId,
contentId,
contentType,
userId,
username,
userAvatar,
content: newComment,
createdAt: new Date(),
likes: 0,
likedBy: [],
dislikes: 0,
dislikedBy: [],
reactions: [],
replies: [],
isAdmin: isAdmin
};
setComments(prev => [newCommentObject, ...prev]);
setNewComment('');
} catch (error) {
console.error('Error adding comment:', error);
// Si erreur, on recharge tous les commentaires
refreshComments();
} finally {
setIsLoadingAddComment(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleAddComment();
}
};
const handleEmojiInsert = (emoji: string) => {
setNewComment(prev => {
if (prev.length + emoji.length <= MAX_COMMENT_LENGTH) {
return prev + emoji;
}
return prev;
});
};
// Formatter le temps restant
const formatCooldownTime = (seconds: number) => {
if (seconds < 60) {
return `${seconds} ${t('time.seconds', { count: seconds })}`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
};
return (
<div className="bg-gray-900 p-4 rounded-lg mt-8">
<div className="flex flex-wrap justify-between items-center mb-4">
<h2 className="text-xl font-bold mb-2 sm:mb-0">{t('comments.title')}</h2>
<AdminLogin onAdminStatusChange={handleAdminStatusChange} />
</div>
{isAuthenticated ? (
<div className="mb-6 flex items-start space-x-3">
<img
src={(() => {
if (localStorage.getItem('discord_auth') === 'true') {
const userInfo = JSON.parse(localStorage.getItem('discord_user') || '{}');
return userInfo.avatar || 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
} else if (localStorage.getItem('google_auth') === 'true') {
const userInfo = JSON.parse(localStorage.getItem('google_user') || '{}');
return userInfo.picture || 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
} else {
// VIP via access_code
const authStr = localStorage.getItem('auth');
if (authStr) {
try {
const authObj = JSON.parse(authStr);
if (authObj.userProfile && authObj.userProfile.provider === 'access_code') {
return authObj.userProfile.avatar || 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
}
} catch {}
}
}
return 'https://as2.ftcdn.net/v2/jpg/05/89/93/27/1000_F_589932782_vQAEAZhHnq1QCGu5ikwrYaQD0Mmurm0N.webp';
})()}
alt={t('common.avatar')}
className="w-10 h-10 rounded-full object-cover"
/>
<div className="flex-1">
<div className="relative flex bg-gray-800 rounded-lg">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={cooldownRemaining > 0 && !isAdmin ? t('comments.cooldownMessage', { time: formatCooldownTime(cooldownRemaining) }) : t('comments.addComment')}
disabled={cooldownRemaining > 0 && !isAdmin}
className={`w-full bg-gray-800 text-white p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none min-h-[80px] ${cooldownRemaining > 0 && !isAdmin ? 'opacity-70 cursor-not-allowed' : ''}`}
maxLength={MAX_COMMENT_LENGTH}
/>
<div className="absolute right-2 bottom-2 flex items-center space-x-2">
<EmojiPicker onEmojiSelect={handleEmojiInsert} />
</div>
</div>
<div className="flex justify-between mt-2">
{cooldownRemaining > 0 && !isAdmin && (
<div className="flex items-center text-yellow-500 text-sm">
<Clock size={16} className="mr-1" />
<span>{t('comments.waitLabel')} {formatCooldownTime(cooldownRemaining)}</span>
</div>
)}
<div className="flex items-center space-x-2 ml-auto">
<span className={`text-xs ${newComment.length >= MAX_COMMENT_LENGTH ? 'text-red-500' : 'text-gray-400'}`}>
{newComment.length}/{MAX_COMMENT_LENGTH}
</span>
<button
onClick={handleAddComment}
disabled={cooldownRemaining > 0 && !isAdmin || !newComment.trim() || isLoadingAddComment}
className={`bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center space-x-1 ${
(cooldownRemaining > 0 && !isAdmin) || !newComment.trim() || isLoadingAddComment ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-700'
}`}
>
<Send size={16} />
<span>{t('comments.comment')}</span>
</button>
</div>
</div>
</div>
</div>
) : (
<div className="mb-6 p-4 bg-gray-800 rounded-lg text-center">
<p className="text-gray-300">{t('comments.loginToComment')}</p>
</div>
)}
{loading ? (
<div className="flex justify-center items-center py-6">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
) : comments.length > 0 ? (
<div>
{comments.map(comment => (
<CommentItem
key={comment.id}
comment={comment}
currentUserId={currentUserId}
contentId={contentId}
contentType={contentType}
refreshComments={refreshComments}
isAdmin={isAdmin}
/>
))}
</div>
) : (
<div className="text-center py-6 text-gray-400">
<p>{t('comments.noComments')}</p>
</div>
)}
</div>
);
};
export default CommentSection;

View file

@ -1,106 +0,0 @@
import React, { useState, useRef } from 'react';
import { Plus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Reaction } from '../types/Comment';
import emojiData from '@emoji-mart/data/sets/14/apple.json';
import Picker from '@emoji-mart/react';
interface ReactionBarProps {
reactions: Reaction[];
onReactionClick: (emoji: string) => void;
currentUserId: string | null;
}
const ReactionBar: React.FC<ReactionBarProps> = ({
reactions,
onReactionClick,
currentUserId
}) => {
const { t } = useTranslation();
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const emojiPickerRef = useRef<HTMLDivElement>(null);
// N'affiche que les réactions qui ont au moins 1 utilisateur
const validReactions = reactions.filter(reaction => reaction.count > 0);
if (!currentUserId && validReactions.length === 0) {
return null;
}
const handleEmojiSelect = (emoji: any) => {
onReactionClick(emoji.native);
setShowEmojiPicker(false);
};
// Configuration pour emoji-mart
const pickerConfig = {
data: emojiData,
onEmojiSelect: handleEmojiSelect,
theme: 'dark',
set: 'apple',
previewPosition: 'none',
skinTonePosition: 'none',
maxFrequentRows: 1,
navPosition: 'bottom',
perLine: 6,
emojiSize: 20,
emojiButtonSize: 28,
locale: 'fr',
categories: ['frequent', 'people', 'nature', 'foods', 'activity', 'places', 'objects', 'symbols', 'flags'],
i18n: {
search: t('emojiPicker.search'),
categories: {
frequent: t('emojiPicker.frequent'),
people: t('emojiPicker.people'),
nature: t('emojiPicker.nature'),
foods: t('emojiPicker.foods'),
activity: t('emojiPicker.activity'),
places: t('emojiPicker.places'),
objects: t('emojiPicker.objects'),
symbols: t('emojiPicker.symbols'),
flags: t('emojiPicker.flags')
}
}
};
return (
<div className="flex flex-wrap gap-1.5 mt-2">
{validReactions.map((reaction, index) => {
const hasReacted = currentUserId && reaction.users.includes(currentUserId);
return (
<button
key={`${reaction.emoji}-${index}`}
onClick={() => onReactionClick(reaction.emoji)}
className={`flex items-center space-x-1.5 px-2 py-1 rounded-full text-sm transition-colors
${hasReacted
? 'bg-blue-600/30 border border-blue-500 text-white transform hover:scale-105'
: 'bg-gray-700/50 hover:bg-gray-700 border border-gray-600 text-gray-300 hover:scale-105'}`}
>
<span className="text-base">{reaction.emoji}</span>
<span className="text-xs">{reaction.count}</span>
</button>
);
})}
{currentUserId && (
<div className="relative" ref={emojiPickerRef}>
<button
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className="flex items-center space-x-1.5 px-2 py-1 rounded-full text-sm bg-gray-700/50 hover:bg-gray-700 border border-gray-600 text-gray-300 hover:scale-105 transition-transform"
>
<Plus size={14} />
</button>
{showEmojiPicker && (
<div className="absolute z-50 bottom-full mb-2 right-0">
<Picker {...pickerConfig} />
</div>
)}
</div>
)}
</div>
);
};
export default ReactionBar;

View file

@ -1,42 +0,0 @@
import { initializeApp } from '@firebase/app';
import {
getFirestore,
collection,
doc,
getDoc,
setDoc,
updateDoc
} from '@firebase/firestore';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const firebaseUtils = {
collection,
doc,
getDoc,
setDoc,
updateDoc
};
interface CustomMovieLink {
customStreamingUrls: string[];
dateAdded: Date;
}
interface CustomTVEpisodeLink {
customStreamingUrls: string[];
dateAdded: Date;
episodeNumber: number;
seasonNumber: number;
}

View file

@ -1,191 +0,0 @@
import { db } from '../config/firebase';
import { doc, getDoc } from '@firebase/firestore';
import { logAdmin, logError } from './logService';
// Local storage key for admin credentials
const ADMIN_CODE_KEY = 'admin_code';
// Helper to get Discord user information
const getDiscordUserInfo = () => {
try {
const isDiscordAuth = localStorage.getItem('discord_auth') === 'true';
if (isDiscordAuth) {
const discordUser = JSON.parse(localStorage.getItem('discord_user') || '{}');
return {
id: discordUser.id,
username: discordUser.username,
roles: discordUser.roles || [],
isAdmin: discordUser.isAdmin || false
};
}
} catch (error) {
console.error('Error getting Discord user info:', error);
}
return null;
};
// Check if admin code exists directly as a document ID in the users collection
export const verifyAdminCode = async (code: string): Promise<boolean> => {
try {
console.log(`Tentative de vérification du code: ${code}`);
// Get the document with the ID that matches the provided code
const userDocRef = doc(db, 'users', code);
const userSnapshot = await getDoc(userDocRef);
console.log(`Document existe: ${userSnapshot.exists()}`);
// If document exists and has role='admin'
if (userSnapshot.exists()) {
const userData = userSnapshot.data();
console.log(`Données utilisateur:`, userData);
// Check if the user is an admin
if (userData.role === 'admin') {
console.log('Utilisateur est admin, authentification réussie');
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log l'événement de connexion avec informations Discord si disponibles
const logDetails = {
method: 'code',
timestamp: new Date().toISOString(),
discordInfo: discordInfo || 'Non connecté via Discord'
};
await logAdmin('Connexion administrateur réussie',
logDetails,
code,
userData.nom || 'Admin'
);
// Store the admin code in localStorage
localStorage.setItem(ADMIN_CODE_KEY, code);
return true;
} else {
console.log(`Rôle trouvé: ${userData.role}, mais ce n'est pas 'admin'`);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log la tentative échouée
await logError('Tentative de connexion admin avec rôle invalide', {
code,
role: userData.role,
expectedRole: 'admin',
discordInfo: discordInfo || 'Non connecté via Discord'
});
}
} else {
console.log(`Aucun document trouvé avec l'ID: ${code}`);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log la tentative échouée
await logError('Tentative de connexion admin avec code invalide', {
code,
discordInfo: discordInfo || 'Non connecté via Discord'
});
}
return false;
} catch (error) {
console.error('Error verifying admin code:', error);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log l'erreur
await logError('Erreur lors de la vérification du code admin',
{ error, discordInfo: discordInfo || 'Non connecté via Discord' },
code
);
return false;
}
};
// Check if the user is already authenticated as admin
export const isAdminAuthenticated = async (): Promise<boolean> => {
try {
const storedCode = localStorage.getItem(ADMIN_CODE_KEY);
console.log(`Code stocké: ${storedCode}`);
// If no code stored, user is not admin
if (!storedCode) {
console.log('Aucun code admin stocké');
return false;
}
const adminChecked = sessionStorage.getItem('admin_checked');
if (adminChecked) {
// Si la vérification a déjà été effectuée cette session, utiliser le résultat stocké
return localStorage.getItem(ADMIN_CODE_KEY) !== null;
} else {
// Première vérification de la session, vérifier auprès de Firebase
const isValid = await verifyAdminCode(storedCode);
// Marquer que la vérification a été effectuée pour cette session
sessionStorage.setItem('admin_checked', 'true');
return isValid;
}
} catch (error) {
console.error('Error checking admin authentication:', error);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log l'erreur
await logError('Erreur lors de la vérification du statut admin',
{ error, discordInfo: discordInfo || 'Non connecté via Discord' }
);
return false;
}
};
// Remove admin authentication
export const logoutAdmin = async (): Promise<void> => {
try {
console.log('Déconnexion admin');
const adminCode = localStorage.getItem(ADMIN_CODE_KEY);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
if (adminCode) {
// Récupérer les infos admin si possible
try {
const userDocRef = doc(db, 'users', adminCode);
const userSnapshot = await getDoc(userDocRef);
if (userSnapshot.exists()) {
const userData = userSnapshot.data();
// Log l'événement de déconnexion
await logAdmin('Déconnexion administrateur', {
timestamp: new Date().toISOString(),
discordInfo: discordInfo || 'Non connecté via Discord'
}, adminCode, userData.nom || 'Admin');
}
} catch (error) {
console.error('Erreur lors de la récupération des infos admin pour le log de déconnexion:', error);
}
}
// Supprimer du localStorage
localStorage.removeItem(ADMIN_CODE_KEY);
} catch (error) {
console.error('Erreur lors de la déconnexion admin:', error);
// Récupérer les informations Discord si disponibles
const discordInfo = getDiscordUserInfo();
// Log l'erreur
await logError('Erreur lors de la déconnexion admin',
{ error, discordInfo: discordInfo || 'Non connecté via Discord' }
);
}
};

View file

@ -1,396 +0,0 @@
import { db } from '../config/firebase';
import {
collection, addDoc, getDocs, query, where, orderBy,
doc, updateDoc, deleteDoc, increment, arrayUnion, arrayRemove,
serverTimestamp
} from '@firebase/firestore';
import { Comment, Reaction, COMMENT_LENGTH_LIMITS } from '../types/Comment';
import { logComment, logError } from './logService';
// Limites de caractères
export const MAX_COMMENT_LENGTH = COMMENT_LENGTH_LIMITS.COMMENT;
export const MAX_REPLY_LENGTH = COMMENT_LENGTH_LIMITS.REPLY;
// Get all comments for a specific content (movie or series)
export const getComments = async (contentId: string, contentType: 'movie' | 'series'): Promise<Comment[]> => {
try {
// Get only root comments (not replies)
const commentsQuery = query(
collection(db, 'comments'),
where('contentId', '==', contentId),
where('contentType', '==', contentType),
where('parentId', '==', null),
orderBy('createdAt', 'desc')
);
const snapshot = await getDocs(commentsQuery);
const comments: Comment[] = [];
for (const docSnapshot of snapshot.docs) {
const data = docSnapshot.data();
// Get replies for this comment
const repliesQuery = query(
collection(db, 'comments'),
where('parentId', '==', docSnapshot.id),
orderBy('createdAt', 'asc')
);
const repliesSnapshot = await getDocs(repliesQuery);
const replies: Comment[] = repliesSnapshot.docs.map(replyDoc => ({
id: replyDoc.id,
...replyDoc.data(),
createdAt: replyDoc.data().createdAt.toDate(),
likedBy: replyDoc.data().likedBy || [],
dislikedBy: replyDoc.data().dislikedBy || [],
dislikes: replyDoc.data().dislikes || 0,
reactions: replyDoc.data().reactions || [],
} as Comment));
comments.push({
id: docSnapshot.id,
...data,
createdAt: data.createdAt.toDate(),
likedBy: data.likedBy || [],
dislikedBy: data.dislikedBy || [],
dislikes: data.dislikes || 0,
reactions: data.reactions || [],
replies
} as Comment);
}
return comments;
} catch (error) {
console.error('Error getting comments:', error);
// Log l'erreur
await logError('Erreur lors de la récupération des commentaires', error);
throw error;
}
};
// Add a new comment
export const addComment = async (
contentId: string,
contentType: 'movie' | 'series',
userId: string,
username: string,
userAvatar: string,
content: string,
parentId?: string,
isAdmin?: boolean
): Promise<string> => {
try {
// Appliquer la limite de caractères
const maxLength = parentId ? MAX_REPLY_LENGTH : MAX_COMMENT_LENGTH;
const trimmedContent = content.slice(0, maxLength);
const commentData = {
contentId,
contentType,
userId,
username,
userAvatar,
content: trimmedContent,
createdAt: serverTimestamp(),
likes: 0,
likedBy: [],
dislikes: 0,
dislikedBy: [],
reactions: [],
parentId: parentId || null,
isAdmin: isAdmin || false
};
const docRef = await addDoc(collection(db, 'comments'), commentData);
// Log l'ajout de commentaire
await logComment(parentId ? 'Réponse ajoutée' : 'Commentaire ajouté', {
commentId: docRef.id,
contentId,
contentType,
isReply: !!parentId,
parentId,
isAdmin: isAdmin || false,
comment: trimmedContent.substring(0, 100) + (trimmedContent.length > 100 ? '...' : '')
}, userId, username);
return docRef.id;
} catch (error) {
console.error('Error adding comment:', error);
// Log l'erreur
await logError('Erreur lors de l\'ajout d\'un commentaire', error, userId, username);
throw error;
}
};
// Delete a comment
export const deleteComment = async (commentId: string, userId?: string, isAdmin?: boolean): Promise<void> => {
try {
// Get comment data first for logging
const commentQuery = query(collection(db, 'comments'), where('__name__', '==', commentId));
const commentSnapshot = await getDocs(commentQuery);
if (commentSnapshot.empty) {
throw new Error('Comment not found');
}
const commentData = commentSnapshot.docs[0].data();
const commentRef = doc(db, 'comments', commentId);
// Delete the comment
await deleteDoc(commentRef);
// Log la suppression avec un message spécifique pour les admins
const logMessage = isAdmin
? '🛑 Commentaire supprimé par ADMINISTRATEUR'
: 'Commentaire supprimé par utilisateur';
await logComment(logMessage, {
commentId,
contentId: commentData.contentId,
contentType: commentData.contentType,
deletedBy: userId || 'unknown',
deletedByAdmin: !!isAdmin,
comment: commentData.content.substring(0, 100) + (commentData.content.length > 100 ? '...' : '')
}, userId || commentData.userId, commentData.username);
} catch (error) {
console.error('Error deleting comment:', error);
// Log l'erreur
await logError('Erreur lors de la suppression d\'un commentaire', error, userId);
throw error;
}
};
// Like a comment
export const likeComment = async (commentId: string, userId: string): Promise<void> => {
try {
const commentRef = doc(db, 'comments', commentId);
const commentDoc = await getDocs(query(collection(db, 'comments'), where('__name__', '==', commentId)));
if (commentDoc.empty) {
throw new Error('Comment not found');
}
const commentData = commentDoc.docs[0].data();
const likedBy = commentData.likedBy || [];
const dislikedBy = commentData.dislikedBy || [];
const isRemovingLike = likedBy.includes(userId);
if (isRemovingLike) {
// User already liked, so remove like
await updateDoc(commentRef, {
likes: increment(-1),
likedBy: arrayRemove(userId)
});
// Log la suppression du like
await logComment('Like retiré d\'un commentaire', {
commentId,
action: 'remove',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
} else {
// Add like
await updateDoc(commentRef, {
likes: increment(1),
likedBy: arrayUnion(userId)
});
// Log l'ajout du like
await logComment('Like ajouté à un commentaire', {
commentId,
action: 'add',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
// If user previously disliked, remove the dislike
if (dislikedBy.includes(userId)) {
await updateDoc(commentRef, {
dislikes: increment(-1),
dislikedBy: arrayRemove(userId)
});
// Log la suppression du dislike
await logComment('Dislike retiré d\'un commentaire', {
commentId,
action: 'remove',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
}
}
} catch (error) {
console.error('Error liking comment:', error);
// Log l'erreur
await logError('Erreur lors du like d\'un commentaire', error, userId);
throw error;
}
};
// Dislike a comment
export const dislikeComment = async (commentId: string, userId: string): Promise<void> => {
try {
const commentRef = doc(db, 'comments', commentId);
const commentDoc = await getDocs(query(collection(db, 'comments'), where('__name__', '==', commentId)));
if (commentDoc.empty) {
throw new Error('Comment not found');
}
const commentData = commentDoc.docs[0].data();
const likedBy = commentData.likedBy || [];
const dislikedBy = commentData.dislikedBy || [];
const isRemovingDislike = dislikedBy.includes(userId);
if (isRemovingDislike) {
// User already disliked, so remove dislike
await updateDoc(commentRef, {
dislikes: increment(-1),
dislikedBy: arrayRemove(userId)
});
// Log la suppression du dislike
await logComment('Dislike retiré d\'un commentaire', {
commentId,
action: 'remove',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
} else {
// Add dislike
await updateDoc(commentRef, {
dislikes: increment(1),
dislikedBy: arrayUnion(userId)
});
// Log l'ajout du dislike
await logComment('Dislike ajouté à un commentaire', {
commentId,
action: 'add',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
// If user previously liked, remove the like
if (likedBy.includes(userId)) {
await updateDoc(commentRef, {
likes: increment(-1),
likedBy: arrayRemove(userId)
});
// Log la suppression du like
await logComment('Like retiré d\'un commentaire', {
commentId,
action: 'remove',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
}
}
} catch (error) {
console.error('Error disliking comment:', error);
// Log l'erreur
await logError('Erreur lors du dislike d\'un commentaire', error, userId);
throw error;
}
};
// Add or remove emoji reaction
export const reactWithEmoji = async (commentId: string, emoji: string, userId: string): Promise<void> => {
try {
const commentRef = doc(db, 'comments', commentId);
const commentDoc = await getDocs(query(collection(db, 'comments'), where('__name__', '==', commentId)));
if (commentDoc.empty) {
throw new Error('Comment not found');
}
const commentData = commentDoc.docs[0].data();
const reactions = commentData.reactions || [];
// Check if this emoji reaction already exists
const existingReactionIndex = reactions.findIndex((r: Reaction) => r.emoji === emoji);
if (existingReactionIndex !== -1) {
const reaction = reactions[existingReactionIndex];
if (reaction.users.includes(userId)) {
// User already reacted with this emoji, so remove the reaction
const updatedReactions = [...reactions];
updatedReactions[existingReactionIndex] = {
...reaction,
count: reaction.count - 1,
users: reaction.users.filter((id: string) => id !== userId)
};
// If count is 0, remove this reaction entirely
const filteredReactions = updatedReactions.filter((r: Reaction) => r.count > 0);
await updateDoc(commentRef, {
reactions: filteredReactions
});
// Log la suppression de la réaction
await logComment(`Réaction emoji retirée: ${emoji}`, {
emoji,
commentId,
action: 'remove',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
} else {
// User hasn't reacted with this emoji yet, so add their reaction
const updatedReactions = [...reactions];
updatedReactions[existingReactionIndex] = {
...reaction,
count: reaction.count + 1,
users: [...reaction.users, userId]
};
await updateDoc(commentRef, {
reactions: updatedReactions
});
// Log l'ajout de la réaction
await logComment(`Réaction emoji ajoutée: ${emoji}`, {
emoji,
commentId,
action: 'add',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
}
} else {
// This emoji reaction doesn't exist yet, create it
const newReaction: Reaction = {
emoji,
count: 1,
users: [userId]
};
await updateDoc(commentRef, {
reactions: [...reactions, newReaction]
});
// Log l'ajout de la réaction
await logComment(`Réaction emoji ajoutée: ${emoji}`, {
emoji,
commentId,
action: 'add',
commentPreview: commentData.content.substring(0, 100)
}, userId, commentData.username);
}
} catch (error) {
console.error('Error reacting to comment:', error);
// Log l'erreur
await logError('Erreur lors de l\'ajout d\'une réaction emoji', error, userId);
throw error;
}
};

View file

@ -1,119 +0,0 @@
// Discord webhook URL
const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/1356950660838588456/8TVg6FQBu1aE4-YhJXzyLCgH1c1Ehby1JFYVICqpX_53QZ9a8gvxuKejLdwTf9qlUqpW';
// Types d'événements simplifiés
export enum LogType {
ADMIN = 'ADMIN',
USER = 'USER',
COMMENT = 'COMMENT',
ERROR = 'ERROR'
}
/**
* Fonction simplifiée pour envoyer un log à Discord
*/
export const log = async (
type: LogType,
message: string,
details?: any,
userId?: string,
username?: string
): Promise<void> => {
// Log dans la console
const timestamp = new Date().toISOString();
const prefix = `[${type}]`;
const userInfo = userId ? `[${username || userId}]` : '';
// Couleurs pour la console
let style = 'color: black';
switch (type) {
case LogType.ADMIN: style = 'color: purple; font-weight: bold'; break;
case LogType.USER: style = 'color: blue'; break;
case LogType.COMMENT: style = 'color: green'; break;
case LogType.ERROR: style = 'color: red; font-weight: bold'; break;
}
// Affichage dans la console
console.log(`%c${timestamp} ${prefix} ${userInfo} ${message}`, style);
if (details) console.log('Details:', details);
// Envoyer à Discord
try {
// Couleurs pour Discord
const colors = {
[LogType.ADMIN]: 0x9B59B6, // Violet
[LogType.USER]: 0x3498DB, // Bleu
[LogType.COMMENT]: 0x2ECC71, // Vert
[LogType.ERROR]: 0xFF0000, // Rouge
};
// Emojis pour les types
const emojis = {
[LogType.ADMIN]: '🔑',
[LogType.USER]: '👤',
[LogType.COMMENT]: '💬',
[LogType.ERROR]: '❌',
};
// Formatage simplifié des détails
let detailsText = '';
if (details) {
try {
detailsText = '```json\n' +
JSON.stringify(details, null, 2).substring(0, 800) +
(JSON.stringify(details).length > 800 ? '\n...' : '') +
'\n```';
} catch (e) {
detailsText = '```Erreur de formatage```';
}
}
// Payload Discord simplifié
const payload = {
embeds: [{
title: `${emojis[type]} ${message}`,
color: colors[type],
description: detailsText,
fields: [
userId ? {
name: 'Utilisateur',
value: username ? `${username} (${userId})` : userId,
inline: true
} : null,
{
name: 'Date',
value: new Date().toLocaleString('fr-FR'),
inline: true
}
].filter(Boolean),
footer: { text: 'Movix' },
timestamp: new Date().toISOString()
}]
};
// Envoi à Discord
await fetch(DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (error) {
console.error('Erreur d\'envoi au webhook:', error);
}
};
// Fonctions d'aide simplifiées
export const logAdmin = (message: string, details?: any, userId?: string, username?: string): Promise<void> =>
log(LogType.ADMIN, message, details, userId, username);
export const logUser = (message: string, details?: any, userId?: string, username?: string): Promise<void> =>
log(LogType.USER, message, details, userId, username);
export const logComment = (message: string, details?: any, userId?: string, username?: string): Promise<void> =>
log(LogType.COMMENT, message, details, userId, username);
export const logError = (message: string, error?: any, userId?: string, username?: string): Promise<void> =>
log(LogType.ERROR, message, error ? {
message: error.message,
stack: error.stack
} : undefined, userId, username);

View file

@ -1,260 +0,0 @@
import { db } from '../config/firebase';
import {
doc,
updateDoc,
arrayUnion,
getDoc,
query,
collection,
where,
getDocs
} from '@firebase/firestore';
import { Notification } from '../types/Comment';
import { v4 as uuidv4 } from 'uuid';
/**
* Ajoute une notification pour l'auteur d'un commentaire lorsqu'une réponse est ajoutée
*/
export const addReplyNotification = async (
parentCommentId: string,
replyId: string,
replyContent: string,
fromUserId: string,
fromUsername: string
): Promise<boolean> => {
try {
// Récupérer les données du commentaire parent
const commentRef = doc(db, 'comments', parentCommentId);
const commentSnapshot = await getDoc(commentRef);
if (!commentSnapshot.exists()) {
console.error('Parent comment not found');
return false;
}
const commentData = commentSnapshot.data();
// Ne pas envoyer de notification si l'utilisateur répond à son propre commentaire
if (commentData.userId === fromUserId) {
return false;
}
// Créer la notification
const notification: Notification = {
id: uuidv4(),
type: 'reply',
commentId: parentCommentId,
replyId: replyId,
content: replyContent.substring(0, 100) + (replyContent.length > 100 ? '...' : ''),
fromUserId,
fromUsername,
createdAt: new Date(),
read: false
};
// Ajouter la notification au commentaire
await updateDoc(commentRef, {
notifications: arrayUnion(notification)
});
return true;
} catch (error) {
console.error('Error adding notification:', error);
return false;
}
};
/**
* Récupère toutes les notifications non lues pour un utilisateur
*/
export const getUserNotifications = async (userId: string): Promise<Notification[]> => {
try {
// Rechercher tous les commentaires de l'utilisateur
const userCommentsQuery = query(
collection(db, 'comments'),
where('userId', '==', userId)
);
const commentsSnapshot = await getDocs(userCommentsQuery);
// Récupérer toutes les notifications de tous les commentaires
const notifications: Notification[] = [];
commentsSnapshot.forEach(commentDoc => {
const commentData = commentDoc.data();
const commentNotifications = commentData.notifications || [];
if (commentNotifications.length > 0) {
// Convertir les dates pour chaque notification
const formattedNotifications = commentNotifications.map((notification: any) => ({
...notification,
createdAt: notification.createdAt?.toDate() || new Date()
}));
notifications.push(...formattedNotifications);
}
});
// Trier par date, les plus récentes en premier
return notifications.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
} catch (error) {
console.error('Error getting user notifications:', error);
return [];
}
};
/**
* Marquer une notification comme lue
*/
export const markNotificationAsRead = async (
commentId: string,
notificationId: string
): Promise<boolean> => {
try {
// Récupérer le commentaire
const commentRef = doc(db, 'comments', commentId);
const commentSnapshot = await getDoc(commentRef);
if (!commentSnapshot.exists()) {
console.error('Comment not found');
return false;
}
const commentData = commentSnapshot.data();
const notifications = commentData.notifications || [];
// Trouver et modifier la notification
const updatedNotifications = notifications.map((notification: any) => {
if (notification.id === notificationId) {
return { ...notification, read: true };
}
return notification;
});
// Mettre à jour le commentaire avec les notifications modifiées
await updateDoc(commentRef, {
notifications: updatedNotifications
});
return true;
} catch (error) {
console.error('Error marking notification as read:', error);
return false;
}
};
/**
* Marquer toutes les notifications comme lues
*/
export const markAllNotificationsAsRead = async (userId: string): Promise<boolean> => {
try {
// Rechercher tous les commentaires de l'utilisateur
const userCommentsQuery = query(
collection(db, 'comments'),
where('userId', '==', userId)
);
const commentsSnapshot = await getDocs(userCommentsQuery);
// Parcourir tous les commentaires et marquer toutes les notifications comme lues
const updatePromises = commentsSnapshot.docs.map(async (commentDoc) => {
const commentData = commentDoc.data();
const notifications = commentData.notifications || [];
if (notifications.length > 0) {
const updatedNotifications = notifications.map((notification: any) => ({
...notification,
read: true
}));
await updateDoc(doc(db, 'comments', commentDoc.id), {
notifications: updatedNotifications
});
}
});
await Promise.all(updatePromises);
return true;
} catch (error) {
console.error('Error marking all notifications as read:', error);
return false;
}
};
/**
* Récupère les détails du contenu (film/série) associé à un commentaire
*/
export const getCommentDetails = async (commentId: string): Promise<{ contentId: string, contentType: 'movie' | 'series' } | null> => {
try {
// Récupérer le commentaire
const commentRef = doc(db, 'comments', commentId);
const commentSnapshot = await getDoc(commentRef);
if (!commentSnapshot.exists()) {
console.error('Comment not found');
return null;
}
const commentData = commentSnapshot.data();
return {
contentId: commentData.contentId,
contentType: commentData.contentType,
};
} catch (error) {
console.error('Error getting comment details:', error);
return null;
}
};
/**
* Récupère le nombre de notifications non lues pour un utilisateur
*/
export const getUnreadNotificationsCount = async (userId: string): Promise<number> => {
try {
const notifications = await getUserNotifications(userId);
return notifications.filter(notification => !notification.read).length;
} catch (error) {
console.error('Error counting unread notifications:', error);
return 0;
}
};
/**
* Supprime une notification spécifique d'un commentaire
*/
export const deleteNotification = async (
commentId: string,
notificationId: string
): Promise<boolean> => {
try {
// Récupérer le commentaire
const commentRef = doc(db, 'comments', commentId);
const commentSnapshot = await getDoc(commentRef);
if (!commentSnapshot.exists()) {
console.error('Comment not found');
return false;
}
const commentData = commentSnapshot.data();
const notifications = commentData.notifications || [];
// Filtrer pour supprimer la notification
const updatedNotifications = notifications.filter(
(notification: any) => notification.id !== notificationId
);
// Mettre à jour le commentaire avec les notifications filtrées
await updateDoc(commentRef, {
notifications: updatedNotifications
});
return true;
} catch (error) {
console.error('Error deleting notification:', error);
return false;
}
};

View file

@ -1,43 +0,0 @@
export interface Reaction {
emoji: string;
count: number;
users: string[]; // IDs des utilisateurs qui ont réagi avec cet emoji
}
export interface Notification {
id: string;
type: 'reply' | 'like' | 'mention' | 'report_resolved' | 'report_resolved_deleted' | 'report_dismissed';
commentId: string;
replyId?: string;
content: string;
fromUserId: string;
fromUsername: string;
createdAt: Date;
read: boolean;
}
// Limites de caractères pour les commentaires
export const COMMENT_LENGTH_LIMITS = {
COMMENT: 500,
REPLY: 300
};
export interface Comment {
id: string;
contentId: string; // ID of the movie or series
contentType: 'movie' | 'series';
userId: string;
username: string;
userAvatar: string;
content: string;
createdAt: Date;
likes: number;
likedBy: string[]; // IDs des utilisateurs qui ont liké
dislikes: number;
dislikedBy: string[]; // IDs des utilisateurs qui ont disliké
reactions: Reaction[]; // Reactions avec emojis
replies?: Comment[];
parentId?: string; // For replies, refers to parent comment ID
isAdmin?: boolean; // Indique si le commentaire provient d'un admin
notifications?: Notification[]; // Notifications liées au commentaire
}