group posts
This commit is contained in:
3
src/assets/icons/group/cup.svg
Normal file
3
src/assets/icons/group/cup.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 4H18V3C18 2.73478 17.8946 2.48043 17.7071 2.29289C17.5196 2.10536 17.2652 2 17 2H7C6.73478 2 6.48043 2.10536 6.29289 2.29289C6.10536 2.48043 6 2.73478 6 3V4H3C2.73478 4 2.48043 4.10536 2.29289 4.29289C2.10536 4.48043 2 4.73478 2 5V8C2 9.06087 2.42143 10.0783 3.17157 10.8284C3.92172 11.5786 4.93913 12 6 12H7.54C8.44453 13.0091 9.66406 13.6824 11 13.91V16H10C9.20435 16 8.44129 16.3161 7.87868 16.8787C7.31607 17.4413 7 18.2044 7 19V21C7 21.2652 7.10536 21.5196 7.29289 21.7071C7.48043 21.8946 7.73478 22 8 22H16C16.2652 22 16.5196 21.8946 16.7071 21.7071C16.8946 21.5196 17 21.2652 17 21V19C17 18.2044 16.6839 17.4413 16.1213 16.8787C15.5587 16.3161 14.7956 16 14 16H13V13.91C14.3359 13.6824 15.5555 13.0091 16.46 12H18C19.0609 12 20.0783 11.5786 20.8284 10.8284C21.5786 10.0783 22 9.06087 22 8V5C22 4.73478 21.8946 4.48043 21.7071 4.29289C21.5196 4.10536 21.2652 4 21 4ZM6 10C5.46957 10 4.96086 9.78929 4.58579 9.41421C4.21071 9.03914 4 8.53043 4 8V6H6V8C6.0022 8.68171 6.12056 9.35806 6.35 10H6ZM14 18C14.2652 18 14.5196 18.1054 14.7071 18.2929C14.8946 18.4804 15 18.7348 15 19V20H9V19C9 18.7348 9.10536 18.4804 9.29289 18.2929C9.48043 18.1054 9.73478 18 10 18H14ZM16 8C16 9.06087 15.5786 10.0783 14.8284 10.8284C14.0783 11.5786 13.0609 12 12 12C10.9391 12 9.92172 11.5786 9.17157 10.8284C8.42143 10.0783 8 9.06087 8 8V4H16V8ZM20 8C20 8.53043 19.7893 9.03914 19.4142 9.41421C19.0391 9.78929 18.5304 10 18 10H17.65C17.8794 9.35806 17.9978 8.68171 18 8V6H20V8Z" fill="#EDF6F7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
src/assets/icons/group/home.svg
Normal file
3
src/assets/icons/group/home.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="M7.5 17.0625H16.5M11.3046 3.21117L3.50457 8.48603C3.18802 8.7001 3 9.04666 3 9.41605V19.2882C3 20.2336 3.80589 21 4.8 21H19.2C20.1941 21 21 20.2336 21 19.2882V9.41605C21 9.04665 20.812 8.7001 20.4954 8.48603L12.6954 3.21117C12.2791 2.92961 11.7209 2.92961 11.3046 3.21117Z" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 469 B |
5
src/assets/icons/group/index.ts
Normal file
5
src/assets/icons/group/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Cup from './cup.svg';
|
||||
import Home from './home.svg';
|
||||
import MessageChat from './message-chat.svg';
|
||||
|
||||
export { Cup, MessageChat, Home };
|
||||
3
src/assets/icons/group/message-chat.svg
Normal file
3
src/assets/icons/group/message-chat.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="M19 10.5V6C19 4.89543 18.1046 4 17 4H5C3.89543 4 3 4.89543 3 6V13.8261C3 14.9307 3.89543 15.8261 5 15.8261H6.56522V20L10.7391 15.8261H11M16.163 18.3913L18.7717 21V18.3913H19C20.1046 18.3913 21 17.4959 21 16.3913V13C21 11.8954 20.1046 11 19 11H13C11.8954 11 11 11.8954 11 13V16.3913C11 17.4959 11.8954 18.3913 13 18.3913H16.163Z" stroke="#EDF6F7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 524 B |
@@ -39,7 +39,7 @@ export const SearchInput: React.FC<searchInputProps> = ({
|
||||
>
|
||||
<input
|
||||
className={cn(
|
||||
'placeholder:text-liquid-light w-full bg-transparent outline-none text-liquid-white',
|
||||
'placeholder:text-liquid-light h-[28px] w-[200px] bg-transparent outline-none text-liquid-white ',
|
||||
)}
|
||||
value={value}
|
||||
name={name}
|
||||
@@ -53,7 +53,10 @@ export const SearchInput: React.FC<searchInputProps> = ({
|
||||
if (onKeyDown) onKeyDown(e);
|
||||
}}
|
||||
/>
|
||||
<img src={iconSearch} className=" absolute top-[8px] left-[16px]" />
|
||||
<img
|
||||
src={iconSearch}
|
||||
className=" absolute top-[8px] left-[16px] w-[24px] h-[24px]"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,8 +6,6 @@ export default function ProtectedRoute() {
|
||||
const isAuthenticated = useAppSelector((state) => !!state.auth.jwt);
|
||||
const location = useLocation();
|
||||
|
||||
console.log('location', location);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/home/login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,6 @@ const ContestEditor = () => {
|
||||
const { contest: contestById, status: contestByIdstatus } = useAppSelector(
|
||||
(state) => state.contests.fetchContestById,
|
||||
);
|
||||
console.log(contestByIdstatus, contestById);
|
||||
useEffect(() => {
|
||||
if (status === 'successful') {
|
||||
}
|
||||
@@ -100,9 +99,7 @@ const ContestEditor = () => {
|
||||
}));
|
||||
setMissionIdInput('');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Ошибка при загрузке миссии:', err);
|
||||
});
|
||||
.catch((err) => {});
|
||||
};
|
||||
|
||||
const removeMission = (removeId: number) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ const Home = () => {
|
||||
path="group-invite/*"
|
||||
element={<GroupInvite />}
|
||||
/>
|
||||
<Route path="group/:groupId" element={<Group />} />
|
||||
<Route path="group/:groupId/*" element={<Group />} />
|
||||
<Route path="groups/*" element={<Groups />} />
|
||||
</Route>
|
||||
|
||||
@@ -85,7 +85,7 @@ const Home = () => {
|
||||
<Route path="articles/*" element={<ArticlesRightPanel />} />
|
||||
<Route path="missions/*" element={<MissionsRightPanel />} />
|
||||
<Route
|
||||
path="group/:groupId"
|
||||
path="group/:groupId/*"
|
||||
element={<GroupRightPanel />}
|
||||
/>
|
||||
</Routes>
|
||||
|
||||
@@ -149,9 +149,7 @@ const Mission = () => {
|
||||
html: htmlStatement.statementTexts['problem.html'],
|
||||
mediaFiles: latexStatement.mediaFiles,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Ошибка парсинга statementTexts:', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return (
|
||||
<div className="h-screen grid grid-rows-[60px,1fr]">
|
||||
@@ -180,7 +178,6 @@ const Mission = () => {
|
||||
<PrimaryButton
|
||||
text="Отправить"
|
||||
onClick={async () => {
|
||||
console.log(contestId);
|
||||
await dispatch(
|
||||
submitMission({
|
||||
missionId: missionIdNumber,
|
||||
@@ -200,7 +197,10 @@ const Mission = () => {
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full ">
|
||||
<MissionSubmissions missionId={missionIdNumber} contestId={contestId} />
|
||||
<MissionSubmissions
|
||||
missionId={missionIdNumber}
|
||||
contestId={contestId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -286,7 +286,6 @@ const authSlice = createSlice({
|
||||
state.error = action.payload as string;
|
||||
|
||||
// Если пользователь не авторизован (401), делаем logout и пытаемся refresh
|
||||
console.log(action);
|
||||
if (
|
||||
action.payload === 'Unauthorized' ||
|
||||
action.payload === 'Failed to fetch user info'
|
||||
|
||||
336
src/redux/slices/groupfeed.ts
Normal file
336
src/redux/slices/groupfeed.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import axios from '../../axios';
|
||||
|
||||
// =====================
|
||||
// Типы
|
||||
// =====================
|
||||
|
||||
type Status = 'idle' | 'loading' | 'successful' | 'failed';
|
||||
|
||||
export interface Post {
|
||||
id: number;
|
||||
groupId: number;
|
||||
authorId: number;
|
||||
authorUsername: string;
|
||||
name: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PostsPage {
|
||||
items: Post[];
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Состояние
|
||||
// =====================
|
||||
|
||||
interface PostsState {
|
||||
fetchPosts: {
|
||||
pages: Record<number, PostsPage>; // страница => данные
|
||||
status: Status;
|
||||
error?: string;
|
||||
};
|
||||
fetchPostById: {
|
||||
post?: Post;
|
||||
status: Status;
|
||||
error?: string;
|
||||
};
|
||||
createPost: {
|
||||
post?: Post;
|
||||
status: Status;
|
||||
error?: string;
|
||||
};
|
||||
updatePost: {
|
||||
post?: Post;
|
||||
status: Status;
|
||||
error?: string;
|
||||
};
|
||||
deletePost: {
|
||||
deletedId?: number;
|
||||
status: Status;
|
||||
error?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: PostsState = {
|
||||
fetchPosts: {
|
||||
pages: {},
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
},
|
||||
fetchPostById: {
|
||||
post: undefined,
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
},
|
||||
createPost: {
|
||||
post: undefined,
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
},
|
||||
updatePost: {
|
||||
post: undefined,
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
},
|
||||
deletePost: {
|
||||
deletedId: undefined,
|
||||
status: 'idle',
|
||||
error: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
// =====================
|
||||
// Async Thunks
|
||||
// =====================
|
||||
|
||||
// Получить посты группы (пагинация)
|
||||
export const fetchGroupPosts = createAsyncThunk(
|
||||
'posts/fetchGroupPosts',
|
||||
async (
|
||||
{
|
||||
groupId,
|
||||
page = 0,
|
||||
pageSize = 20,
|
||||
}: { groupId: number; page?: number; pageSize?: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/groups/${groupId}/feed?page=${page}&pageSize=${pageSize}`,
|
||||
);
|
||||
return { page, data: response.data as PostsPage };
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка загрузки постов',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Получить один пост
|
||||
export const fetchPostById = createAsyncThunk(
|
||||
'posts/fetchPostById',
|
||||
async (
|
||||
{ groupId, postId }: { groupId: number; postId: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`/groups/${groupId}/feed/${postId}`,
|
||||
);
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка загрузки поста',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Создать пост
|
||||
export const createPost = createAsyncThunk(
|
||||
'posts/createPost',
|
||||
async (
|
||||
{
|
||||
groupId,
|
||||
name,
|
||||
content,
|
||||
}: { groupId: number; name: string; content: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post(`/groups/${groupId}/feed`, {
|
||||
name,
|
||||
content,
|
||||
});
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка создания поста',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Обновить пост
|
||||
export const updatePost = createAsyncThunk(
|
||||
'posts/updatePost',
|
||||
async (
|
||||
{
|
||||
groupId,
|
||||
postId,
|
||||
name,
|
||||
content,
|
||||
}: {
|
||||
groupId: number;
|
||||
postId: number;
|
||||
name: string;
|
||||
content: string;
|
||||
},
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.put(
|
||||
`/groups/${groupId}/feed/${postId}`,
|
||||
{
|
||||
name,
|
||||
content,
|
||||
},
|
||||
);
|
||||
return response.data as Post;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка обновления поста',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Удалить пост
|
||||
export const deletePost = createAsyncThunk(
|
||||
'posts/deletePost',
|
||||
async (
|
||||
{ groupId, postId }: { groupId: number; postId: number },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
await axios.delete(`/groups/${groupId}/feed/${postId}`);
|
||||
return postId;
|
||||
} catch (err: any) {
|
||||
return rejectWithValue(
|
||||
err.response?.data?.message || 'Ошибка удаления поста',
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// =====================
|
||||
// Slice
|
||||
// =====================
|
||||
|
||||
const postsSlice = createSlice({
|
||||
name: 'posts',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
// fetchGroupPosts
|
||||
builder.addCase(fetchGroupPosts.pending, (state) => {
|
||||
state.fetchPosts.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
fetchGroupPosts.fulfilled,
|
||||
(
|
||||
state,
|
||||
action: PayloadAction<{ page: number; data: PostsPage }>,
|
||||
) => {
|
||||
const { page, data } = action.payload;
|
||||
state.fetchPosts.status = 'successful';
|
||||
state.fetchPosts.pages[page] = data;
|
||||
},
|
||||
);
|
||||
builder.addCase(fetchGroupPosts.rejected, (state, action: any) => {
|
||||
state.fetchPosts.status = 'failed';
|
||||
state.fetchPosts.error = action.payload;
|
||||
});
|
||||
|
||||
// fetchPostById
|
||||
builder.addCase(fetchPostById.pending, (state) => {
|
||||
state.fetchPostById.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
fetchPostById.fulfilled,
|
||||
(state, action: PayloadAction<Post>) => {
|
||||
state.fetchPostById.status = 'successful';
|
||||
state.fetchPostById.post = action.payload;
|
||||
},
|
||||
);
|
||||
builder.addCase(fetchPostById.rejected, (state, action: any) => {
|
||||
state.fetchPostById.status = 'failed';
|
||||
state.fetchPostById.error = action.payload;
|
||||
});
|
||||
|
||||
// createPost
|
||||
builder.addCase(createPost.pending, (state) => {
|
||||
state.createPost.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
createPost.fulfilled,
|
||||
(state, action: PayloadAction<Post>) => {
|
||||
state.createPost.status = 'successful';
|
||||
state.createPost.post = action.payload;
|
||||
|
||||
// добавляем сразу в первую страницу (page = 0)
|
||||
if (state.fetchPosts.pages[0]) {
|
||||
state.fetchPosts.pages[0].items.unshift(action.payload);
|
||||
}
|
||||
},
|
||||
);
|
||||
builder.addCase(createPost.rejected, (state, action: any) => {
|
||||
state.createPost.status = 'failed';
|
||||
state.createPost.error = action.payload;
|
||||
});
|
||||
|
||||
// updatePost
|
||||
builder.addCase(updatePost.pending, (state) => {
|
||||
state.updatePost.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
updatePost.fulfilled,
|
||||
(state, action: PayloadAction<Post>) => {
|
||||
state.updatePost.status = 'successful';
|
||||
state.updatePost.post = action.payload;
|
||||
|
||||
// обновим в списках
|
||||
for (const page of Object.values(state.fetchPosts.pages)) {
|
||||
const index = page.items.findIndex(
|
||||
(p) => p.id === action.payload.id,
|
||||
);
|
||||
if (index !== -1) page.items[index] = action.payload;
|
||||
}
|
||||
|
||||
// обновим если открыт одиночный пост
|
||||
if (state.fetchPostById.post?.id === action.payload.id) {
|
||||
state.fetchPostById.post = action.payload;
|
||||
}
|
||||
},
|
||||
);
|
||||
builder.addCase(updatePost.rejected, (state, action: any) => {
|
||||
state.updatePost.status = 'failed';
|
||||
state.updatePost.error = action.payload;
|
||||
});
|
||||
|
||||
// deletePost
|
||||
builder.addCase(deletePost.pending, (state) => {
|
||||
state.deletePost.status = 'loading';
|
||||
});
|
||||
builder.addCase(
|
||||
deletePost.fulfilled,
|
||||
(state, action: PayloadAction<number>) => {
|
||||
state.deletePost.status = 'successful';
|
||||
state.deletePost.deletedId = action.payload;
|
||||
|
||||
// удалить из всех страниц
|
||||
for (const page of Object.values(state.fetchPosts.pages)) {
|
||||
page.items = page.items.filter(
|
||||
(p) => p.id !== action.payload,
|
||||
);
|
||||
}
|
||||
|
||||
// если открыт индивидуальный пост
|
||||
if (state.fetchPostById.post?.id === action.payload) {
|
||||
state.fetchPostById.post = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
builder.addCase(deletePost.rejected, (state, action: any) => {
|
||||
state.deletePost.status = 'failed';
|
||||
state.deletePost.error = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const groupFeedReducer = postsSlice.reducer;
|
||||
@@ -5,6 +5,7 @@ interface StorState {
|
||||
menu: {
|
||||
activePage: string;
|
||||
activeProfilePage: string;
|
||||
activeGroupPage: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ const initialState: StorState = {
|
||||
menu: {
|
||||
activePage: '',
|
||||
activeProfilePage: '',
|
||||
activeGroupPage: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,9 +32,19 @@ const storeSlice = createSlice({
|
||||
) => {
|
||||
state.menu.activeProfilePage = activeProfilePage.payload;
|
||||
},
|
||||
setMenuActiveGroupPage: (
|
||||
state,
|
||||
activeGroupPage: PayloadAction<string>,
|
||||
) => {
|
||||
state.menu.activeGroupPage = activeGroupPage.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setMenuActivePage, setMenuActiveProfilePage } =
|
||||
storeSlice.actions;
|
||||
export const {
|
||||
setMenuActivePage,
|
||||
setMenuActiveProfilePage,
|
||||
setMenuActiveGroupPage,
|
||||
} = storeSlice.actions;
|
||||
|
||||
export const storeReducer = storeSlice.reducer;
|
||||
|
||||
@@ -56,7 +56,6 @@ const initialState: SubmitState = {
|
||||
export const submitMission = createAsyncThunk(
|
||||
'submit/submitMission',
|
||||
async (submitData: Submit, { rejectWithValue }) => {
|
||||
console.log(submitData);
|
||||
try {
|
||||
const response = await axios.post('/submits', submitData);
|
||||
return response.data;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { submitReducer } from './slices/submit';
|
||||
import { contestsReducer } from './slices/contests';
|
||||
import { groupsReducer } from './slices/groups';
|
||||
import { articlesReducer } from './slices/articles';
|
||||
import { groupFeedReducer } from './slices/groupfeed';
|
||||
|
||||
// использование
|
||||
// import { useAppDispatch, useAppSelector } from '../redux/hooks';
|
||||
@@ -25,6 +26,7 @@ export const store = configureStore({
|
||||
contests: contestsReducer,
|
||||
groups: groupsReducer,
|
||||
articles: articlesReducer,
|
||||
groupfeed: groupFeedReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ const AccountMenu = () => {
|
||||
(state) => state.store.menu.activeProfilePage,
|
||||
);
|
||||
|
||||
console.log('active', [activeProfilePage]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative flex p-[20px] gap-[10px]">
|
||||
{menuItems.map((v, i) => (
|
||||
|
||||
@@ -22,8 +22,6 @@ const Contests = () => {
|
||||
dispatch(fetchRegisteredContests({}));
|
||||
}, []);
|
||||
|
||||
console.log(myContestsState);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full relative flex flex-col text-[60px] font-bold p-[20px] gap-[20px]">
|
||||
{/* Контесты, в которых я участвую */}
|
||||
|
||||
@@ -36,13 +36,13 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => console.log(v)}
|
||||
onChange={(v) => {}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => console.log(values)}
|
||||
onChange={(values) => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -27,7 +27,7 @@ const Login = () => {
|
||||
// После успешного логина
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActivePage('account'));
|
||||
console.log(submitClicked);
|
||||
submitClicked;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -38,7 +38,7 @@ const Register = () => {
|
||||
const path = from ? from.pathname + from.search : '/home/account';
|
||||
navigate(path, { replace: true });
|
||||
}
|
||||
console.log(submitClicked);
|
||||
submitClicked;
|
||||
}, [jwt]);
|
||||
|
||||
const handleRegister = () => {
|
||||
|
||||
@@ -73,7 +73,6 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
||||
: ' bg-liquid-background',
|
||||
)}
|
||||
onClick={() => {
|
||||
console.log(456);
|
||||
navigate(`/contest/${id}`);
|
||||
}}
|
||||
>
|
||||
@@ -99,12 +98,7 @@ const ContestItem: React.FC<ContestItemProps> = ({
|
||||
{statusRegister == 'reg' ? (
|
||||
<>
|
||||
{' '}
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
console.log(123);
|
||||
}}
|
||||
text="Регистрация"
|
||||
/>
|
||||
<PrimaryButton onClick={() => {}} text="Регистрация" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import { FC } from 'react';
|
||||
import { FC, useEffect } from 'react';
|
||||
import { cn } from '../../../lib/cn';
|
||||
import { useParams, Navigate } from 'react-router-dom';
|
||||
import { useParams, Navigate, Routes, Route } from 'react-router-dom';
|
||||
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';
|
||||
|
||||
interface GroupsBlockProps {}
|
||||
|
||||
const Group: FC<GroupsBlockProps> = () => {
|
||||
const { groupId } = useParams<{ groupId: string }>();
|
||||
const groupIdNumber = Number(groupId);
|
||||
|
||||
if (!groupId || isNaN(groupIdNumber) || !groupIdNumber) {
|
||||
const groupId = Number(useParams<{ groupId: string }>().groupId);
|
||||
if (!groupId) {
|
||||
return <Navigate to="/home/groups" replace />;
|
||||
}
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const group = useAppSelector((state) => state.groups.fetchGroupById.group);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGroupById(groupId));
|
||||
}, [groupId]);
|
||||
|
||||
console.log(group);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b-[1px] border-b-liquid-lighter rounded-[10px]',
|
||||
' h-screen w-full text-liquid-white p-[20px] flex gap-[20px] flex-col',
|
||||
)}
|
||||
>
|
||||
{groupIdNumber}
|
||||
<div className="font-bold text-[40px]">{group?.name}</div>
|
||||
|
||||
<GroupMenu groupId={groupId} />
|
||||
|
||||
<Routes>
|
||||
<Route path="home" element={<Posts groupId={groupId} />} />
|
||||
<Route path="chat" element={<Chat />} />
|
||||
<Route path="contests" element={<Contests />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to={`/group/${groupId}/home`} />}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
96
src/views/home/group/GroupMenu.tsx
Normal file
96
src/views/home/group/GroupMenu.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { MessageChat, Home, Cup } from '../../../assets/icons/group';
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '../../../redux/hooks';
|
||||
import {
|
||||
setMenuActivePage,
|
||||
setMenuActiveProfilePage,
|
||||
} from '../../../redux/slices/store';
|
||||
|
||||
interface MenuItemProps {
|
||||
icon: string;
|
||||
text: string;
|
||||
href: string;
|
||||
page: string;
|
||||
profilePage: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({
|
||||
icon,
|
||||
text = '',
|
||||
href = '',
|
||||
active = false,
|
||||
page = '',
|
||||
profilePage = '',
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={href}
|
||||
className={`
|
||||
flex items-center gap-3 p-[16px] rounded-[10px] h-[40px] text-[18px] font-bold
|
||||
transition-all duration-300 text-liquid-white
|
||||
active:scale-95 hover:bg-liquid-lighter hover:ring-[1px] hover:ring-liquid-light hover:ring-inset
|
||||
${active && 'bg-liquid-lighter '}
|
||||
`}
|
||||
onClick={() => {
|
||||
dispatch(setMenuActivePage(page));
|
||||
dispatch(setMenuActiveProfilePage(profilePage));
|
||||
}}
|
||||
>
|
||||
<img src={icon} />
|
||||
<span>{text}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
interface GroupMenuProps {
|
||||
groupId: number;
|
||||
}
|
||||
|
||||
const GroupMenu: FC<GroupMenuProps> = ({ groupId }) => {
|
||||
const menuItems = [
|
||||
{
|
||||
text: 'Главная',
|
||||
href: `/group/${groupId}/home`,
|
||||
icon: Home,
|
||||
page: 'group',
|
||||
profilePage: 'home',
|
||||
},
|
||||
{
|
||||
text: 'Чат',
|
||||
href: `/group/${groupId}/chat`,
|
||||
icon: MessageChat,
|
||||
page: 'group',
|
||||
profilePage: 'chat',
|
||||
},
|
||||
{
|
||||
text: 'Контесты',
|
||||
href: `/group/${groupId}/contests`,
|
||||
icon: Cup,
|
||||
page: 'group',
|
||||
profilePage: 'contests',
|
||||
},
|
||||
];
|
||||
|
||||
const activeGroupPage = useAppSelector(
|
||||
(state) => state.store.menu.activeGroupPage,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full relative flex gap-[10px]">
|
||||
{menuItems.map((v, i) => (
|
||||
<MenuItem
|
||||
{...v}
|
||||
key={i}
|
||||
active={activeGroupPage == v.profilePage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMenu;
|
||||
12
src/views/home/group/chat/Chat.tsx
Normal file
12
src/views/home/group/chat/Chat.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from '../../../../redux/hooks';
|
||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||
|
||||
export const Chat = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveGroupPage('chat'));
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
12
src/views/home/group/contests/Contests.tsx
Normal file
12
src/views/home/group/contests/Contests.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAppDispatch } from '../../../../redux/hooks';
|
||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||
|
||||
export const Contests = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveGroupPage('contests'));
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
0
src/views/home/group/posts/PostItem.tsx
Normal file
0
src/views/home/group/posts/PostItem.tsx
Normal file
83
src/views/home/group/posts/Posts.tsx
Normal file
83
src/views/home/group/posts/Posts.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
|
||||
import { useAppSelector, useAppDispatch } from '../../../../redux/hooks';
|
||||
import { fetchGroupPosts } from '../../../../redux/slices/groupfeed';
|
||||
import { SearchInput } from '../../../../components/input/SearchInput';
|
||||
import { setMenuActiveGroupPage } from '../../../../redux/slices/store';
|
||||
|
||||
interface PostsProps {
|
||||
groupId: number;
|
||||
}
|
||||
|
||||
export const Posts: FC<PostsProps> = ({ groupId }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { pages, status } = useAppSelector(
|
||||
(state) => state.groupfeed.fetchPosts,
|
||||
);
|
||||
|
||||
// Загружаем только первую страницу
|
||||
useEffect(() => {
|
||||
dispatch(fetchGroupPosts({ groupId, page: 0, pageSize: 20 }));
|
||||
}, [groupId]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setMenuActiveGroupPage('home'));
|
||||
}, []);
|
||||
|
||||
const page0 = pages[0];
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-scroll thin-dark-scrollbar">
|
||||
<div className="h-[40px] mb-[20px]">
|
||||
<SearchInput
|
||||
onChange={(v) => {}}
|
||||
placeholder="Поиск сообщений"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status === 'loading' && <div>Загрузка...</div>}
|
||||
{status === 'failed' && <div>Ошибка загрузки постов</div>}
|
||||
|
||||
{status == 'successful' &&
|
||||
page0?.items &&
|
||||
page0.items.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{page0.items.map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="border border-gray-700 rounded p-3"
|
||||
>
|
||||
<div>
|
||||
<b>ID:</b> {post.id}
|
||||
</div>
|
||||
<div>
|
||||
<b>Название:</b> {post.name}
|
||||
</div>
|
||||
<div>
|
||||
<b>Содержимое:</b> {post.content}
|
||||
</div>
|
||||
<div>
|
||||
<b>Автор:</b> {post.authorUsername}
|
||||
</div>
|
||||
<div>
|
||||
<b>Автор ID:</b> {post.authorId}
|
||||
</div>
|
||||
<div>
|
||||
<b>Group ID:</b> {post.groupId}
|
||||
</div>
|
||||
<div>
|
||||
<b>Создан:</b> {post.createdAt}
|
||||
</div>
|
||||
<div>
|
||||
<b>Обновлён:</b> {post.updatedAt}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : status === 'successful' ? (
|
||||
<div>Постов пока нет</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -63,9 +63,7 @@ const GroupInvite = () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
await dispatch(joinGroupByToken(token)).unwrap();
|
||||
} catch (err) {
|
||||
console.error('Failed to join group', err);
|
||||
}
|
||||
} catch (err) {}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
|
||||
@@ -36,13 +36,13 @@ const Filters = () => {
|
||||
text: 'ID',
|
||||
},
|
||||
]}
|
||||
onChange={(v) => console.log(v)}
|
||||
onChange={(v) => {}}
|
||||
/>
|
||||
|
||||
<FilterDropDown
|
||||
items={items}
|
||||
defaultState={[]}
|
||||
onChange={(values) => console.log(values)}
|
||||
onChange={(values) => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user