feat: initial JaniChat messenger — PWA, WebSocket, admin panel

This commit is contained in:
Ai
2026-05-22 11:06:43 +03:00
commit 1cabe9d04f
46 changed files with 3570 additions and 0 deletions

121
frontend/src/store/index.ts Normal file
View File

@@ -0,0 +1,121 @@
import { create } from 'zustand';
import { Chat, Message, User } from '../types';
interface AppStore {
user: User | null;
chats: Chat[];
activeChat: Chat | null;
messages: Record<string, Message[]>;
onlineUsers: Set<string>;
typingUsers: Record<string, string[]>;
connected: boolean;
setUser: (user: User | null) => void;
setChats: (chats: Chat[]) => void;
updateChat: (chat: Partial<Chat> & { id: string }) => void;
setActiveChat: (chat: Chat | null) => void;
setMessages: (chatId: string, messages: Message[]) => void;
prependMessages: (chatId: string, messages: Message[]) => void;
addMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
removeMessage: (chatId: string, messageId: string) => void;
setOnline: (userId: string, online: boolean) => void;
setTyping: (chatId: string, userId: string, typing: boolean) => void;
setConnected: (v: boolean) => void;
markRead: (chatId: string) => void;
logout: () => void;
}
export const useStore = create<AppStore>((set, get) => ({
user: null,
chats: [],
activeChat: null,
messages: {},
onlineUsers: new Set(),
typingUsers: {},
connected: false,
setUser: (user) => set({ user }),
setChats: (chats) => set({ chats }),
updateChat: (partial) => set(state => ({
chats: state.chats.map(c => c.id === partial.id ? { ...c, ...partial } : c),
activeChat: state.activeChat?.id === partial.id ? { ...state.activeChat, ...partial } : state.activeChat,
})),
setActiveChat: (chat) => set({ activeChat: chat }),
setMessages: (chatId, messages) => set(state => ({
messages: { ...state.messages, [chatId]: messages }
})),
prependMessages: (chatId, messages) => set(state => ({
messages: {
...state.messages,
[chatId]: [...messages, ...(state.messages[chatId] || [])]
}
})),
addMessage: (message) => set(state => {
const existing = state.messages[message.chatId] || [];
if (existing.find(m => m.id === message.id)) return state;
const updated = [...existing, message];
// Update chat last message
const chats = state.chats.map(c =>
c.id === message.chatId
? { ...c, lastMessage: message.content, lastMessageAt: message.createdAt,
unreadCount: state.activeChat?.id === message.chatId ? 0 : c.unreadCount + 1 }
: c
).sort((a, b) => {
const ta = a.lastMessageAt || a.createdAt;
const tb = b.lastMessageAt || b.createdAt;
return new Date(tb).getTime() - new Date(ta).getTime();
});
return { messages: { ...state.messages, [message.chatId]: updated }, chats };
}),
updateMessage: (message) => set(state => ({
messages: {
...state.messages,
[message.chatId]: (state.messages[message.chatId] || []).map(m =>
m.id === message.id ? message : m
)
}
})),
removeMessage: (chatId, messageId) => set(state => ({
messages: {
...state.messages,
[chatId]: (state.messages[chatId] || []).map(m =>
m.id === messageId ? { ...m, isDeleted: true, content: 'Сообщение удалено' } : m
)
}
})),
setOnline: (userId, online) => set(state => {
const next = new Set(state.onlineUsers);
online ? next.add(userId) : next.delete(userId);
return { onlineUsers: next };
}),
setTyping: (chatId, userId, typing) => set(state => {
const current = state.typingUsers[chatId] || [];
const next = typing
? [...new Set([...current, userId])]
: current.filter(id => id !== userId);
return { typingUsers: { ...state.typingUsers, [chatId]: next } };
}),
setConnected: (v) => set({ connected: v }),
markRead: (chatId) => set(state => ({
chats: state.chats.map(c => c.id === chatId ? { ...c, unreadCount: 0 } : c)
})),
logout: () => {
localStorage.removeItem('jc_token');
localStorage.removeItem('jc_user');
set({ user: null, chats: [], activeChat: null, messages: {}, connected: false });
},
}));