Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion backend/src/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import User from "../models/User.js";
export const authMe = async (req, res) => {
try {
const user = req.user; // lấy từ authMiddleware
Expand All @@ -9,4 +10,20 @@ export const authMe = async (req, res) => {
console.error("Lỗi khi gọi authMe", error);
return res.status(500).json({ message: "Lỗi hệ thống" });
}
};
};

export const searchUserByUserName = async (req, res) => {
try {
const {username} = req.query;
if(!username || username.trim() === ""){
return res.status(400).json({message:"Hay nhap username"})
}

const user = await User.findOne({username}).select("_id displayName userName avartarUrl");

return res.status(200).json({user});
} catch (error) {
console.error("Loi xay ra khi search user by username:",error);
return res.status(500).json({message:"Loi he thong"})
}
};
3 changes: 2 additions & 1 deletion backend/src/routes/userRoute.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import express from "express";
import { authMe } from "../controllers/userController.js";
import { authMe, searchUserByUserName } from "../controllers/userController.js";

const router = express.Router();

router.get("/me", authMe);
router.get("/search",searchUserByUserName)

export default router;
94 changes: 94 additions & 0 deletions frontend/src/components/AddFriendModal/SearchForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { FieldErrors, UseFormRegister } from "react-hook-form";
import type { IFormValues } from "../chat/modals/AddFriendModal";
import { Label } from "../ui/label";
import { Input } from "../ui/input";
import { DialogClose, DialogFooter } from "../ui/dialog";
import { Button } from "../ui/button";
import { LoaderIcon, Search } from "lucide-react";

interface SearchFormProps {
register: UseFormRegister<IFormValues>;
errors: FieldErrors<IFormValues>;
loading: boolean;
usernameValue: string;
isFound: boolean | null;
searchedUsername: string;
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
onCancel: () => void;
}

const SearchForm = ({
register,
errors,
loading,
usernameValue,
isFound,
searchedUsername,
onSubmit,
onCancel,
}: SearchFormProps) => {
return (
<form onSubmit={onSubmit} className="space-y-4 ">
<div className="space-y-2">
<Label htmlFor="username" className="text-sm font-semiblod">
Tìm bằng username
</Label>
<Input
id="username"
placeholder="Nhập username để kết bạn"
className="glass border-border/50 focus:border-primary/50 transition-smooth"
{...register("username", {
required: "Username không được bỏ trống",
})}
></Input>
{errors.username && (
<p className="text-sm text-destructive">{errors.username.message}</p>
)}

{isFound === false && (
<span className="text-sm text-destructive">
Không tìm thấy người dùng
<span className="font-semibold">@{searchedUsername}</span>
</span>
)}
</div>

<DialogFooter>
<DialogClose>
<Button
type="button"
variant={"outline"}
className={"flex-1 glass hover:text-destructive"}
onClick={onCancel}
>
Hủy
</Button>
</DialogClose>

<Button
type="submit"
disabled={loading || !usernameValue?.trim()}
className={
"flex-1 bg-gradient-chat text-white hover:opacity-90 transition-smooth "
}
>
{loading ? (
<>
<LoaderIcon
role="status"
aria-label="Loading"
className="size-4 animate-spin dark:text-white"
/>
</>
) : (
<>
<Search className="size-4 mr-2" />
</>
)}
</Button>
</DialogFooter>
</form>
);
};

export default SearchForm;
76 changes: 76 additions & 0 deletions frontend/src/components/AddFriendModal/SendFriendRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { UseFormRegister } from "react-hook-form";
import type { IFormValues } from "../chat/modals/AddFriendModal";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
import { DialogFooter } from "../ui/dialog";
import { Button } from "../ui/button";
import { LoaderIcon, UserPlus } from "lucide-react";

interface SendRequestProps {
register: UseFormRegister<IFormValues>;
loading: boolean;
searchedUserName: string;
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
onBack: () => void;
}
const SendFriendRequest = ({
register,
loading,
searchedUserName,
onSubmit,
onBack,
}: SendRequestProps) => {
return (
<form onSubmit={onSubmit}>
<div className="space-y-4">
<span className="success-message">
Đã tìm thấy<span className="font-semibold">@{searchedUserName}</span>
</span>
<div className="space-y-2">
<Label htmlFor="message" className="text-sm font-semibold">
Giới thiệu
</Label>
<Textarea
id="message"
rows={3}
placeholder="Chào bạn, có thể kết bạn được không?"
className="glass border-border/50 focus:border-primary/50 transition-smooth resize-none"
{...register("message")}
/>
</div>
<DialogFooter>
<Button
type="button"
variant={"outline"}
className={"flex-1 glass hover:text-destructive"}
onClick={onBack}
>
Quay lại
</Button>
<Button
type="submit"
disabled={loading}
className={
"flex-1 bg-gradient-chat text-white hover:opacity-90 transition-smooth"
}
>
{loading ? (
<>
<LoaderIcon
role="status"
aria-label="Loading"
className="size-4 animate-spin dark:text-white"
/>
</>
) : (
<>
<UserPlus className="size-4 mr-2" /> Kết bạn
</>
)}
</Button>
</DialogFooter>
</div>
</form>)
};

