add group chat
This commit is contained in:
@@ -5,6 +5,7 @@ import upload from './upload.svg';
|
||||
import chevroneDropDownList from './chevron-drop-down.svg';
|
||||
import checkMark from './check-mark.svg';
|
||||
import Edit from './edit.svg';
|
||||
import Send from './send.svg';
|
||||
|
||||
export {
|
||||
Edit,
|
||||
@@ -14,4 +15,5 @@ export {
|
||||
upload,
|
||||
chevroneDropDownList,
|
||||
checkMark,
|
||||
Send,
|
||||
};
|
||||
|
||||
3
src/assets/icons/input/send.svg
Normal file
3
src/assets/icons/input/send.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.4045 11.5763L6.32338 11.5763M5.06681 2.74198L20.8828 10.4103C21.8567 10.8826 21.8567 12.2701 20.8828 12.7423L5.06681 20.4107C3.98332 20.936 2.83166 19.8284 3.3143 18.7253L6.21474 12.0957C6.3596 11.7646 6.3596 11.388 6.21474 11.0569L3.3143 4.42737C2.83167 3.32419 3.98332 2.21665 5.06681 2.74198Z" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 472 B |
@@ -44,7 +44,6 @@ export const DropDownList: React.FC<DropDownListProps> = ({
|
||||
setValue(defaultState != undefined ? defaultState : items[0]);
|
||||
}, [defaultState]);
|
||||
|
||||
console.log(defaultState, items);
|
||||
return (
|
||||
<div className={cn('relative', className)} ref={ref}>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { Modal } from './Modal';
|
||||
import { PrimaryButton } from '../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../components/button/SecondaryButton';
|
||||
|
||||
@@ -99,7 +99,9 @@ const ContestEditor = () => {
|
||||
}));
|
||||
setMissionIdInput('');
|
||||
})
|
||||
.catch((err) => {});
|
||||
.catch((err) => {
|
||||
err;
|
||||
});
|
||||
};
|
||||
|
||||
const removeMission = (removeId: number) => {
|
||||
|
||||
@@ -19,7 +19,8 @@ export interface ChatMessage {
|
||||
interface FetchMessagesParams {
|
||||
groupId: number;
|
||||
limit?: number;
|
||||
afterMessageId?: number | null;
|
||||
afterMessageId?: number;
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
interface SendMessageParams {
|
||||
@@ -33,8 +34,7 @@ interface SendMessageParams {
|
||||
|
||||
interface GroupChatState {
|
||||
messages: Record<number, ChatMessage[]>; // по группам
|
||||
hasMore: Record<number, boolean>; // есть ли ещё старые сообщения
|
||||
isInitialLoaded: Record<number, boolean>; // загружена ли первая порция
|
||||
lastMessage: Record<number, number>;
|
||||
|
||||
fetchMessages: {
|
||||
status: Status;
|
||||
@@ -48,8 +48,7 @@ interface GroupChatState {
|
||||
|
||||
const initialState: GroupChatState = {
|
||||
messages: {},
|
||||
hasMore: {},
|
||||
isInitialLoaded: {},
|
||||
lastMessage: {},
|
||||
fetchMessages: {
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
@@ -73,13 +72,13 @@ export const fetchGroupMessages = createAsyncThunk(
|
||||
params: {
|
||||
limit: params.limit,
|
||||
afterMessageId: params.afterMessageId,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
groupId: params.groupId,
|
||||
messages: response.data as ChatMessage[],
|
||||
afterMessageId: params.afterMessageId,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
@@ -117,8 +116,18 @@ const groupChatSlice = createSlice({
|
||||
reducers: {
|
||||
clearChat(state, action: PayloadAction<number>) {
|
||||
delete state.messages[action.payload];
|
||||
delete state.hasMore[action.payload];
|
||||
delete state.isInitialLoaded[action.payload];
|
||||
},
|
||||
setGroupChatStatus: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
key: keyof GroupChatState;
|
||||
status: Status;
|
||||
}>,
|
||||
) => {
|
||||
const { key, status } = action.payload;
|
||||
if (state[key]) {
|
||||
(state[key] as any).status = status;
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -134,28 +143,22 @@ const groupChatSlice = createSlice({
|
||||
action: PayloadAction<{
|
||||
groupId: number;
|
||||
messages: ChatMessage[];
|
||||
afterMessageId?: number | null;
|
||||
}>,
|
||||
) => {
|
||||
const { groupId, messages, afterMessageId } = action.payload;
|
||||
const { groupId, messages } = action.payload;
|
||||
const existing = state.messages[groupId] || [];
|
||||
|
||||
// первичная загрузка
|
||||
if (!afterMessageId) {
|
||||
state.messages[groupId] = messages;
|
||||
state.isInitialLoaded[groupId] = true;
|
||||
state.hasMore[groupId] = messages.length > 0;
|
||||
}
|
||||
// догружаем старые (scroll up)
|
||||
else if (afterMessageId) {
|
||||
const ids = new Set(existing.map((m) => m.id));
|
||||
const filtered = messages.filter((m) => !ids.has(m.id));
|
||||
console.log('messages', messages);
|
||||
console.log('filtered', filtered);
|
||||
console.log('ids', ids);
|
||||
console.log('existing', existing);
|
||||
state.messages[groupId] = [...existing, ...filtered];
|
||||
state.hasMore[groupId] = filtered.length > 0;
|
||||
|
||||
state.messages[groupId] = [...existing, ...filtered].sort(
|
||||
(a, b) => a.id - b.id,
|
||||
);
|
||||
if (state.messages[groupId].length) {
|
||||
state.lastMessage[groupId] =
|
||||
state.messages[groupId][
|
||||
state.messages[groupId].length - 1
|
||||
].id;
|
||||
}
|
||||
|
||||
state.fetchMessages.status = 'successful';
|
||||
@@ -190,5 +193,5 @@ const groupChatSlice = createSlice({
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearChat } = groupChatSlice.actions;
|
||||
export const { clearChat, setGroupChatStatus } = groupChatSlice.actions;
|
||||
export const groupChatReducer = groupChatSlice.reducer;
|
||||
|
||||
@@ -68,14 +68,14 @@ function greet(user: User) {
|
||||
return \`Привет, \${user.name}! 👋 Роль: \${user.role}\`;
|
||||
}
|
||||
|
||||
console.log(greet({ name: "Ты", role: "Разработчик" }));
|
||||
consol.log(greet({ name: "Ты", role: "Разработчик" }));
|
||||
\`\`\`
|
||||
|
||||
Пример **JavaScript**:
|
||||
|
||||
\`\`\`js
|
||||
const sum = (a, b) => a + b;
|
||||
console.log(sum(2, 3)); // 5
|
||||
consol.log(sum(2, 3)); // 5
|
||||
\`\`\`
|
||||
|
||||
Пример **Python**:
|
||||
@@ -256,9 +256,7 @@ const MarkdownEditor: FC<MarkdownEditorProps> = ({
|
||||
markdown.slice(cursorPos);
|
||||
|
||||
setMarkdown(newText);
|
||||
} catch (err) {
|
||||
console.error('Ошибка загрузки изображения:', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,13 +36,17 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => {}}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => {}}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import MissionItem from "./MissionItem";
|
||||
import { FC, useEffect } from 'react';
|
||||
import MissionItem from './MissionItem';
|
||||
import {
|
||||
Contest,
|
||||
fetchMySubmissions,
|
||||
setContestStatus,
|
||||
} from "../../../redux/slices/contests";
|
||||
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
|
||||
import { PrimaryButton } from "../../../components/button/PrimaryButton";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { arrowLeft } from "../../../assets/icons/header";
|
||||
} from '../../../redux/slices/contests';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { arrowLeft } from '../../../assets/icons/header';
|
||||
|
||||
export interface Article {
|
||||
id: number;
|
||||
@@ -24,7 +24,7 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { submissions, status } = useAppSelector(
|
||||
(state) => state.contests.fetchMySubmissions
|
||||
(state) => state.contests.fetchMySubmissions,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,8 +32,10 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||
}, [contest]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status == "successful") {
|
||||
dispatch(setContestStatus({ key: "fetchMySubmissions", status: "idle" }));
|
||||
if (status == 'successful') {
|
||||
dispatch(
|
||||
setContestStatus({ key: 'fetchMySubmissions', status: 'idle' }),
|
||||
);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
@@ -45,8 +47,8 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||
submissions.some(
|
||||
(s) =>
|
||||
s.solution.missionId === mission.id &&
|
||||
s.solution.status === "Accepted: All tests passed"
|
||||
)
|
||||
s.solution.status === 'Accepted: All tests passed',
|
||||
),
|
||||
).length;
|
||||
|
||||
const totalCount = contest.missions?.length ?? 0;
|
||||
@@ -87,19 +89,19 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||
<div className="w-full">
|
||||
{(contest.missions ?? []).map((v, i) => {
|
||||
const missionSubmissions = submissions.filter(
|
||||
(s) => s.solution.missionId === v.id
|
||||
(s) => s.solution.missionId === v.id,
|
||||
);
|
||||
|
||||
const hasSuccess = missionSubmissions.some(
|
||||
(s) => s.solution.status == "Accepted: All tests passed"
|
||||
(s) =>
|
||||
s.solution.status ==
|
||||
'Accepted: All tests passed',
|
||||
);
|
||||
|
||||
console.log(missionSubmissions);
|
||||
|
||||
const status = hasSuccess
|
||||
? "success"
|
||||
? 'success'
|
||||
: missionSubmissions.length > 0
|
||||
? "error"
|
||||
? 'error'
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
@@ -111,7 +113,7 @@ const ContestMissions: FC<ContestMissionsProps> = ({ contest }) => {
|
||||
timeLimit={v.timeLimitMilliseconds}
|
||||
memoryLimit={v.memoryLimitBytes}
|
||||
status={status}
|
||||
type={i % 2 ? "second" : "first"}
|
||||
type={i % 2 ? 'second' : 'first'}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -36,13 +36,17 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => console.log(v)}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => console.log(values)}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import { fetchGroupById } from '../../../redux/slices/groups';
|
||||
import GroupMenu from './GroupMenu';
|
||||
import { Posts } from './posts/Posts';
|
||||
import { SearchInput } from '../../../components/input/SearchInput';
|
||||
import { Chat } from './chat/Chat';
|
||||
import { Contests } from './contests/Contests';
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { FC, useEffect, useRef } from 'react';
|
||||
import { FC, useEffect, useRef, useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||
import { fetchGroupMessages } from '../../../../redux/slices/groupChat';
|
||||
import {
|
||||
fetchGroupMessages,
|
||||
sendGroupMessage,
|
||||
setGroupChatStatus,
|
||||
} from '../../../../redux/slices/groupChat';
|
||||
import { SearchInput } from '../../../../components/input/SearchInput';
|
||||
import { MessageItem } from './MessageItem';
|
||||
import { Send } from '../../../../assets/icons/input';
|
||||
|
||||
interface GroupChatProps {
|
||||
groupId: number;
|
||||
@@ -13,22 +19,27 @@ const CHUNK_SIZE = 10;
|
||||
export const Chat: FC<GroupChatProps> = ({ groupId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const messages = useAppSelector((s) => s.groupchat.messages[groupId] || []);
|
||||
const hasMore = useAppSelector((s) => s.groupchat.hasMore[groupId]);
|
||||
const isInitialLoaded = useAppSelector(
|
||||
(s) => s.groupchat.isInitialLoaded[groupId],
|
||||
const messagesState = useAppSelector(
|
||||
(state) => state.groupchat.fetchMessages.status,
|
||||
);
|
||||
const lastMessageId = useAppSelector(
|
||||
(state) => state.groupchat.lastMessage[groupId] || 0,
|
||||
);
|
||||
const user = useAppSelector((state) => state.auth);
|
||||
|
||||
const [text, setText] = useState<string>('');
|
||||
const [firstMessagesFetch, setFirctMessagesFetch] = useState<boolean>(true);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// добавлено: ref для хранения предыдущей высоты
|
||||
const prevHeightRef = useRef(0);
|
||||
|
||||
// активируем таб
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveGroupPage('chat'));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(messages);
|
||||
}, [messages]);
|
||||
|
||||
// первичная загрузка
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
@@ -39,23 +50,112 @@ export const Chat: FC<GroupChatProps> = ({ groupId }) => {
|
||||
);
|
||||
}, [groupId]);
|
||||
|
||||
// автоскролл вниз после начальной загрузки
|
||||
// автоскролл вниз после начальной загрузки (но не при догрузке)
|
||||
useEffect(() => {
|
||||
if (!isInitialLoaded || !scrollRef.current) return;
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}, [isInitialLoaded, messages.length]);
|
||||
const div = scrollRef.current;
|
||||
if (!div) return;
|
||||
|
||||
// если prevHeightRef == 0 — значит это не догрузка, а обычная загрузка
|
||||
if (prevHeightRef.current === 0) {
|
||||
div.scrollTop = div.scrollHeight;
|
||||
}
|
||||
}, [messages.length]);
|
||||
|
||||
// добавлено: компенсирование скролла при догрузке
|
||||
useEffect(() => {
|
||||
const div = scrollRef.current;
|
||||
if (!div) return;
|
||||
|
||||
if (prevHeightRef.current > 0) {
|
||||
const diff = div.scrollHeight - prevHeightRef.current;
|
||||
div.scrollTop = diff; // компенсируем смещение
|
||||
|
||||
prevHeightRef.current = 0; // сбрасываем
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messagesState == 'successful') {
|
||||
dispatch(
|
||||
setGroupChatStatus({ key: 'fetchMessages', status: 'idle' }),
|
||||
);
|
||||
}
|
||||
if (messagesState == 'failed') {
|
||||
dispatch(
|
||||
setGroupChatStatus({ key: 'fetchMessages', status: 'idle' }),
|
||||
);
|
||||
}
|
||||
}, [messagesState]);
|
||||
|
||||
const lastMessageIdRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
lastMessageIdRef.current = lastMessageId;
|
||||
if (firstMessagesFetch) {
|
||||
setFirctMessagesFetch(false);
|
||||
dispatch(
|
||||
fetchGroupMessages({
|
||||
groupId,
|
||||
afterMessageId: lastMessageIdRef.current,
|
||||
timeoutSeconds: 10,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (lastMessageIdRef.current === null) return;
|
||||
|
||||
dispatch(
|
||||
fetchGroupMessages({
|
||||
groupId,
|
||||
afterMessageId: lastMessageIdRef.current,
|
||||
timeoutSeconds: 10,
|
||||
}),
|
||||
);
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [groupId]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!text.trim()) return;
|
||||
|
||||
dispatch(
|
||||
sendGroupMessage({
|
||||
groupId,
|
||||
content: text.trim(),
|
||||
}),
|
||||
).then(() => {
|
||||
setText('');
|
||||
setTimeout(() => {
|
||||
const div = scrollRef.current;
|
||||
if (div) div.scrollTop = div.scrollHeight;
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
|
||||
// догрузка старых сообщений при скролле вверх
|
||||
const handleScroll = () => {
|
||||
const div = scrollRef.current;
|
||||
if (!div || !hasMore) return;
|
||||
if (!div) return;
|
||||
|
||||
// если скролл в верхней точке
|
||||
if (div.scrollTop === 0) {
|
||||
prevHeightRef.current = div.scrollHeight; // запоминаем высоту до загрузки
|
||||
|
||||
if (div.scrollTop < 100) {
|
||||
const first = messages[0];
|
||||
if (!first) return;
|
||||
if (!first || first.id == 1) return;
|
||||
|
||||
const beforeId = first.id - CHUNK_SIZE;
|
||||
console.log(beforeId);
|
||||
|
||||
dispatch(
|
||||
fetchGroupMessages({
|
||||
@@ -69,7 +169,7 @@ export const Chat: FC<GroupChatProps> = ({ groupId }) => {
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
<div className="grid grid-rows-[40px,1fr,50px] h-full relative min-h-0 gap-[20px]">
|
||||
<div className="grid grid-rows-[40px,1fr,40px] h-full relative min-h-0 gap-[20px]">
|
||||
<div className="relative">
|
||||
<SearchInput
|
||||
className="w-[216px]"
|
||||
@@ -83,25 +183,38 @@ export const Chat: FC<GroupChatProps> = ({ groupId }) => {
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className="flex flex-col gap-[20px] min-h-0 h-0">
|
||||
{messages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="bg-gray-800 text-white p-3 rounded-lg"
|
||||
>
|
||||
<div className="text-sm opacity-60">
|
||||
{msg.authorUsername} {msg.id}
|
||||
</div>
|
||||
<div>{msg.content}</div>
|
||||
<div className="text-[10px] opacity-40 mt-1">
|
||||
{new Date(msg.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[20px] min-h-0 h-0 px-[16px]">
|
||||
{messages.map((msg, i) => (
|
||||
<MessageItem
|
||||
key={i}
|
||||
message={msg.content}
|
||||
createdAt={msg.createdAt}
|
||||
id={msg.id}
|
||||
groupId={msg.groupId}
|
||||
authorId={msg.authorId}
|
||||
authorUsername={msg.authorUsername}
|
||||
myMessage={msg.authorId == Number(user.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-300">footer / input bar</div>
|
||||
<label className="bg-liquid-lighter rounded-[10px] cursor-text flex items-center px-[16px]">
|
||||
<input
|
||||
className="w-[calc(100%-50px)] outline-none bg-transparent placeholder:text-[16px] placeholder:text-liquid-light placeholder:font-medium"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleEnter}
|
||||
placeholder="Введите сообщение"
|
||||
/>
|
||||
<img
|
||||
src={Send}
|
||||
className=" absolute cursor-pointer right-[16px] active:scale-90 transition-all duration-300"
|
||||
onClick={() => {
|
||||
handleSend();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
81
src/views/home/group/chat/MessageItem.tsx
Normal file
81
src/views/home/group/chat/MessageItem.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { FC } from 'react';
|
||||
import { useAppSelector } from '../../../../redux/hooks';
|
||||
|
||||
function convertDate(isoString: string) {
|
||||
const date = new Date(isoString);
|
||||
|
||||
const dd = String(date.getUTCDate()).padStart(2, '0');
|
||||
const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const yyyy = date.getUTCFullYear();
|
||||
|
||||
const hh = String(date.getUTCHours()).padStart(2, '0');
|
||||
const min = String(date.getUTCMinutes()).padStart(2, '0');
|
||||
|
||||
return `${dd}.${mm}.${yyyy} ${hh}:${min}`;
|
||||
}
|
||||
|
||||
interface MessageItemProps {
|
||||
id: number;
|
||||
groupId: number;
|
||||
authorId: number;
|
||||
authorUsername: string;
|
||||
createdAt: string;
|
||||
message: string;
|
||||
myMessage: boolean;
|
||||
}
|
||||
|
||||
export const MessageItem: FC<MessageItemProps> = ({
|
||||
authorId,
|
||||
authorUsername,
|
||||
createdAt,
|
||||
message,
|
||||
myMessage,
|
||||
}) => {
|
||||
const members = useAppSelector(
|
||||
(state) => state.groups.fetchGroupById.group?.members,
|
||||
);
|
||||
const member = members?.find((m) => m.userId === authorId);
|
||||
|
||||
return myMessage ? (
|
||||
<div className="flex flex-col gap-[20px] items-end leading-[20px] text-[16px]">
|
||||
<div className="w-[50%] flex flex-col gap-[10px]">
|
||||
<div className="h-[20px] w-full flex gap-[10px] relative justify-end ">
|
||||
<div className="font-bold text-liquid-light">
|
||||
{convertDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="bg-liquid-lighter w-fit max-w-full break-words px-[16px] py-[8px] rounded-[10px] ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-[20px] ">
|
||||
<div className="w-[50%] flex flex-col gap-[10px]">
|
||||
<div className="h-[40px] w-full flex gap-[10px] relative ">
|
||||
<div className="h-[40px] w-[40px] bg-[#D9D9D9] rounded-[10px]"></div>
|
||||
<div className=" leading-[20px] font-bold text-[16px] ">
|
||||
<div>{authorUsername} </div>
|
||||
<div className="text-liquid-light">
|
||||
{member ? member.role : 'роль не найдена'}
|
||||
</div>
|
||||
</div>
|
||||
<div className=" leading-[20px] font-bold text-[16px] ">
|
||||
<div className="text-liquid-light">
|
||||
{convertDate(createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex">
|
||||
<div className="bg-liquid-lighter w-fit max-w-full break-words px-[16px] py-[8px] rounded-[10px] ">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,9 +2,7 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import { createGroup } from '../../../../redux/slices/groups';
|
||||
import MarkdownEditor from '../../../articleeditor/Editor';
|
||||
import {
|
||||
createPost,
|
||||
|
||||
@@ -2,12 +2,9 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import { createGroup } from '../../../../redux/slices/groups';
|
||||
import MarkdownEditor, { MarkDownPattern } from '../../../articleeditor/Editor';
|
||||
import {
|
||||
createPost,
|
||||
deletePost,
|
||||
fetchPostById,
|
||||
setGroupFeedStatus,
|
||||
@@ -55,7 +52,7 @@ const ModalUpdate: FC<ModalUpdateProps> = ({
|
||||
}, [statusDelete]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchPostById({ groupId, postId }));
|
||||
if (postId) dispatch(fetchPostById({ groupId, postId }));
|
||||
}, [postId]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,13 +32,10 @@ interface PostItemProps {
|
||||
|
||||
export const PostItem: FC<PostItemProps> = ({
|
||||
id,
|
||||
groupId,
|
||||
authorId,
|
||||
authorUsername,
|
||||
name,
|
||||
content,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
isAdmin,
|
||||
setModalUpdateActive,
|
||||
setUpdatePostId,
|
||||
|
||||
@@ -58,7 +58,9 @@ export const Posts: FC<PostsProps> = ({ groupId }) => {
|
||||
<div className="h-[40px] mb-[20px] relative">
|
||||
<SearchInput
|
||||
className="w-[216px]"
|
||||
onChange={(v) => {}}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
placeholder="Поиск сообщений"
|
||||
/>
|
||||
{isAdmin && (
|
||||
|
||||
@@ -36,13 +36,17 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => {}}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => {}}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,6 @@ const GroupItem: React.FC<GroupItemProps> = ({
|
||||
id,
|
||||
name,
|
||||
visible,
|
||||
role,
|
||||
description,
|
||||
setUpdateGroup,
|
||||
setUpdateActive,
|
||||
|
||||
@@ -4,7 +4,6 @@ import { fetchGroupJoinLink } from '../../../redux/slices/groups';
|
||||
import { Modal } from '../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../components/input/Input';
|
||||
import { toastSuccess } from '../../../lib/toastNotification';
|
||||
|
||||
interface ModalInviteProps {
|
||||
@@ -54,9 +53,7 @@ const ModalInvite: FC<ModalInviteProps> = ({
|
||||
await navigator.clipboard.writeText(inviteLink);
|
||||
toastSuccess('Приглашение скопировано в буфер обмена!');
|
||||
setActive(false);
|
||||
} catch (err) {
|
||||
console.error('Не удалось скопировать ссылку:', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,13 +36,17 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => console.log(v)}
|
||||
onChange={(v) => {
|
||||
v;
|
||||
}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => console.log(values)}
|
||||
onChange={(values) => {
|
||||
values;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react';
|
||||
import { FC, Fragment } from 'react';
|
||||
|
||||
export const ArticlesRightPanel: FC = () => {
|
||||
const items = [
|
||||
@@ -23,7 +23,7 @@ export const ArticlesRightPanel: FC = () => {
|
||||
|
||||
{items.map((v, i) => {
|
||||
return (
|
||||
<>
|
||||
<Fragment key={i}>
|
||||
{
|
||||
<div className="font-bold text-liquid-light text-[16px]">
|
||||
{v.name}
|
||||
@@ -32,7 +32,7 @@ export const ArticlesRightPanel: FC = () => {
|
||||
{i + 1 != items.length && (
|
||||
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC } from 'react';
|
||||
import { FC, Fragment } from 'react';
|
||||
import { cn } from '../../../lib/cn';
|
||||
|
||||
export const MissionsRightPanel: FC = () => {
|
||||
@@ -32,7 +32,7 @@ export const MissionsRightPanel: FC = () => {
|
||||
|
||||
{items.map((v, i) => {
|
||||
return (
|
||||
<>
|
||||
<Fragment key={i}>
|
||||
{
|
||||
<div className="text-liquid-light text-[16px]">
|
||||
<div className="font-bold ">{v.name}</div>
|
||||
@@ -60,7 +60,7 @@ export const MissionsRightPanel: FC = () => {
|
||||
{i + 1 != items.length && (
|
||||
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { FC, Fragment, useEffect, useState } from 'react';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import { fetchGroupById, GroupMember } from '../../../../redux/slices/groups';
|
||||
@@ -54,7 +54,7 @@ export const GroupRightPanel: FC = () => {
|
||||
|
||||
{group?.members.map((v, i) => {
|
||||
return (
|
||||
<>
|
||||
<Fragment key={i}>
|
||||
{
|
||||
<div className="text-liquid-light text-[16px] grid grid-cols-[40px,1fr] gap-[10px] items-center cursor-pointer hover:bg-liquid-lighter transition-all duration-300 rounded-[10px] p-[5px] group">
|
||||
<div className="h-[40px] w-[40px] rounded-[10px] bg-[#D9D9D9]"></div>
|
||||
@@ -101,7 +101,7 @@ export const GroupRightPanel: FC = () => {
|
||||
{i + 1 != group?.members.length && (
|
||||
<div className="h-[1px] w-full bg-liquid-lighter"></div>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
|
||||
@@ -2,13 +2,10 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import {
|
||||
deleteGroup,
|
||||
removeGroupMember,
|
||||
setGroupsStatus,
|
||||
updateGroup,
|
||||
} from '../../../../redux/slices/groups';
|
||||
import ConfirmModal from '../../../../components/modal/ConfirmModal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -2,16 +2,13 @@ import { FC, useEffect, useState } from 'react';
|
||||
import { Modal } from '../../../../components/modal/Modal';
|
||||
import { PrimaryButton } from '../../../../components/button/PrimaryButton';
|
||||
import { SecondaryButton } from '../../../../components/button/SecondaryButton';
|
||||
import { Input } from '../../../../components/input/Input';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
|
||||
import {
|
||||
addGroupMember,
|
||||
deleteGroup,
|
||||
fetchGroupById,
|
||||
GroupMember,
|
||||
removeGroupMember,
|
||||
setGroupsStatus,
|
||||
updateGroup,
|
||||
} from '../../../../redux/slices/groups';
|
||||
import ConfirmModal from '../../../../components/modal/ConfirmModal';
|
||||
import { DropDownList } from '../../../../components/drop-down-list/DropDownList';
|
||||
@@ -31,7 +28,6 @@ const ModalUpdate: FC<ModalUpdateProps> = ({
|
||||
active,
|
||||
setActive,
|
||||
groupId,
|
||||
userId,
|
||||
user,
|
||||
adminUser,
|
||||
groupName,
|
||||
@@ -76,7 +72,6 @@ const ModalUpdate: FC<ModalUpdateProps> = ({
|
||||
}, [statusUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(user);
|
||||
if (user) {
|
||||
setUserRole(
|
||||
user?.role.includes('Creator') ? 'Creator' : user?.role,
|
||||
|
||||
@@ -17,9 +17,7 @@ const CopyableDiv: FC<CopyableDivPropd> = ({ content }) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
alert('Скопировано!');
|
||||
} catch (err) {
|
||||
console.error('Ошибка копирования:', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"root":["./src/app.tsx","./src/axios.ts","./src/main.tsx","./src/vite-env.d.ts","./src/assets/icons/account/index.ts","./src/assets/icons/auth/index.ts","./src/assets/icons/filters/index.ts","./src/assets/icons/group/index.ts","./src/assets/icons/groups/index.ts","./src/assets/icons/header/index.ts","./src/assets/icons/input/index.ts","./src/assets/icons/menu/index.ts","./src/assets/icons/missions/index.ts","./src/assets/logos/index.ts","./src/components/button/primarybutton.tsx","./src/components/button/reversebutton.tsx","./src/components/button/secondarybutton.tsx","./src/components/checkbox/checkbox.tsx","./src/components/drop-down-list/dropdownlist.tsx","./src/components/drop-down-list/filter.tsx","./src/components/drop-down-list/sorter.tsx","./src/components/input/daterangeinput.tsx","./src/components/input/input.tsx","./src/components/input/searchinput.tsx","./src/components/modal/confirmmodal.tsx","./src/components/modal/modal.tsx","./src/components/router/protectedroute.tsx","./src/components/switch/switch.tsx","./src/config/colors.ts","./src/hooks/useclickoutside.ts","./src/hooks/usequery.ts","./src/lib/cn.ts","./src/lib/toastnotification.ts","./src/pages/article.tsx","./src/pages/articleeditor.tsx","./src/pages/contesteditor.tsx","./src/pages/home.tsx","./src/pages/mission.tsx","./src/redux/hooks.ts","./src/redux/store.ts","./src/redux/slices/articles.ts","./src/redux/slices/auth.ts","./src/redux/slices/contests.ts","./src/redux/slices/groupfeed.ts","./src/redux/slices/groups.ts","./src/redux/slices/missions.ts","./src/redux/slices/store.ts","./src/redux/slices/submit.ts","./src/views/article/header.tsx","./src/views/articleeditor/editor.tsx","./src/views/articleeditor/header.tsx","./src/views/articleeditor/marckdownpreview.tsx","./src/views/home/account/account.tsx","./src/views/home/account/accoutmenu.tsx","./src/views/home/account/rightpanel.tsx","./src/views/home/account/articles/articlesblock.tsx","./src/views/home/account/contests/contests.tsx","./src/views/home/account/contests/contestsblock.tsx","./src/views/home/account/contests/mycontestitem.tsx","./src/views/home/account/contests/registercontestitem.tsx","./src/views/home/account/missions/missions.tsx","./src/views/home/account/missions/missionsblock.tsx","./src/views/home/account/missions/mymissionitem.tsx","./src/views/home/articles/articleitem.tsx","./src/views/home/articles/articles.tsx","./src/views/home/articles/filter.tsx","./src/views/home/auth/login.tsx","./src/views/home/auth/register.tsx","./src/views/home/contest/contest.tsx","./src/views/home/contest/missionitem.tsx","./src/views/home/contest/missions.tsx","./src/views/home/contest/submissionitem.tsx","./src/views/home/contest/submissions.tsx","./src/views/home/contests/contestitem.tsx","./src/views/home/contests/contests.tsx","./src/views/home/contests/contestsblock.tsx","./src/views/home/contests/filter.tsx","./src/views/home/contests/modalcreate.tsx","./src/views/home/group/group.tsx","./src/views/home/group/groupmenu.tsx","./src/views/home/group/chat/chat.tsx","./src/views/home/group/contests/contests.tsx","./src/views/home/group/posts/modalcreate.tsx","./src/views/home/group/posts/modalupdate.tsx","./src/views/home/group/posts/postitem.tsx","./src/views/home/group/posts/posts.tsx","./src/views/home/groupinviter/groupinvite.tsx","./src/views/home/groups/filter.tsx","./src/views/home/groups/groupitem.tsx","./src/views/home/groups/groups.tsx","./src/views/home/groups/groupsblock.tsx","./src/views/home/groups/modalcreate.tsx","./src/views/home/groups/modalinvite.tsx","./src/views/home/groups/modalupdate.tsx","./src/views/home/menu/menu.tsx","./src/views/home/menu/menuitem.tsx","./src/views/home/missions/filter.tsx","./src/views/home/missions/missionitem.tsx","./src/views/home/missions/missions.tsx","./src/views/home/missions/modalcreate.tsx","./src/views/home/rightpanel/articles.tsx","./src/views/home/rightpanel/missions.tsx","./src/views/home/rightpanel/group/group.tsx","./src/views/home/rightpanel/group/modalleave.tsx","./src/views/home/rightpanel/group/modalupdate.tsx","./src/views/mission/codeeditor/codeeditor.tsx","./src/views/mission/statement/header.tsx","./src/views/mission/statement/latextcontainer.tsx","./src/views/mission/statement/missionsubmissions.tsx","./src/views/mission/statement/statement.tsx","./src/views/mission/statement/submissionitem.tsx","./src/views/mission/submission/submission.tsx"],"errors":true,"version":"5.6.2"}
|
||||
Reference in New Issue
Block a user