added username suggestion to AddGroupMember.tsx

This commit is contained in:
slawk0
2024-12-05 22:37:31 +01:00
parent b9b925d612
commit 7bb7dcd669
5 changed files with 223 additions and 36 deletions

View File

@@ -1,13 +1,13 @@
import LoadingWheel from './LoadingWheel.tsx';
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
import { axiosClient } from '../../App.tsx';
import { ContactsProps } from '../../pages/Chat.tsx';
import { socket } from '../../socket/socket.tsx';
import { AxiosResponse } from 'axios';
type Inputs = {
username: string;
user_id: number;
};
interface AddGroupMemberProps {
@@ -15,20 +15,61 @@ interface AddGroupMemberProps {
}
function AddGroupMember({ contact }: AddGroupMemberProps) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const { register, handleSubmit, watch, reset } = useForm<Inputs>();
const modalRef = useRef<HTMLDialogElement | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<Inputs>();
const contactInput = watch('username');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [notFound, setNotFound] = useState<boolean>(false);
const [selectedIndex, setSelectedIndex] = useState<number>(0);
useEffect(() => {
const fetchSuggestions = async () => {
if (contactInput?.length >= 3) {
try {
setIsLoading(true);
const response: AxiosResponse<string[]> = await axiosClient.get(
`/api/chat/contacts/suggestions/${contactInput}`,
);
setSuggestions(response.data);
setSelectedIndex(0); // Reset selection to first item when suggestions update
if (response.data.length < 1) {
setIsLoading(false);
setNotFound(true);
} else {
setIsLoading(false);
setNotFound(false);
}
} catch (error) {
setIsLoading(false);
console.error('Error fetching suggestions:', error);
}
} else {
setNotFound(false);
setIsLoading(false);
setSuggestions([]);
}
};
const delay = setTimeout(() => {
fetchSuggestions();
}, 300);
return () => clearTimeout(delay);
}, [contactInput]);
if (!socket) return;
const onSubmit: SubmitHandler<Inputs> = async (data) => {
const contactToSubmit =
suggestions.length > 0
? suggestions[selectedIndex]
: data.username.trim();
try {
setIsLoading(true);
const response = await axiosClient.post(`/api/chat/groups/addMember/`, {
group_id: contact?.conversation_id,
username: data.username,
username: contactToSubmit,
});
console.log(response.data);
setIsLoading(false);
@@ -42,6 +83,30 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (suggestions.length > 0) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % suggestions.length);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(
(prev) => (prev - 1 + suggestions.length) % suggestions.length,
);
break;
case 'Enter':
if (suggestions.length > 0) {
e.preventDefault();
reset({ username: suggestions[selectedIndex] });
handleSubmit(onSubmit)();
}
break;
}
}
};
return (
<>
<div>
@@ -65,26 +130,51 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
<div className="flex flex-col items-center justify-center pb-1 mt-6">
<h3 className="text-lg mb-4">Enter username</h3>
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full max-w-xs relative"
>
<input
className="input input-bordered bg-green-50 w-full text-black rounded-md text-center"
{...register('username', {
required: true,
minLength: 4,
maxLength: 20,
})}
aria-invalid={errors.username ? 'true' : 'false'}
/>
{errors.username?.type === 'minLength' && (
<p className="text-gray-300">Invalid username</p>
)}
{errors.username?.type === 'maxLength' && (
<p className="text-gray-300">Invalid username</p>
)}
</form>
<div className="text-center">
<form onSubmit={handleSubmit(onSubmit)}>
<input
className="text-black bg-green-50 pl-2 shadow-lg rounded-md h-8 mb-2 mt-2"
type="text"
onKeyDown={handleKeyDown}
{...register('username', {
minLength: 4,
maxLength: 20,
})}
/>
</form>
<div className="m-1">
{suggestions?.length > 0 ? (
<ul className="text-left p-2 bg-gray-900 shadow-md rounded-md w-full border">
{suggestions.map((suggestion, index) => (
<li
key={suggestion}
className={`p-1 cursor-pointer rounded-md mt-1 mb-1 ${
index === selectedIndex
? 'bg-gray-500'
: 'hover:bg-gray-500'
}`}
onClick={() => {
reset({ username: suggestion });
handleSubmit(() =>
onSubmit({ username: suggestion }),
)();
setSuggestions([]);
}}
>
{suggestion}
</li>
))}
</ul>
) : isLoading ? (
<LoadingWheel />
) : null}
{notFound ? (
<p className="p-1 bg-gray-800 shadow-md rounded-md w-full border text-gray-400">
user not found
</p>
) : null}
</div>
</div>
</div>
<div className="mt-4 flex justify-center">

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { getContactsList } from '../../api/contactsApi.tsx';
import { ChatMessages, ContactsProps } from '../../pages/Chat.tsx';
import { axiosClient } from '../../App.tsx';
import { joinRoom, socket } from '../../socket/socket.tsx';
import { socket } from '../../socket/socket.tsx';
import GroupIcon from '../../../assets/group.svg';
type ContactsListProps = {
@@ -34,7 +34,6 @@ function ContactsList({
fetchContacts();
});
// Cleanup socket event listener
return () => {
socket?.off('added to group');
};

View File

@@ -0,0 +1,90 @@
import LoadingWheel from './LoadingWheel.tsx';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useEffect, useRef, useState } from 'react';
import { AxiosResponse } from 'axios';
import { axiosClient } from '../../App.tsx';
import { ContactsProps } from '../../pages/Chat.tsx';
import { socket } from '../../socket/socket.tsx';
type ContactsSuggestionsProps = {
contact: ContactsProps;
};
function ContactsSuggestions({ contact }: ContactsSuggestionsProps) {
const { register, handleSubmit, reset, watch } = useForm<{
contact: string;
}>();
const submitContact: SubmitHandler<{ contact: string }> = async (data) => {
const contactToSubmit =
suggestions.length > 0 ? suggestions[selectedIndex] : data.contact.trim();
try {
setIsLoading(true);
const response = await axiosClient.post(`/api/chat/groups/addMember/`, {
group_id: contact?.conversation_id,
username: data.username,
});
console.log(response.data);
setIsLoading(false);
socket?.emit('added to group', { group_id: contact?.conversation_id });
if (modalRef.current) {
modalRef.current.close();
}
} catch (e) {
console.error('Failed to create group: ', e);
setIsLoading(false);
}
};
return (
<>
<div className="text-center">
<form onSubmit={handleSubmit(submitContact)}>
<input
className="text-black bg-green-50 pl-2 shadow-lg rounded-md h-8 mb-2 mt-2"
type="text"
placeholder="Enter contact"
onKeyDown={handleKeyDown}
{...register('contact', {
minLength: 4,
maxLength: 20,
})}
/>
</form>
<div className="m-1">
{suggestions?.length > 0 ? (
<ul className="text-left p-2 bg-gray-900 shadow-md rounded-md w-full border">
{suggestions.map((suggestion, index) => (
<li
key={suggestion}
className={`p-1 cursor-pointer rounded-md mt-1 mb-1 ${
index === selectedIndex
? 'bg-gray-500'
: 'hover:bg-gray-500'
}`}
onClick={() => {
reset({ contact: suggestion });
handleSubmit(() =>
submitContact({ contact: suggestion }),
)();
setSuggestions([]);
}}
>
{suggestion}
</li>
))}
</ul>
) : isLoading ? (
<LoadingWheel />
) : null}
{notFound ? (
<p className="p-1 bg-gray-800 shadow-md rounded-md w-full border text-gray-400">
user not found
</p>
) : null}
</div>
</div>
</>
);
}

View File

@@ -18,6 +18,7 @@ type MessagesAreaProps = {
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
fetchPreviousMessages: (contact: number | null) => Promise<void>;
errorMessage: string | null;
setContactStatus: (contact: string, read: boolean) => Promise<void>;
};
function MessagesArea({
@@ -53,7 +54,7 @@ function MessagesArea({
const previousScrollHeight = container.scrollHeight;
try {
await fetchPreviousMessages(currentContact?.conversation_id);
await fetchPreviousMessages(currentContact!.conversation_id);
container.scrollTop = container.scrollHeight - previousScrollHeight;
} catch (e) {
@@ -79,7 +80,9 @@ function MessagesArea({
prevMessages.filter((message) => message.message_id !== message_id),
);
} catch (e) {
console.error('Failed to delete message');
if (e instanceof Error) {
console.error('Failed to delete message');
}
}
};
@@ -110,6 +113,7 @@ function MessagesArea({
id: msg.message_id,
user_id: msg.sender_id,
conversation_id: msg.conversation_id,
type: 'direct',
},
];
}

View File

@@ -8,6 +8,7 @@ import ContactsList from '../components/chat/ContactsList.tsx';
import { initializeSocket, joinRoom } from '../socket/socket.tsx';
import Cookies from 'js-cookie';
import { getMessages, setContactStatus } from '../api/contactsApi.tsx';
import axios from 'axios';
export type ChatMessages = {
sender: string;
@@ -94,7 +95,9 @@ function Chat() {
messages.messages.forEach(messageHandler);
} catch (e) {
setErrorMessage(e.response.data.message);
if (axios.isAxiosError(e)) {
setErrorMessage(e.message);
}
}
};
@@ -133,7 +136,9 @@ function Chat() {
);
});
} catch (e) {
setErrorMessage(e.response.data.message);
if (axios.isAxiosError(e)) {
setErrorMessage(e.message);
}
}
};
function messageHandler(msg: ChatMessages) {
@@ -199,7 +204,6 @@ function Chat() {
setContactsList={setContactsList}
fetchPreviousMessages={fetchPreviousMessages}
errorMessage={errorMessage}
hasMoreMessages={hasMoreMessages}
/>
</div>
<div className="flex-shrink-0 mb-2 mt-0">