feat: accessibility for most components
This commit is contained in:
parent
ef521dfc47
commit
bc95fb10fc
13 changed files with 226 additions and 125 deletions
|
|
@ -25,11 +25,16 @@ function ChatInput({ sendMessage }: ChatInputProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="mt-4">
|
<form onSubmit={handleSubmit} className="mt-4">
|
||||||
|
<label htmlFor="chat-input" className="sr-only">
|
||||||
|
Type a message
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="chat-input"
|
||||||
type="text"
|
type="text"
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
placeholder="Message"
|
placeholder="Type a message" // this message is more descriptive than a simple "Message", therefore better for accessibility
|
||||||
|
aria-label="Chat message input"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ function ChatMenu() {
|
||||||
const { actions, state } = useChatState();
|
const { actions, state } = useChatState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 w-full">
|
<div aria-label="Chat navigation" className="flex gap-4 w-full">
|
||||||
<button
|
<button
|
||||||
onClick={() => actions.setMenu("chat")}
|
onClick={() => actions.setMenu("chat")}
|
||||||
|
aria-pressed={state.selectedMenu === "chat"}
|
||||||
className={`w-1/2 py-4 ${
|
className={`w-1/2 py-4 ${
|
||||||
state.selectedMenu === "chat" ? "bg-white" : "bg-gray-100"
|
state.selectedMenu === "chat" ? "bg-white" : "bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -16,6 +17,7 @@ function ChatMenu() {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => actions.setMenu("participants")}
|
onClick={() => actions.setMenu("participants")}
|
||||||
|
aria-pressed={state.selectedMenu === "participants"}
|
||||||
className={`w-1/2 py-4 ${
|
className={`w-1/2 py-4 ${
|
||||||
state.selectedMenu === "participants" ? "bg-white" : "bg-gray-100"
|
state.selectedMenu === "participants" ? "bg-white" : "bg-gray-100"
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { ChatMessage } from "../../../shared";
|
|
||||||
|
|
||||||
function ChatMessageDisplay({
|
|
||||||
message,
|
|
||||||
userId,
|
|
||||||
onDelete,
|
|
||||||
onEdit,
|
|
||||||
}: {
|
|
||||||
message: ChatMessage;
|
|
||||||
userId: string | undefined;
|
|
||||||
onDelete: (messageId: string) => void;
|
|
||||||
onEdit: (messageId: string, newContent: string) => void;
|
|
||||||
}) {
|
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
|
||||||
const [editContent, setEditContent] = useState(message.content);
|
|
||||||
const isOwn = userId === message.author.userId;
|
|
||||||
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (editContent.trim() !== message.content) {
|
|
||||||
onEdit(message.id, editContent);
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setEditContent(message.content);
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={message.id} className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
|
||||||
<div
|
|
||||||
className={`flex items-center gap-2 text-sm text-gray-600 mb-1
|
|
||||||
${isOwn ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
|
||||||
{!isOwn && (
|
|
||||||
<span className="font-bold text-gray-700">{message.author.name}</span>
|
|
||||||
)}
|
|
||||||
<span className="font-medium opacity-45">{timestamp}</span>
|
|
||||||
{isOwn && (
|
|
||||||
<span className="font-bold text-gray-700">{message.author.name}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className={`max-w-[80%] ${isOwn ? "ml-auto" : "mr-auto"}`}>
|
|
||||||
<textarea
|
|
||||||
value={editContent}
|
|
||||||
onChange={(e) => setEditContent(e.target.value)}
|
|
||||||
className="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
||||||
rows={2}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`flex gap-2 mt-2 ${isOwn ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={handleSave}
|
|
||||||
className="px-3 py-1 text-sm text-white bg-blue-600 rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="px-3 py-1 text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={`max-w-[80%] p-3 rounded-2xl shadow-sm bg-gray-200
|
|
||||||
${isOwn ? "ml-auto rounded-tr-none" : "mr-auto rounded-tl-none"}`}
|
|
||||||
>
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isOwn && !isEditing && (
|
|
||||||
<div
|
|
||||||
className={`flex gap-2 mt-0.5 text-[10px]
|
|
||||||
${isOwn ? "justify-end" : "justify-start"}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsEditing(true)}
|
|
||||||
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<span className="text-gray-400">•</span>
|
|
||||||
<button
|
|
||||||
onClick={() => onDelete?.(message.id)}
|
|
||||||
className="text-red-600 hover:text-red-800 active:text-red-900"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default React.memo(ChatMessageDisplay);
|
|
||||||
81
client/src/components/ChatMessage/ChatMessageDisplay.tsx
Normal file
81
client/src/components/ChatMessage/ChatMessageDisplay.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { ChatMessage } from "../../../../shared";
|
||||||
|
import MessageHeader from "./MessageHeader";
|
||||||
|
import MessageEditForm from "./MessageEditForm";
|
||||||
|
import MessageActions from "./MessageActions";
|
||||||
|
|
||||||
|
function ChatMessageDisplay({
|
||||||
|
message,
|
||||||
|
userId,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
}: {
|
||||||
|
message: ChatMessage;
|
||||||
|
userId: string | undefined;
|
||||||
|
onDelete: (messageId: string) => void;
|
||||||
|
onEdit: (messageId: string, newContent: string) => void;
|
||||||
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [editContent, setEditContent] = useState(message.content);
|
||||||
|
const isOwn = userId === message.author.userId;
|
||||||
|
|
||||||
|
const timestamp = new Date(message.timestamp).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (editContent.trim() !== message.content) {
|
||||||
|
onEdit(message.id, editContent);
|
||||||
|
}
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setEditContent(message.content);
|
||||||
|
setIsEditing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
key={message.id}
|
||||||
|
className={`mb-4 ${isOwn ? "ml-auto" : "mr-auto"}`}
|
||||||
|
aria-label={`Message from ${message.author.name} on ${timestamp}`}
|
||||||
|
>
|
||||||
|
<MessageHeader
|
||||||
|
author={message.author.name}
|
||||||
|
timestamp={timestamp}
|
||||||
|
isOwn={isOwn}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{isEditing ? (
|
||||||
|
<MessageEditForm
|
||||||
|
editContent={editContent}
|
||||||
|
setEditContent={setEditContent}
|
||||||
|
onSave={handleSave}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] p-3 rounded-2xl shadow-sm bg-gray-200
|
||||||
|
${isOwn ? "ml-auto rounded-tr-none" : "mr-auto rounded-tl-none"}`}
|
||||||
|
aria-label="Message content"
|
||||||
|
>
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
{isOwn && (
|
||||||
|
<MessageActions
|
||||||
|
onEdit={() => setIsEditing(true)}
|
||||||
|
onDelete={() => onDelete(message.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(ChatMessageDisplay);
|
||||||
34
client/src/components/ChatMessage/MessageActions.tsx
Normal file
34
client/src/components/ChatMessage/MessageActions.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
const MessageActions = ({
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`flex gap-2 mt-0.5 text-[10px] justify-end`}
|
||||||
|
aria-label="Message actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onEdit}
|
||||||
|
className="text-blue-600 hover:text-blue-800 active:text-blue-900"
|
||||||
|
aria-label="Edit message"
|
||||||
|
title="Edit message"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<span className="text-gray-400" aria-hidden="true">
|
||||||
|
•
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={onDelete}
|
||||||
|
className="text-red-600 hover:text-red-800 active:text-red-900"
|
||||||
|
aria-label="Delete message"
|
||||||
|
title="Delete message"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MessageActions;
|
||||||
44
client/src/components/ChatMessage/MessageEditForm.tsx
Normal file
44
client/src/components/ChatMessage/MessageEditForm.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
const MessageEditForm = ({
|
||||||
|
editContent,
|
||||||
|
setEditContent,
|
||||||
|
onSave,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
editContent: string;
|
||||||
|
setEditContent: (content: string) => void;
|
||||||
|
onSave: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => (
|
||||||
|
<div className={`max-w-[80%] ml-auto`}>
|
||||||
|
<textarea
|
||||||
|
value={editContent}
|
||||||
|
onChange={(e) => setEditContent(e.target.value)}
|
||||||
|
className="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
rows={2}
|
||||||
|
autoFocus
|
||||||
|
aria-label="Edit message content"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`flex gap-2 mt-2 justify-end`}
|
||||||
|
role="group"
|
||||||
|
aria-label="Edit message actions"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onSave}
|
||||||
|
className="px-3 py-1 text-sm text-white bg-blue-600 rounded hover:bg-blue-700"
|
||||||
|
aria-label="Save changes"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
className="px-3 py-1 text-sm text-gray-600 bg-gray-200 rounded hover:bg-gray-300"
|
||||||
|
aria-label="Cancel editing"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MessageEditForm;
|
||||||
20
client/src/components/ChatMessage/MessageHeader.tsx
Normal file
20
client/src/components/ChatMessage/MessageHeader.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
const MessageHeader = ({
|
||||||
|
author,
|
||||||
|
timestamp,
|
||||||
|
isOwn,
|
||||||
|
}: {
|
||||||
|
author: string;
|
||||||
|
timestamp: string;
|
||||||
|
isOwn: boolean;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-2 text-sm text-gray-600 mb-1
|
||||||
|
${isOwn ? "justify-end" : "justify-start"}`}
|
||||||
|
>
|
||||||
|
{!isOwn && <span className="font-bold text-gray-700">{author}</span>}
|
||||||
|
<span className="font-medium opacity-45">{timestamp}</span>
|
||||||
|
{isOwn && <span className="font-bold text-gray-700">{author}</span>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MessageHeader;
|
||||||
|
|
@ -16,7 +16,7 @@ function Connect({ onConnect }: ConnectProps) {
|
||||||
|
|
||||||
actions.setUser({
|
actions.setUser({
|
||||||
name: name,
|
name: name,
|
||||||
userId: "", // leave userId empty for now, it is server generated
|
userId: "", // leave userId empty for now, it is server generated and inserted by the REGISTRATION_CONFIRMED message
|
||||||
});
|
});
|
||||||
|
|
||||||
onConnect(name);
|
onConnect(name);
|
||||||
|
|
@ -29,16 +29,26 @@ function Connect({ onConnect }: ConnectProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 mt-4">
|
<div className="flex gap-4 mt-4" role="form" aria-label="Connect to chat">
|
||||||
|
<label htmlFor="username-input" className="sr-only">
|
||||||
|
Enter your name
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
|
id="username-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter your name"
|
placeholder="Enter your name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
|
aria-required="true"
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
<button onClick={handleConnect} disabled={!name} className="btn">
|
<button
|
||||||
|
onClick={handleConnect}
|
||||||
|
disabled={!name}
|
||||||
|
className="btn"
|
||||||
|
aria-label="Connect to chat room"
|
||||||
|
>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ function Layout({ children }: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="min-h-screen p-4">
|
<main className="min-h-screen p-4">
|
||||||
<header className="top-0 w-full bg-white">
|
<header className="top-0 w-full bg-white" role="banner">
|
||||||
<h1 className="text-3xl text-center">Simple Chat</h1>
|
<h1 className="text-3xl text-center">Simple Chat</h1>
|
||||||
</header>
|
</header>
|
||||||
<div className="max-w-2xl md:p-8 m-auto">{children}</div>
|
<div className="max-w-2xl md:p-8 m-auto" role="main">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
} from "../../../shared";
|
} from "../../../shared";
|
||||||
import { useChatState } from "../contexts/ChatContext";
|
import { useChatState } from "../contexts/ChatContext";
|
||||||
import ChatMessage from "./ChatMessage";
|
import ChatMessageDisplay from "./ChatMessage/ChatMessageDisplay";
|
||||||
|
|
||||||
interface MessageDisplayProps {
|
interface MessageDisplayProps {
|
||||||
sendMessage: (message: ClientMessage) => void;
|
sendMessage: (message: ClientMessage) => void;
|
||||||
|
|
@ -56,7 +56,7 @@ function ChatMessages({ sendMessage }: MessageDisplayProps) {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "CHAT_MESSAGE":
|
case "CHAT_MESSAGE":
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessageDisplay
|
||||||
message={message.payload}
|
message={message.payload}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
key={message.payload.id}
|
key={message.payload.id}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ function ParticipantList() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ul>
|
<ul role="list" aria-label="Participants list">
|
||||||
{state.participants.map((participant) => (
|
{state.participants.map((participant) => (
|
||||||
<li key={participant.userId} className="p-4 border-b border-gray-200">
|
<li key={participant.userId} className="p-4 border-b border-gray-200">
|
||||||
{participant.name}
|
{participant.name}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ function Home() {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClickCreateRoom}
|
onClick={onClickCreateRoom}
|
||||||
className="btn w-full"
|
className="btn w-full"
|
||||||
|
aria-label="Create new chat room"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -47,15 +48,18 @@ function Home() {
|
||||||
<input
|
<input
|
||||||
id="roomId"
|
id="roomId"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Room ID"
|
placeholder="Enter Room ID"
|
||||||
value={roomId}
|
value={roomId}
|
||||||
onChange={(e) => setRoomId(e.target.value)}
|
onChange={(e) => setRoomId(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
aria-required="true"
|
||||||
|
aria-invalid={!roomId}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClickJoinRoom}
|
onClick={onClickJoinRoom}
|
||||||
disabled={!roomId}
|
disabled={!roomId}
|
||||||
|
aria-disabled={!roomId}
|
||||||
className="w-full btn bg-green-600 hover:bg-green-700"
|
className="w-full btn bg-green-600 hover:bg-green-700"
|
||||||
>
|
>
|
||||||
Join
|
Join
|
||||||
|
|
|
||||||
|
|
@ -30,16 +30,24 @@ function Room() {
|
||||||
<Layout>
|
<Layout>
|
||||||
<section className="w-full">
|
<section className="w-full">
|
||||||
<RoomHeader roomId={roomId!} isConnected={isConnected} />
|
<RoomHeader roomId={roomId!} isConnected={isConnected} />
|
||||||
|
<nav aria-label="Chat navigation">
|
||||||
|
<ChatMenu />
|
||||||
|
</nav>
|
||||||
|
|
||||||
<ChatMenu />
|
|
||||||
{state.selectedMenu === "chat" && (
|
{state.selectedMenu === "chat" && (
|
||||||
<>
|
<section aria-label="Chat Section" className="chat-section">
|
||||||
<ChatMessages sendMessage={sendMessage} />
|
<ChatMessages sendMessage={sendMessage} />
|
||||||
<ChatInput sendMessage={sendMessage} />
|
<ChatInput sendMessage={sendMessage} />
|
||||||
</>
|
</section>
|
||||||
|
)}
|
||||||
|
{state.selectedMenu === "participants" && (
|
||||||
|
<section
|
||||||
|
aria-label="Participants list"
|
||||||
|
className="participants-section"
|
||||||
|
>
|
||||||
|
<ParticipantList />
|
||||||
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.selectedMenu === "participants" && <ParticipantList />}
|
|
||||||
</section>
|
</section>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue