Files
memegoat/frontend/src/components/content-card.tsx
Mathis HERRIOT 9ccbd2ceb1 refactor: improve formatting, type safety, and component organization
- Adjusted inconsistent formatting for better readability across components and services.
- Enhanced type safety by adding placeholders for ignored error parameters and improving types across services.
- Improved component organization by reordering imports consistently and applying formatting updates in UI components.
2026-01-29 14:11:28 +01:00

324 lines
9.3 KiB
TypeScript

"use client";
import {
Edit,
Eye,
Flag,
Heart,
MoreHorizontal,
Share2,
Trash2,
Volume2,
VolumeX,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import * as React from "react";
import { toast } from "sonner";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAudio } from "@/providers/audio-provider";
import { useAuth } from "@/providers/auth-provider";
import { ContentService } from "@/services/content.service";
import { FavoriteService } from "@/services/favorite.service";
import type { Content } from "@/types/content";
import { UserContentEditDialog } from "./user-content-edit-dialog";
interface ContentCardProps {
content: Content;
onUpdate?: () => void;
}
export function ContentCard({ content, onUpdate }: ContentCardProps) {
const { isAuthenticated, user } = useAuth();
const { isGlobalMuted, activeVideoId, toggleGlobalMute, setActiveVideo } =
useAudio();
const router = useRouter();
const [isLiked, setIsLiked] = React.useState(content.isLiked || false);
const [likesCount, setLikesCount] = React.useState(content.favoritesCount);
const [editDialogOpen, setEditDialogOpen] = React.useState(false);
const [_reportDialogOpen, setReportDialogOpen] = React.useState(false);
const isAuthor = user?.uuid === content.authorId;
const isVideo = !content.mimeType.startsWith("image/");
const isThisVideoActive = activeVideoId === content.id;
const isMuted = isGlobalMuted || (isVideo && !isThisVideoActive);
const videoRef = React.useRef<HTMLVideoElement>(null);
React.useEffect(() => {
if (videoRef.current) {
if (isThisVideoActive) {
const playPromise = videoRef.current.play();
if (playPromise !== undefined) {
playPromise.catch((_error) => {
// L'auto-lecture peut échouer si l'utilisateur n'a pas interagi avec la page
// On peut tenter de mettre en sourdine pour forcer la lecture si nécessaire
});
}
} else {
videoRef.current.pause();
}
}
}, [isThisVideoActive]);
React.useEffect(() => {
setIsLiked(content.isLiked || false);
setLikesCount(content.favoritesCount);
}, [content.isLiked, content.favoritesCount]);
const handleLike = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!isAuthenticated) {
toast.error("Vous devez être connecté pour liker un mème");
router.push("/login");
return;
}
try {
if (isLiked) {
await FavoriteService.remove(content.id);
setIsLiked(false);
setLikesCount((prev) => prev - 1);
} else {
await FavoriteService.add(content.id);
setIsLiked(true);
setLikesCount((prev) => prev + 1);
}
} catch (_error) {
toast.error("Une erreur est survenue");
}
};
const handleToggleMute = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (isGlobalMuted) {
setActiveVideo(content.id);
} else if (isThisVideoActive) {
toggleGlobalMute();
} else {
setActiveVideo(content.id);
}
};
const handleUse = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
try {
await ContentService.incrementUsage(content.id);
toast.success("Mème prêt à être utilisé !");
} catch (_error) {
toast.error("Une erreur est survenue");
}
};
const handleDelete = async () => {
if (!confirm("Êtes-vous sûr de vouloir supprimer ce mème ?")) return;
try {
await ContentService.remove(content.id);
toast.success("Mème supprimé !");
if (onUpdate) {
onUpdate();
} else {
// Si pas de onUpdate, on est probablement sur la page de détail
router.push("/");
}
} catch (_error) {
toast.error("Erreur lors de la suppression.");
}
};
return (
<>
<Card className="overflow-hidden border-none gap-0 shadow-none bg-transparent">
<CardHeader className="p-3 flex flex-row items-center space-y-0 gap-3">
<Avatar className="h-8 w-8 border">
<AvatarImage src={content.author.avatarUrl} />
<AvatarFallback>
{content.author.username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<Link
href={`/user/${content.author.username}`}
className="text-sm font-bold hover:underline"
>
{content.author.username}
</Link>
</div>
<div className="ml-auto flex items-center gap-1">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isAuthor && (
<>
<DropdownMenuItem onClick={() => setEditDialogOpen(true)}>
<Edit className="h-4 w-4 mr-2" />
Modifier
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleDelete}
className="text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
Supprimer
</DropdownMenuItem>
</>
)}
<DropdownMenuItem onClick={() => toast.success("Lien copié !")}>
<Share2 className="h-4 w-4 mr-2" />
Partager
</DropdownMenuItem>
{!isAuthor && (
<DropdownMenuItem onClick={() => setReportDialogOpen(true)}>
<Flag className="h-4 w-4 mr-2" />
Signaler
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="p-0 relative bg-zinc-200 dark:bg-zinc-900 flex items-center justify-center overflow-hidden aspect-square w-full">
<Link
href={`/meme/${content.slug}`}
className="w-full h-full block relative"
>
{content.mimeType.startsWith("image/") ? (
<Image
src={content.url}
alt={content.title}
width={content.width || 1000}
height={content.height || 1000}
className="w-full h-full object-contain"
priority={false}
/>
) : (
<video
ref={videoRef}
src={content.url}
controls={false}
autoPlay={isThisVideoActive}
muted={isMuted}
loop
playsInline
className="w-full h-full object-contain"
/>
)}
</Link>
{isVideo && (
<Button
variant="ghost"
size="icon"
className="absolute bottom-2 right-2 h-8 w-8 rounded-full bg-black/50 text-white hover:bg-black/70 hover:text-white"
onClick={handleToggleMute}
>
{isMuted ? (
<VolumeX className="h-4 w-4" />
) : (
<Volume2 className="h-4 w-4" />
)}
</Button>
)}
</CardContent>
<CardFooter className="p-3 flex flex-col items-start gap-2">
<div className="w-full flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={handleLike}
className={`transition-transform active:scale-125 ${isLiked ? "text-red-500" : "hover:text-muted-foreground"}`}
>
<Heart className={`h-6 w-6 ${isLiked ? "fill-current" : ""}`} />
</button>
<div className="flex items-center gap-1.5 text-muted-foreground">
<Eye className="h-6 w-6" />
<span className="text-sm font-medium">{content.views}</span>
</div>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/meme/${content.slug}`,
);
toast.success("Lien copié !");
}}
className="hover:text-muted-foreground"
>
<Share2 className="h-6 w-6" />
</button>
</div>
<Button
size="sm"
variant="secondary"
className="text-xs h-8 font-semibold rounded-full px-4"
onClick={handleUse}
>
Utiliser
</Button>
</div>
<div className="space-y-1">
<p className="text-sm font-bold">{likesCount} J'aime</p>
<div className="text-sm leading-snug">
<Link
href={`/user/${content.author.username}`}
className="font-bold mr-2 hover:underline"
>
{content.author.username}
</Link>
<Link href={`/meme/${content.slug}`} className="break-words">
{content.title}
</Link>
</div>
<div className="flex flex-wrap gap-1 mt-1">
{content.tags.slice(0, 5).map((tag, _i) => (
<Link
key={typeof tag === "string" ? tag : tag.id}
href={`/?tag=${typeof tag === "string" ? tag : tag.slug}`}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
#{typeof tag === "string" ? tag : tag.name}
</Link>
))}
</div>
<p className="text-[10px] text-muted-foreground uppercase mt-1">
{new Date(content.createdAt).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
})}
</p>
</div>
</CardFooter>
</Card>
<UserContentEditDialog
content={content}
open={editDialogOpen}
onOpenChange={setEditDialogOpen}
onSuccess={() => onUpdate?.()}
/>
</>
);
}