ui/ux crud and stuff
This commit is contained in:
parent
094582aab7
commit
0342c0d295
@ -1,5 +1,12 @@
|
||||
import { ChatService } from "@/services/chatService.js";
|
||||
import { AuthenticatedRequest, CreateChatRoomRequest, GetMessagesQuery } from "@/types/index.js";
|
||||
import {
|
||||
AddChatRoomMemberRequest,
|
||||
AuthenticatedRequest,
|
||||
CreateChatRoomRequest,
|
||||
GetMessagesQuery,
|
||||
RemoveChatRoomMemberRequest,
|
||||
UpdateChatRoomRequest,
|
||||
} from "@/types/index.js";
|
||||
import type { Response } from "express";
|
||||
|
||||
export class ChatController {
|
||||
@ -40,4 +47,55 @@ export class ChatController {
|
||||
const result = await ChatService.getMessages(query);
|
||||
res.status(result.success ? 200 : 500).json(result);
|
||||
}
|
||||
|
||||
static async updateChatRoom(req: AuthenticatedRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = req.params.roomId;
|
||||
const data: UpdateChatRoomRequest = req.body;
|
||||
|
||||
const result = await ChatService.updateChatRoom(req.user.userId, roomId, data);
|
||||
res.status(result.success ? 200 : result.status || 400).json(result);
|
||||
}
|
||||
|
||||
static async deleteChatRoom(req: AuthenticatedRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = req.params.roomId;
|
||||
|
||||
const result = await ChatService.deleteChatRoom(req.user.userId, roomId);
|
||||
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||
}
|
||||
|
||||
static async addChatRoomMember(req: AuthenticatedRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = req.params.roomId;
|
||||
const data: AddChatRoomMemberRequest = req.body;
|
||||
|
||||
const result = await ChatService.addChatRoomMember(req.user.userId, roomId, data);
|
||||
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||
}
|
||||
|
||||
static async removeChatRoomMember(req: AuthenticatedRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomId = req.params.roomId;
|
||||
const data: RemoveChatRoomMemberRequest = req.body;
|
||||
|
||||
const result = await ChatService.removeChatRoomMember(req.user.userId, roomId, data);
|
||||
res.status(result.success ? 200 : result.status || 500).json(result);
|
||||
}
|
||||
}
|
||||
|
||||
17
backend/src/controllers/userController.ts
Normal file
17
backend/src/controllers/userController.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { UserService } from "@/services/userService";
|
||||
import { AuthenticatedRequest, UpdateUserRequest } from "@/types";
|
||||
import { Response } from "express";
|
||||
|
||||
export class UserController {
|
||||
static async updateProfile(req: AuthenticatedRequest, res: Response) {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
|
||||
const data: UpdateUserRequest = req.body;
|
||||
const result = await UserService.updateProfile(req.user.userId, data);
|
||||
|
||||
res.status(result.success ? 200 : 400).json(result);
|
||||
}
|
||||
}
|
||||
@ -13,19 +13,19 @@ const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
methods: ["GET", "POST"],
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
origin: "http://localhost:5173",
|
||||
},
|
||||
});
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
methods: ["GET", "POST"],
|
||||
methods: ["GET", "POST", "PUT", "DELETE"],
|
||||
origin: "http://localhost:5173",
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.json({ limit: "5mb" }));
|
||||
|
||||
app.use("/api", apiRoutes);
|
||||
|
||||
|
||||
@ -36,3 +36,23 @@ export const createChatRoomSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
memberUsernames: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
username: z.string().min(3, "Username must be at least 3 characters long").optional(),
|
||||
email: z.string().email("Invalid email format").optional(),
|
||||
password: z.string().min(6, "Password must be at least 6 characters long").optional(),
|
||||
avatar: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const updateChatRoomSchema = z.object({
|
||||
name: z.string().min(1, "Room name is required"),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export const addChatRoomMemberSchema = z.object({
|
||||
username: z.string().min(1, "Username is required"),
|
||||
});
|
||||
|
||||
export const removeChatRoomMemberSchema = z.object({
|
||||
userId: z.string().min(1, "User ID is required"),
|
||||
});
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { ChatController } from "@/controllers/chatController.js";
|
||||
import { authenticateToken } from "@/middleware/auth.js";
|
||||
import { createChatRoomSchema, validate } from "@/middleware/validation.js";
|
||||
import {
|
||||
addChatRoomMemberSchema,
|
||||
createChatRoomSchema,
|
||||
removeChatRoomMemberSchema,
|
||||
updateChatRoomSchema,
|
||||
validate,
|
||||
} from "@/middleware/validation.js";
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
@ -9,6 +15,10 @@ router.use(authenticateToken);
|
||||
|
||||
router.get("/chat-rooms", ChatController.getChatRooms);
|
||||
router.post("/chat-rooms", validate(createChatRoomSchema), ChatController.createChatRoom);
|
||||
router.put("/chat-rooms/:roomId", validate(updateChatRoomSchema), ChatController.updateChatRoom);
|
||||
router.delete("/chat-rooms/:roomId", ChatController.deleteChatRoom);
|
||||
router.post("/chat-rooms/:roomId/members", validate(addChatRoomMemberSchema), ChatController.addChatRoomMember);
|
||||
router.delete("/chat-rooms/:roomId/members", validate(removeChatRoomMemberSchema), ChatController.removeChatRoomMember);
|
||||
router.get("/messages/:roomId", ChatController.getMessages);
|
||||
|
||||
export { router as chatRoutes };
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { authRoutes } from "./authRoutes.js";
|
||||
import { chatRoutes } from "./chatRoutes.js";
|
||||
import { userRoutes } from "./userRoutes.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/auth", authRoutes);
|
||||
router.use("/users", userRoutes);
|
||||
router.use("/", chatRoutes);
|
||||
|
||||
export { router as apiRoutes };
|
||||
|
||||
11
backend/src/routes/userRoutes.ts
Normal file
11
backend/src/routes/userRoutes.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { UserController } from "@/controllers/userController.js";
|
||||
import { authenticateToken } from "@/middleware/auth.js";
|
||||
import { updateUserSchema, validate } from "@/middleware/validation.js";
|
||||
import { Router } from "express";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authenticateToken);
|
||||
router.put("/profile", validate(updateUserSchema), UserController.updateProfile);
|
||||
|
||||
export { router as userRoutes };
|
||||
@ -1,5 +1,12 @@
|
||||
import { prisma } from "@/config/database.js";
|
||||
import { ApiResponse, CreateChatRoomRequest, GetMessagesQuery } from "@/types/index.js";
|
||||
import {
|
||||
AddChatRoomMemberRequest,
|
||||
ApiResponse,
|
||||
CreateChatRoomRequest,
|
||||
GetMessagesQuery,
|
||||
RemoveChatRoomMemberRequest,
|
||||
UpdateChatRoomRequest,
|
||||
} from "@/types/index.js";
|
||||
|
||||
export class ChatService {
|
||||
static async getChatRooms(userId: string): Promise<ApiResponse> {
|
||||
@ -201,4 +208,323 @@ export class ChatService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async updateChatRoom(userId: string, roomId: string, data: UpdateChatRoomRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
const membership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "You are not a member of this chat room",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
if (membership.role !== "admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Only admins can update chat room settings",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
const updatedRoom = await prisma.chatRoom.update({
|
||||
where: { id: roomId },
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isOnline: true,
|
||||
lastSeen: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
messages: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRoom,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Update chat room error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to update chat room",
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static async deleteChatRoom(userId: string, roomId: string): Promise<ApiResponse> {
|
||||
try {
|
||||
const membership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "You are not a member of this chat room",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
if (membership.role !== "admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Only admins can delete chat rooms",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.chatRoom.delete({
|
||||
where: { id: roomId },
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { id: roomId },
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Delete chat room error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to delete chat room",
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static async addChatRoomMember(userId: string, roomId: string, data: AddChatRoomMemberRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
const membership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "You are not a member of this chat room",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
if (membership.role !== "admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Only admins can add members",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username: data.username },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: "User not found",
|
||||
status: 404,
|
||||
};
|
||||
}
|
||||
|
||||
const existingMembership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId: user.id,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMembership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "User is already a member of this chat room",
|
||||
status: 409,
|
||||
};
|
||||
}
|
||||
|
||||
await prisma.chatRoomMember.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
roomId,
|
||||
role: "member",
|
||||
},
|
||||
});
|
||||
|
||||
const updatedRoom = await prisma.chatRoom.findUnique({
|
||||
where: { id: roomId },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isOnline: true,
|
||||
lastSeen: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
messages: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRoom,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Add chat room member error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to add member to chat room",
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static async removeChatRoomMember(userId: string, roomId: string, data: RemoveChatRoomMemberRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
const membership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "You are not a member of this chat room",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
if (membership.role !== "admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Only admins can remove members",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if target user is an admin
|
||||
const targetMembership = await prisma.chatRoomMember.findUnique({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId: data.userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetMembership) {
|
||||
return {
|
||||
success: false,
|
||||
error: "User is not a member of this chat room",
|
||||
status: 404,
|
||||
};
|
||||
}
|
||||
|
||||
if (targetMembership.role === "admin") {
|
||||
return {
|
||||
success: false,
|
||||
error: "Cannot remove an admin from the chat room",
|
||||
status: 403,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove user from chat room
|
||||
await prisma.chatRoomMember.delete({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
userId: data.userId,
|
||||
roomId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get updated chat room
|
||||
const updatedRoom = await prisma.chatRoom.findUnique({
|
||||
where: { id: roomId },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
isOnline: true,
|
||||
lastSeen: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
messages: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedRoom,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Remove chat room member error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to remove member from chat room",
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
backend/src/services/userService.ts
Normal file
72
backend/src/services/userService.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { prisma } from "@/config/database.js";
|
||||
import { ApiResponse, UpdateUserRequest } from "@/types/index.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export class UserService {
|
||||
static async updateProfile(userId: string, data: UpdateUserRequest): Promise<ApiResponse> {
|
||||
try {
|
||||
if (data.username) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: data.username,
|
||||
id: { not: userId },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Username already taken",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (data.email) {
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: data.email,
|
||||
id: { not: userId },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Email already taken",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Partial<UpdateUserRequest> = {};
|
||||
if (data.username) updateData.username = data.username;
|
||||
if (data.email) updateData.email = data.email;
|
||||
if (data.avatar !== undefined) updateData.avatar = data.avatar;
|
||||
|
||||
if (data.password) {
|
||||
updateData.password = await bcrypt.hash(data.password, 12);
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Update profile error:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to update profile",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,7 @@ export type ApiResponse<T = any> = {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
status?: number;
|
||||
};
|
||||
|
||||
export type CreateChatRoomRequest = {
|
||||
@ -50,6 +51,13 @@ export type CreateChatRoomRequest = {
|
||||
memberUsernames: string[];
|
||||
};
|
||||
|
||||
export type UpdateUserRequest = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
avatar?: string | null;
|
||||
};
|
||||
|
||||
export type PaginationQuery = {
|
||||
page?: string;
|
||||
limit?: string;
|
||||
@ -69,3 +77,16 @@ export type LoginRequest = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UpdateChatRoomRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type AddChatRoomMemberRequest = {
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type RemoveChatRoomMemberRequest = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
276
frontend/src/components/ChatRoomSettingsDialog.tsx
Normal file
276
frontend/src/components/ChatRoomSettingsDialog.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { chatAPI } from '@/lib/api';
|
||||
import { useChatStore } from '@/stores/chatStore';
|
||||
import type { ChatRoom, UpdateChatRoomRequest } from '@/types';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Trash2, UserMinus, UserPlus } from 'lucide-react';
|
||||
import { useState, type FC, type FormEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from './ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Textarea } from './ui/textarea';
|
||||
|
||||
type ChatRoomSettingsDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
room: ChatRoom | null;
|
||||
};
|
||||
|
||||
export const ChatRoomSettingsDialog: FC<ChatRoomSettingsDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
room,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { updateChatRoom, removeChatRoom } = useChatStore();
|
||||
const [newMemberUsername, setNewMemberUsername] = useState('');
|
||||
const [formData, setFormData] = useState<UpdateChatRoomRequest>({
|
||||
name: room?.name || '',
|
||||
description: room?.description || '',
|
||||
});
|
||||
|
||||
useState(() => {
|
||||
if (room) {
|
||||
setFormData({
|
||||
name: room.name,
|
||||
description: room.description || '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const updateRoomMutation = useMutation({
|
||||
mutationKey: ['updateChatRoom'],
|
||||
mutationFn: (data: UpdateChatRoomRequest) =>
|
||||
chatAPI.updateChatRoom(room?.id || '', data),
|
||||
onSuccess: (response) => {
|
||||
if (response.data.success) {
|
||||
updateChatRoom(response.data.data);
|
||||
toast.success('Chat room updated successfully');
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to update chat room');
|
||||
},
|
||||
});
|
||||
|
||||
const addMemberMutation = useMutation({
|
||||
mutationKey: ['addMember'],
|
||||
mutationFn: (username: string) =>
|
||||
chatAPI.addChatRoomMember(room?.id || '', { username }),
|
||||
onSuccess: (response) => {
|
||||
if (response.data.success) {
|
||||
updateChatRoom(response.data.data);
|
||||
setNewMemberUsername('');
|
||||
toast.success('Member added successfully');
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to add member');
|
||||
},
|
||||
});
|
||||
|
||||
const removeMemberMutation = useMutation({
|
||||
mutationKey: ['removeMember'],
|
||||
mutationFn: (userId: string) =>
|
||||
chatAPI.removeChatRoomMember(room?.id || '', { userId }),
|
||||
onSuccess: (response) => {
|
||||
if (response.data.success) {
|
||||
updateChatRoom(response.data.data);
|
||||
toast.success('Member removed successfully');
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to remove member');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteRoomMutation = useMutation({
|
||||
mutationKey: ['deleteChatRoom'],
|
||||
mutationFn: () => chatAPI.deleteChatRoom(room?.id || ''),
|
||||
onSuccess: (response) => {
|
||||
if (response.data.success) {
|
||||
removeChatRoom(room?.id || '');
|
||||
onOpenChange(false);
|
||||
navigate('/');
|
||||
toast.success('Chat room deleted successfully');
|
||||
queryClient.invalidateQueries({ queryKey: ['chatRooms'] });
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error.response?.data?.error || 'Failed to delete chat room');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (room) {
|
||||
updateRoomMutation.mutate(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMember = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (room && newMemberUsername) {
|
||||
addMemberMutation.mutate(newMemberUsername);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveMember = (userId: string) => {
|
||||
if (room) {
|
||||
removeMemberMutation.mutate(userId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRoom = () => {
|
||||
if (
|
||||
window.confirm(
|
||||
'Are you sure you want to delete this chat room? This action cannot be undone.'
|
||||
)
|
||||
) {
|
||||
deleteRoomMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
if (!room) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Room Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Room Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
placeholder="Room description (optional)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateRoomMutation.isPending}>
|
||||
{updateRoomMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="space-y-4 pt-4 border-t">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-2">
|
||||
Members ({room.members.length})
|
||||
</h3>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
{room.members.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-full bg-blue-500 text-white flex items-center justify-center text-xs">
|
||||
{member.user.avatar ? (
|
||||
<img
|
||||
src={member.user.avatar}
|
||||
alt={member.user.username}
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
) : (
|
||||
member.user.username.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm">{member.user.username}</span>
|
||||
{member.role === 'admin' && (
|
||||
<span className="text-xs text-gray-500">(admin)</span>
|
||||
)}
|
||||
</div>
|
||||
{member.role !== 'admin' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveMember(member.userId)}
|
||||
disabled={removeMemberMutation.isPending}
|
||||
>
|
||||
<UserMinus className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddMember} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Add member by username"
|
||||
value={newMemberUsername}
|
||||
onChange={(e) => setNewMemberUsername(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={!newMemberUsername || addMemberMutation.isPending}
|
||||
>
|
||||
<UserPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={handleDeleteRoom}
|
||||
disabled={deleteRoomMutation.isPending}
|
||||
>
|
||||
{deleteRoomMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Deleting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Room
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -18,6 +18,7 @@ import { useTheme } from 'next-themes';
|
||||
import { useState, type FC, type FormEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { UserSettingsDialog } from './SettingsDialog';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
@ -44,6 +45,7 @@ export const ChatSidebar: FC<ChatSidebarProps> = ({
|
||||
}) => {
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
|
||||
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
|
||||
const [newRoomData, setNewRoomData] = useState<CreateChatRoomRequest>({
|
||||
name: '',
|
||||
description: '',
|
||||
@ -240,16 +242,25 @@ export const ChatSidebar: FC<ChatSidebarProps> = ({
|
||||
{room.members.slice(0, 3).map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`w-4 h-4 rounded-full border border-white dark:border-gray-800 flex items-center justify-center text-xs font-medium ${
|
||||
onlineUsers.has(member.userId)
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-300 text-gray-600 dark:bg-gray-500 dark:text-gray-300'
|
||||
}`}
|
||||
className="relative w-4 h-4"
|
||||
title={`${member.user.username} - ${
|
||||
onlineUsers.has(member.userId) ? 'Online' : 'Offline'
|
||||
}`}
|
||||
>
|
||||
{member.user.username.charAt(0).toUpperCase()}
|
||||
<div className="w-4 h-4 rounded-full border border-white dark:border-gray-800 flex items-center justify-center text-xs font-medium bg-gray-300 dark:bg-gray-600 overflow-hidden">
|
||||
{member.user.avatar ? (
|
||||
<img
|
||||
src={member.user.avatar}
|
||||
alt={member.user.username}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
member.user.username.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
{onlineUsers.has(member.userId) && (
|
||||
<div className="absolute bottom-0 right-0 w-1.5 h-1.5 bg-green-500 border border-white dark:border-gray-800 rounded-full z-10"></div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{room.members.length > 3 && (
|
||||
@ -267,16 +278,19 @@ export const ChatSidebar: FC<ChatSidebarProps> = ({
|
||||
<Popover open={isUserMenuOpen} onOpenChange={setIsUserMenuOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex items-center space-x-3 cursor-pointer p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center">
|
||||
{user?.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user?.username}
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<User className="h-6 w-6" />
|
||||
)}
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center">
|
||||
{user?.avatar ? (
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user?.username}
|
||||
className="w-full h-full rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<User className="h-6 w-6" />
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white dark:border-gray-800 rounded-full"></div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm dark:text-white">
|
||||
@ -330,7 +344,7 @@ export const ChatSidebar: FC<ChatSidebarProps> = ({
|
||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setIsUserMenuOpen(false);
|
||||
// settings functionality TODO
|
||||
setIsSettingsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
@ -347,6 +361,10 @@ export const ChatSidebar: FC<ChatSidebarProps> = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<UserSettingsDialog
|
||||
open={isSettingsDialogOpen}
|
||||
onOpenChange={setIsSettingsDialogOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { useChatStore } from '@/stores/chatStore';
|
||||
import { Settings, Users } from 'lucide-react';
|
||||
import { type FC } from 'react';
|
||||
import { useState, type FC } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { ChatRoomSettingsDialog } from './ChatRoomSettingsDialog';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export const NavBar: FC = () => {
|
||||
const { chatRooms, onlineUsers } = useChatStore();
|
||||
const { roomId } = useParams<{ roomId: string }>();
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
const currentRoom = chatRooms.find((room) => room.id === roomId);
|
||||
|
||||
@ -24,14 +26,18 @@ export const NavBar: FC = () => {
|
||||
{currentRoom ? (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold dark:text-white">{currentRoom.name}</h1>
|
||||
<h1 className="text-lg font-semibold dark:text-white">
|
||||
{currentRoom.name}
|
||||
</h1>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Users className="h-3 w-3 mr-1" />
|
||||
{getOnlineMembersCount()} / {currentRoom.members.length} online
|
||||
</Badge>
|
||||
</div>
|
||||
{currentRoom.description && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">{currentRoom.description}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{currentRoom.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
@ -40,11 +46,23 @@ export const NavBar: FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
{currentRoom && (
|
||||
<Button size="sm" variant="ghost">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentRoom && (
|
||||
<ChatRoomSettingsDialog
|
||||
open={isSettingsOpen}
|
||||
onOpenChange={setIsSettingsOpen}
|
||||
room={currentRoom}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
220
frontend/src/components/SettingsDialog.tsx
Normal file
220
frontend/src/components/SettingsDialog.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import { userAPI } from '@/lib/api';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { useChatStore } from '@/stores/chatStore';
|
||||
import type { UpdateUserRequest } from '@/types';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Camera, Loader2 } from 'lucide-react';
|
||||
import { useState, type FC, type FormEvent } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from './ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
|
||||
type UserSettingsDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const UserSettingsDialog: FC<UserSettingsDialogProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}) => {
|
||||
const { user, updateUser } = useAuthStore();
|
||||
const { updateUserInRooms } = useChatStore();
|
||||
const [formData, setFormData] = useState<UpdateUserRequest>({
|
||||
username: user?.username || '',
|
||||
email: user?.email || '',
|
||||
password: '',
|
||||
avatar: user?.avatar,
|
||||
});
|
||||
const [previewAvatar, setPreviewAvatar] = useState<string | null>(
|
||||
user?.avatar || null
|
||||
);
|
||||
|
||||
const updateProfileMutation = useMutation({
|
||||
mutationFn: userAPI.updateProfile,
|
||||
onSuccess: (response) => {
|
||||
if (response.data.success && response.data.data) {
|
||||
const updatedUser = response.data.data;
|
||||
updateUser(updatedUser);
|
||||
|
||||
if (user) {
|
||||
updateUserInRooms(user.id, updatedUser);
|
||||
}
|
||||
|
||||
toast.success('Profile updated successfully');
|
||||
onOpenChange(false);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onError: (error: any) => {
|
||||
toast.error(
|
||||
'Failed to update profile: ' +
|
||||
(error.response?.data?.error || 'Unknown error')
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const updateData: Partial<UpdateUserRequest> = {};
|
||||
if (formData.username && formData.username !== user?.username) {
|
||||
updateData.username = formData.username;
|
||||
}
|
||||
if (formData.email && formData.email !== user?.email) {
|
||||
updateData.email = formData.email;
|
||||
}
|
||||
if (formData.password) {
|
||||
updateData.password = formData.password;
|
||||
}
|
||||
if (formData.avatar !== user?.avatar) {
|
||||
updateData.avatar = formData.avatar;
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
updateProfileMutation.mutate(updateData);
|
||||
} else {
|
||||
toast.info('No changes to save');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > 1024 * 1024) {
|
||||
toast.error('Image too large. Please select an image under 1MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64 = event.target?.result as string;
|
||||
setPreviewAvatar(base64);
|
||||
setFormData({ ...formData, avatar: base64 });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const handleRemoveAvatar = () => {
|
||||
setPreviewAvatar(null);
|
||||
setFormData({ ...formData, avatar: null });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 py-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<div className="relative">
|
||||
<div className="w-24 h-24 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden border-2 border-gray-300">
|
||||
{previewAvatar ? (
|
||||
<img
|
||||
src={previewAvatar}
|
||||
alt="Avatar preview"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-2xl font-semibold text-gray-500">
|
||||
{formData.username?.charAt(0).toUpperCase() || '?'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<label
|
||||
htmlFor="avatar-upload"
|
||||
className="absolute bottom-0 right-0 bg-blue-500 hover:bg-blue-600 text-white p-1.5 rounded-full cursor-pointer transition-colors"
|
||||
>
|
||||
<Camera className="h-4 w-4" />
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="avatar-upload"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{previewAvatar && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRemoveAvatar}
|
||||
>
|
||||
Remove Photo
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, username: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, email: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">
|
||||
New Password (leave empty to keep current)
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, password: e.target.value })
|
||||
}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateProfileMutation.isPending}>
|
||||
{updateProfileMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
'Save Changes'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -1,9 +1,13 @@
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import type {
|
||||
AddChatRoomMemberRequest,
|
||||
ApiResponse,
|
||||
CreateChatRoomRequest,
|
||||
LoginRequest,
|
||||
RegisterRequest,
|
||||
RemoveChatRoomMemberRequest,
|
||||
UpdateChatRoomRequest,
|
||||
UpdateUserRequest,
|
||||
} from '@/types';
|
||||
import axios, { type AxiosResponse } from 'axios';
|
||||
|
||||
@ -50,6 +54,30 @@ export const chatAPI = {
|
||||
limit = 50
|
||||
): Promise<AxiosResponse<ApiResponse>> =>
|
||||
api.get(`/messages/${roomId}?page=${page}&limit=${limit}`),
|
||||
// Add these new endpoints
|
||||
updateChatRoom: (
|
||||
roomId: string,
|
||||
data: UpdateChatRoomRequest
|
||||
): Promise<AxiosResponse<ApiResponse>> =>
|
||||
api.put(`/chat-rooms/${roomId}`, data),
|
||||
deleteChatRoom: (roomId: string): Promise<AxiosResponse<ApiResponse>> =>
|
||||
api.delete(`/chat-rooms/${roomId}`),
|
||||
addChatRoomMember: (
|
||||
roomId: string,
|
||||
data: AddChatRoomMemberRequest
|
||||
): Promise<AxiosResponse<ApiResponse>> =>
|
||||
api.post(`/chat-rooms/${roomId}/members`, data),
|
||||
removeChatRoomMember: (
|
||||
roomId: string,
|
||||
data: RemoveChatRoomMemberRequest
|
||||
): Promise<AxiosResponse<ApiResponse>> =>
|
||||
api.delete(`/chat-rooms/${roomId}/members`, { data }),
|
||||
};
|
||||
|
||||
export const userAPI = {
|
||||
updateProfile: (
|
||||
data: UpdateUserRequest
|
||||
): Promise<AxiosResponse<ApiResponse>> => api.put('/users/profile', data),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ChatRoom, Message, MessageReaction } from '@/types';
|
||||
import type { ChatRoom, Message, MessageReaction, User } from '@/types';
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
@ -11,6 +11,8 @@ type ChatState = {
|
||||
|
||||
setChatRooms: (rooms: ChatRoom[]) => void;
|
||||
addChatRoom: (room: ChatRoom) => void;
|
||||
updateChatRoom: (room: ChatRoom) => void;
|
||||
removeChatRoom: (roomId: string) => void;
|
||||
setSelectedRoom: (roomId: string | null) => void;
|
||||
setMessages: (roomId: string, messages: Message[]) => void;
|
||||
addMessage: (message: Message) => void;
|
||||
@ -24,6 +26,7 @@ type ChatState = {
|
||||
addTypingUser: (roomId: string, username: string) => void;
|
||||
removeTypingUser: (roomId: string, username: string) => void;
|
||||
updateMessageSeen: (messageId: string, seenBy: string[]) => void;
|
||||
updateUserInRooms: (userId: string, userData: Partial<User>) => void;
|
||||
};
|
||||
|
||||
export const useChatStore = create<ChatState>()(
|
||||
@ -41,6 +44,7 @@ export const useChatStore = create<ChatState>()(
|
||||
addChatRoom: (room) => {
|
||||
set((state) => ({
|
||||
chatRooms: [room, ...state.chatRooms],
|
||||
selectedRoomId: room.id,
|
||||
}));
|
||||
},
|
||||
|
||||
@ -147,5 +151,45 @@ export const useChatStore = create<ChatState>()(
|
||||
return { messages: newMessages };
|
||||
});
|
||||
},
|
||||
|
||||
updateUserInRooms: (userId: string, userData: Partial<User>) => {
|
||||
set((state) => {
|
||||
const updatedRooms = state.chatRooms.map((room) => ({
|
||||
...room,
|
||||
members: room.members.map((member) =>
|
||||
member.userId === userId
|
||||
? {
|
||||
...member,
|
||||
user: { ...member.user, ...userData },
|
||||
}
|
||||
: member
|
||||
),
|
||||
}));
|
||||
|
||||
return { chatRooms: updatedRooms };
|
||||
});
|
||||
},
|
||||
|
||||
updateChatRoom: (room) => {
|
||||
set((state) => ({
|
||||
chatRooms: state.chatRooms.map((r) => (r.id === room.id ? room : r)),
|
||||
}));
|
||||
},
|
||||
|
||||
removeChatRoom: (roomId) => {
|
||||
set((state) => {
|
||||
const newMessages = { ...state.messages };
|
||||
delete newMessages[roomId];
|
||||
|
||||
return {
|
||||
chatRooms: state.chatRooms.filter((r) => r.id !== roomId),
|
||||
selectedRoomId:
|
||||
state.selectedRoomId === roomId ? null : state.selectedRoomId,
|
||||
messages: newMessages,
|
||||
onlineUsers: state.onlineUsers,
|
||||
typingUsers: state.typingUsers,
|
||||
};
|
||||
});
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
@ -67,8 +67,28 @@ export type RegisterRequest = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UpdateUserRequest = {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
avatar?: string | null;
|
||||
};
|
||||
|
||||
export type CreateChatRoomRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
memberUsernames: string[];
|
||||
};
|
||||
|
||||
export type UpdateChatRoomRequest = {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type AddChatRoomMemberRequest = {
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type RemoveChatRoomMemberRequest = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user