mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-08-03 20:26:04 +00:00
j'ai oublié de supprimé les fichiers code morts
This commit is contained in:
parent
4bba4cab40
commit
e57910d274
10 changed files with 0 additions and 2389 deletions
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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' }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in a new issue