feat: improve accessibility, security & user interaction in notifications and setup

- 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`.
This commit is contained in:
Mathis HERRIOT
2026-01-29 17:21:19 +01:00
parent 6398965f16
commit f9b202375f
4 changed files with 92 additions and 47 deletions

View File

@@ -2,7 +2,7 @@
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { fr } from "date-fns/locale"; import { fr } from "date-fns/locale";
import { Search, Send, UserPlus, X } from "lucide-react"; import { ArrowLeft, Search, Send, UserPlus, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import * as React from "react"; import * as React from "react";
@@ -238,7 +238,11 @@ export default function MessagesPage() {
return ( return (
<div className="h-[calc(100vh-4rem)] flex overflow-hidden bg-white dark:bg-zinc-950"> <div className="h-[calc(100vh-4rem)] flex overflow-hidden bg-white dark:bg-zinc-950">
{/* Sidebar - Liste des conversations */} {/* Sidebar - Liste des conversations */}
<div className="w-80 border-r flex flex-col"> <div
className={`w-full md:w-80 border-r flex flex-col ${
activeConv ? "hidden md:flex" : "flex"
}`}
>
<div className="p-4 border-b"> <div className="p-4 border-b">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Messages</h2> <h2 className="text-xl font-bold">Messages</h2>
@@ -378,14 +382,26 @@ export default function MessagesPage() {
</div> </div>
{/* Zone de chat */} {/* Zone de chat */}
<div className="flex-1 flex flex-col"> <div
className={`flex-1 flex flex-col ${
!activeConv ? "hidden md:flex" : "flex"
}`}
>
{activeConv ? ( {activeConv ? (
<> <>
{/* Header */} {/* Header */}
<div className="p-4 border-b flex items-center justify-between"> <div className="p-4 border-b flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="md:hidden rounded-full"
onClick={() => setActiveConv(null)}
>
<ArrowLeft className="h-5 w-5" />
</Button>
<Link <Link
href={`/user/${activeConv.recipient.username}`} href={`/user/${activeConv.recipient.username}`}
className="flex items-center gap-3 hover:opacity-80 transition-opacity" className="flex-1 flex items-center gap-3 hover:opacity-80 transition-opacity"
> >
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
<AvatarImage src={activeConv.recipient.avatarUrl} /> <AvatarImage src={activeConv.recipient.avatarUrl} />

View File

@@ -30,10 +30,9 @@ export function NotificationHandler() {
toast.custom( toast.custom(
(t) => ( (t) => (
<div <button
role="button" type="button"
tabIndex={0} className="flex items-start gap-3 bg-white dark:bg-zinc-900 p-4 rounded-xl shadow-lg border border-zinc-200 dark:border-zinc-800 w-full max-w-sm cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors text-left"
className="flex items-start gap-3 bg-white dark:bg-zinc-900 p-4 rounded-xl shadow-lg border border-zinc-200 dark:border-zinc-800 w-full max-w-sm cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800 transition-colors"
onClick={() => { onClick={() => {
toast.dismiss(t); toast.dismiss(t);
if (data.type === "message") { if (data.type === "message") {
@@ -42,16 +41,6 @@ export function NotificationHandler() {
router.push(`/meme/${data.contentId}`); router.push(`/meme/${data.contentId}`);
} }
}} }}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
toast.dismiss(t);
if (data.type === "message") {
router.push("/messages");
} else if (data.contentId) {
router.push(`/meme/${data.contentId}`);
}
}
}}
> >
<div className="bg-primary/10 p-2 rounded-full shrink-0"> <div className="bg-primary/10 p-2 rounded-full shrink-0">
{data.type === "comment" && ( {data.type === "comment" && (
@@ -71,15 +60,15 @@ export function NotificationHandler() {
</div> </div>
<button <button
type="button" type="button"
className="text-muted-foreground hover:text-foreground p-1 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
toast.dismiss(t); toast.dismiss(t);
}} }}
className="text-muted-foreground hover:text-foreground"
> >
<Bell className="h-3 w-3" /> <Bell className="h-3 w-3" />
</button> </button>
</div> </button>
), ),
{ {
duration: 5000, duration: 5000,
@@ -91,20 +80,23 @@ export function NotificationHandler() {
socket.on("notification", handleNotification); socket.on("notification", handleNotification);
// Aussi pour les nouveaux messages (si on veut un toast global) // Aussi pour les nouveaux messages (si on veut un toast global)
socket.on("new_message", (data: { message: any }) => { socket.on(
if (window.location.pathname !== "/messages") { "new_message",
toast( (data: { message: { text: string; sender?: { username: string } } }) => {
`Nouveau message de @${data.message.sender?.username || "un membre"}`, if (window.location.pathname !== "/messages") {
{ toast(
description: data.message.text.substring(0, 50), `Nouveau message de @${data.message.sender?.username || "un membre"}`,
action: { {
label: "Voir", description: data.message.text.substring(0, 50),
onClick: () => router.push("/messages"), action: {
label: "Voir",
onClick: () => router.push("/messages"),
},
}, },
}, );
); }
} },
}); );
return () => { return () => {
socket.off("notification"); socket.off("notification");

View File

@@ -28,9 +28,10 @@ interface ShareDialogProps {
export function ShareDialog({ export function ShareDialog({
contentId, contentId,
contentTitle, contentTitle,
contentUrl: _unused, // Support legacy prop
open, open,
onOpenChange, onOpenChange,
}: Omit<ShareDialogProps, "contentUrl">) { }: ShareDialogProps) {
const [searchQuery, setSearchQuery] = React.useState(""); const [searchQuery, setSearchQuery] = React.useState("");
const [results, setResults] = React.useState<User[]>([]); const [results, setResults] = React.useState<User[]>([]);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);

View File

@@ -29,6 +29,7 @@ export function TwoFactorSetup() {
const [secret, setSecret] = useState<string | null>(null); const [secret, setSecret] = useState<string | null>(null);
const [otpValue, setOtpValue] = useState(""); const [otpValue, setOtpValue] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isRevealed, setIsRevealed] = useState(false);
const handleSetup = async () => { const handleSetup = async () => {
setIsLoading(true); setIsLoading(true);
@@ -152,24 +153,59 @@ export function TwoFactorSetup() {
</CardHeader> </CardHeader>
<CardContent className="flex flex-col items-center gap-6"> <CardContent className="flex flex-col items-center gap-6">
{qrCode && ( {qrCode && (
<div className="bg-white p-4 rounded-xl border-4 border-zinc-100"> <div className="relative group">
<Image <div
src={qrCode} className={`bg-white p-4 rounded-xl border-4 border-zinc-100 transition-all duration-300 ${
alt="QR Code 2FA" !isRevealed ? "blur-md select-none" : ""
width={192} }`}
height={192} >
className="w-48 h-48" <Image
unoptimized src={qrCode}
/> alt="QR Code 2FA"
width={192}
height={192}
className="w-48 h-48"
unoptimized
/>
</div>
{!isRevealed && (
<div className="absolute inset-0 flex items-center justify-center">
<Button
variant="secondary"
size="sm"
onClick={() => setIsRevealed(true)}
className="shadow-lg"
>
Afficher le QR Code
</Button>
</div>
)}
</div> </div>
)} )}
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<p className="text-sm font-medium text-center"> <p className="text-sm font-medium text-center">
Ou entrez ce code manuellement : Ou entrez ce code manuellement :
</p> </p>
<code className="block p-2 bg-muted text-center rounded text-xs font-mono break-all"> <div className="relative group">
{secret} <code
</code> className={`block p-2 bg-muted text-center rounded text-xs font-mono break-all transition-all duration-300 ${
!isRevealed ? "blur-[3px] select-none" : ""
}`}
>
{secret}
</code>
{!isRevealed && (
<div className="absolute inset-0 flex items-center justify-center">
<button
type="button"
onClick={() => setIsRevealed(true)}
className="text-[10px] font-bold uppercase tracking-wider text-primary hover:underline"
>
Afficher le code
</button>
</div>
)}
</div>
</div> </div>
<div className="flex flex-col items-center gap-4 w-full border-t pt-6"> <div className="flex flex-col items-center gap-4 w-full border-t pt-6">
<p className="text-sm font-medium"> <p className="text-sm font-medium">