- Replaced `div` with `button` elements in `NotificationHandler` for better semantics and accessibility. - Added conditional QR Code reveal in 2FA setup with `blur` effect for enhanced security and user control. - Enhanced messages layout for responsiveness on smaller screens with dynamic chat/sidebar toggling. - Simplified legacy prop handling in `ShareDialog`.
187 lines
5.3 KiB
TypeScript
187 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { Search, Send, X } from "lucide-react";
|
|
import * as React from "react";
|
|
import { toast } from "sonner";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { MessageService } from "@/services/message.service";
|
|
import { UserService } from "@/services/user.service";
|
|
import type { User } from "@/types/user";
|
|
|
|
interface ShareDialogProps {
|
|
contentId: string;
|
|
contentTitle: string;
|
|
contentUrl: string;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function ShareDialog({
|
|
contentId,
|
|
contentTitle,
|
|
contentUrl: _unused, // Support legacy prop
|
|
open,
|
|
onOpenChange,
|
|
}: ShareDialogProps) {
|
|
const [searchQuery, setSearchQuery] = React.useState("");
|
|
const [results, setResults] = React.useState<User[]>([]);
|
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
const [sendingTo, setSendingTo] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!open) {
|
|
setSearchQuery("");
|
|
setResults([]);
|
|
return;
|
|
}
|
|
|
|
const fetchInitial = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
// Par défaut, montrer les conversations récentes ou suggérer des gens
|
|
const recent = await UserService.search("");
|
|
setResults(recent);
|
|
} catch (error) {
|
|
console.error("Failed to fetch users", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
fetchInitial();
|
|
}, [open]);
|
|
|
|
React.useEffect(() => {
|
|
if (searchQuery.length < 2) return;
|
|
|
|
const timeout = setTimeout(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await UserService.search(searchQuery);
|
|
setResults(data);
|
|
} catch (error) {
|
|
console.error("Search failed", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, 300);
|
|
|
|
return () => clearTimeout(timeout);
|
|
}, [searchQuery]);
|
|
|
|
const handleShare = async (user: User) => {
|
|
setSendingTo(user.uuid);
|
|
try {
|
|
const shareUrl = `${window.location.origin}/meme/${contentId}`;
|
|
await MessageService.sendMessage(
|
|
user.uuid,
|
|
`Regarde ce mème : ${contentTitle}\n${shareUrl}`,
|
|
);
|
|
toast.success(`Partagé avec @${user.username}`);
|
|
onOpenChange(false);
|
|
} catch (_error) {
|
|
toast.error("Échec du partage");
|
|
} finally {
|
|
setSendingTo(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[425px] p-0 gap-0 overflow-hidden">
|
|
<DialogHeader className="p-4 border-b">
|
|
<DialogTitle>Partager avec</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="p-4 border-b">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Rechercher un membre..."
|
|
className="pl-9 h-9"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchQuery("")}
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full"
|
|
>
|
|
<X className="h-3 w-3 text-muted-foreground" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<ScrollArea className="h-[300px]">
|
|
<div className="p-2 space-y-1">
|
|
{isLoading && results.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
Chargement...
|
|
</div>
|
|
) : results.length === 0 ? (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
Aucun membre trouvé.
|
|
</div>
|
|
) : (
|
|
results.map((user) => (
|
|
<div
|
|
key={user.uuid}
|
|
className="flex items-center justify-between p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-900"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-9 w-9">
|
|
<AvatarImage src={user.avatarUrl} />
|
|
<AvatarFallback>{user.username[0].toUpperCase()}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex flex-col">
|
|
<span className="text-sm font-bold leading-none">
|
|
{user.displayName || user.username}
|
|
</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
@{user.username}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
size="sm"
|
|
variant={sendingTo === user.uuid ? "outline" : "default"}
|
|
disabled={sendingTo !== null}
|
|
onClick={() => handleShare(user)}
|
|
className="h-8 px-4 rounded-full"
|
|
>
|
|
{sendingTo === user.uuid ? "Envoi..." : "Envoyer"}
|
|
</Button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
<div className="p-4 border-t bg-zinc-50 dark:bg-zinc-900/50">
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start gap-2 h-10 rounded-xl"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
`${window.location.origin}/meme/${contentId}`,
|
|
);
|
|
toast.success("Lien copié !");
|
|
onOpenChange(false);
|
|
}}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Copier le lien
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|