export default SendFriendRequest;
1 change: 0 additions & 1 deletion frontend/src/components/auth/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useAuthStore } from "@/stores/useAuthStore";
import { useEffect, useRef, useState } from "react";
import { Navigate, Outlet } from "react-router";
import { LoaderIcon } from "lucide-react";
import { cn } from "@/lib/utils";

const ProtectedRoute = () => {
const { accessToken, user, loading, refresh, fetchMe, clearState } =
Expand Down
126 changes: 122 additions & 4 deletions frontend/src/components/chat/modals/AddFriendModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,125 @@
import SearchForm from "@/components/AddFriendModal/SearchForm";
import SendFriendRequest from "@/components/AddFriendModal/SendFriendRequest";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useFriendStore } from "@/stores/useFriendRequest";
import type { User } from "@/types/user";
import { UserPlus } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";

export interface IFormValues {
username: string;
message: string;
}

const AddFriendModal = () => {
const [isFound, setIsFound] = useState<boolean | null>(null);
const [searchUser, setSearchUser] = useState<User>();
const [searchedUserName, setsearchedUserName] = useState("");
const { loading, searchByUserName, addFriend } = useFriendStore();

const {
register,
handleSubmit,
watch,
reset,
formState: { errors },
} = useForm<IFormValues>({
defaultValues: { username: "", message: "" },
});

const usernameValue = watch("username");

const handleSearch = handleSubmit(async (data) => {
const username = data.username.trim();
if (!username) return;

setIsFound(null);
setsearchedUserName(username);

try {
const foundUser = await searchByUserName(username);

if (foundUser) {
setIsFound(true);
setSearchUser(foundUser);
} else {
setIsFound(false);
}
} catch (error) {
console.error(error);
setIsFound(false);
}
});

const handleSend = handleSubmit(async (data) => {
if (!searchUser) return;
try {
const message = await addFriend(searchUser._id, data.message.trim());
toast.success(message);
handleCancel();
} catch (error) {
console.error("Lỗi xảy ra khi gửi request từ form:", error);
}
});

const handleCancel = () => {
reset();
setsearchedUserName("");
setIsFound(null);
};

return (
<div>AddFriendModal</div>
)
}
<Dialog>
<DialogTrigger>
<div
className="flex justify-center items-center size-5
rounded-full hover:bg-sidebar-accent cursor-pointer z-10"
>
<UserPlus className="size-4" />
<span className="sr-only">Kết bạn</span>
</div>
</DialogTrigger>

<DialogContent className={"sm:max-w-[425px] border-none"}>
<DialogHeader>
<DialogTitle>Kết bạn</DialogTitle>
</DialogHeader>
{!isFound && (
<>
<SearchForm
register={register}
errors={errors}
usernameValue={usernameValue}
loading={loading}
isFound={isFound}
searchedUsername={searchedUserName}
onSubmit={handleSearch}
onCancel={handleCancel}
/>
</>
)}
{isFound && (
<>
<SendFriendRequest
register={register}
loading={loading}
searchedUserName={searchedUserName}
onSubmit={handleSend}
onBack={() => setIsFound(null)}
/>
</>
)}
</DialogContent>
</Dialog>
);
};

export default AddFriendModal
export default AddFriendModal;
5 changes: 5 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@
dark:scrollbar-thumb-slate-600;
}

.success-message{
@apply text-sm text-emerald-500
}


/* Animations */
.transition-bounce {
transition: var(--transition-bounce);
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/services/friendService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import api from "@/lib/axios";

export const friendService = {

async searchByUserName(username:string){
const res = await api.get(`/users/search?username=${username}`);
return res.data.user;
},

async sendFriendRequest(to:string,message?:string){
const res = await api.post("/friend/request",{to,message});
return res.data.message;
}
}
Loading