news & things
This commit is contained in:
@ -19,6 +19,7 @@ import { useNavigate } from "react-router-dom";
|
||||
const AuthModal = (props: {
|
||||
open: boolean;
|
||||
setOpen: (arg0: boolean) => void;
|
||||
setDrawerOpen: (arg0: boolean) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const messageApi = useContext(MessageContext);
|
||||
@ -58,6 +59,7 @@ const AuthModal = (props: {
|
||||
.then(() => refetch())
|
||||
.then(() => props.setOpen(false))
|
||||
.then(() => navigate("/dashboard"))
|
||||
.then(() => props.setDrawerOpen(false))
|
||||
.catch(() => messageApi.error(tr("Login failed!")));
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
GlobalOutlined,
|
||||
LogoutOutlined,
|
||||
MenuOutlined,
|
||||
PicCenterOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
@ -100,6 +101,11 @@ const HeaderComponent = () => {
|
||||
icon: <DesktopOutlined />,
|
||||
disabled: !user,
|
||||
},
|
||||
{
|
||||
label: <Link to="/news">{tr("News")}</Link>,
|
||||
key: "news",
|
||||
icon: <PicCenterOutlined />,
|
||||
},
|
||||
{
|
||||
label: tr("Language"),
|
||||
key: "language",
|
||||
@ -111,7 +117,7 @@ const HeaderComponent = () => {
|
||||
label: user ? user.username : tr("Log in"),
|
||||
key: "login",
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => setAuthModalOpen(true),
|
||||
onClick: () => !user && setAuthModalOpen(true),
|
||||
...(user ? { children: userMenuItems } : {}),
|
||||
},
|
||||
];
|
||||
@ -139,6 +145,13 @@ const HeaderComponent = () => {
|
||||
key: "dashboard",
|
||||
icon: <DesktopOutlined />,
|
||||
disabled: !user,
|
||||
onClick: () => setDrawerOpen(false),
|
||||
},
|
||||
{
|
||||
label: <Link to="/news">{tr("News")}</Link>,
|
||||
key: "news",
|
||||
icon: <PicCenterOutlined />,
|
||||
onClick: () => setDrawerOpen(false),
|
||||
},
|
||||
{
|
||||
label: tr("Language"),
|
||||
@ -151,14 +164,18 @@ const HeaderComponent = () => {
|
||||
label: user ? user.username : tr("Log in"),
|
||||
key: "login",
|
||||
icon: <UserOutlined />,
|
||||
onClick: () => setAuthModalOpen(true),
|
||||
onClick: () => !user && setAuthModalOpen(true),
|
||||
...(user ? { children: userMenuItems } : {}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthModal open={authModalOpen} setOpen={setAuthModalOpen} />
|
||||
<AuthModal
|
||||
open={authModalOpen}
|
||||
setOpen={setAuthModalOpen}
|
||||
setDrawerOpen={setDrawerOpen}
|
||||
/>
|
||||
<Header className="header">
|
||||
<Menu
|
||||
theme="dark"
|
||||
|
||||
73
frontend/app/src/components/news/CreateNewsCard.tsx
Normal file
73
frontend/app/src/components/news/CreateNewsCard.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useContext } from "react";
|
||||
import "../styles.css";
|
||||
import { Button, Form, Input, Spin } from "antd";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import tr from "../../config/translation";
|
||||
import { MessageContext } from "../../App";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
import { CreateNewsRequest, useCreateNewsMutation } from "../../slice/NewsApi";
|
||||
|
||||
const CreateNewsCard = (): JSX.Element => {
|
||||
const messageApi = useContext(MessageContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [createNews, { isLoading }] = useCreateNewsMutation();
|
||||
|
||||
const submit = (formData: CreateNewsRequest) => {
|
||||
createNews(formData)
|
||||
.unwrap()
|
||||
.then(() => navigate(`/news`))
|
||||
.then(() => messageApi.success(tr("News created")))
|
||||
.catch(() => messageApi.error(tr("Failed to create news")));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<Spin spinning={isLoading}>
|
||||
<Title level={2}>{tr("New news")}</Title>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={(formData: CreateNewsRequest) => submit(formData)}
|
||||
>
|
||||
<Form.Item
|
||||
name={"title"}
|
||||
label={tr("Title")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: tr("Please input news title!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={"content"}
|
||||
label={tr("Content")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: tr("Please input news content!"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Button
|
||||
style={{ width: "100%" }}
|
||||
icon={<PlusCircleOutlined />}
|
||||
type="primary"
|
||||
onClick={() => form.submit()}
|
||||
>
|
||||
{tr("Publish")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default CreateNewsCard;
|
||||
40
frontend/app/src/components/news/NewsListCard.tsx
Normal file
40
frontend/app/src/components/news/NewsListCard.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import "../styles.css";
|
||||
import { Card, Spin } from "antd";
|
||||
import { News, useGetNewsQuery } from "../../slice/NewsApi";
|
||||
import { ClockCircleOutlined } from "@ant-design/icons";
|
||||
|
||||
const formatTime = (s: string) => {
|
||||
const d = new Date(s);
|
||||
return d.toLocaleString();
|
||||
};
|
||||
|
||||
const NewsListCard = (): JSX.Element => {
|
||||
const { data, isLoading } = useGetNewsQuery({});
|
||||
return (
|
||||
<div className="card">
|
||||
<Spin spinning={isLoading}>
|
||||
{data &&
|
||||
data.map((news: News) => (
|
||||
<Card
|
||||
title={news.title}
|
||||
key={news.id}
|
||||
style={{ width: "100%", marginBottom: "1rem" }}
|
||||
bordered={false}
|
||||
>
|
||||
<p style={{ textAlign: "left", color: "white" }}>
|
||||
{news.content}
|
||||
</p>
|
||||
<br />
|
||||
<div className="news-footer">
|
||||
<ClockCircleOutlined />
|
||||
<br />
|
||||
<p style={{ marginLeft: "1rem" }}>{formatTime(news.created)}</p>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default NewsListCard;
|
||||
@ -91,3 +91,9 @@
|
||||
transform: rotate(0.05turn);
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
.news-footer {
|
||||
color: grey;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { AuthApi, User } from "../slice/AuthApi";
|
||||
import { QueueApi } from "../slice/QueueApi";
|
||||
import { NewsApi } from "../slice/NewsApi";
|
||||
|
||||
export type AuthDataType = {
|
||||
token: string | null;
|
||||
@ -45,6 +46,7 @@ export const store = configureStore({
|
||||
// Add the generated reducer as a specific top-level slice
|
||||
[AuthApi.reducerPath]: AuthApi.reducer,
|
||||
[QueueApi.reducerPath]: QueueApi.reducer,
|
||||
[NewsApi.reducerPath]: NewsApi.reducer,
|
||||
auth: createReducer(initialAuthDataState, (builder) => {
|
||||
builder.addCase(updateToken, (state, action) => {
|
||||
state.token = action.payload;
|
||||
@ -85,7 +87,8 @@ export const store = configureStore({
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware()
|
||||
.concat(AuthApi.middleware)
|
||||
.concat(QueueApi.middleware),
|
||||
.concat(QueueApi.middleware)
|
||||
.concat(NewsApi.middleware),
|
||||
});
|
||||
|
||||
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
|
||||
|
||||
@ -94,5 +94,8 @@
|
||||
},
|
||||
"Failed to create queue": {
|
||||
"ru": "Не удалось создать очередь"
|
||||
},
|
||||
"News": {
|
||||
"ru": "Новости"
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ import DashboardPage from "./DashboardPage";
|
||||
import PropTypes from "prop-types";
|
||||
import NotFoundPage from "./NotFoundPage";
|
||||
import NewQueuePage from "./NewQueuePage";
|
||||
import NewsPage from "./NewsPage";
|
||||
import CreateNewsPage from "./CreateNewsPage";
|
||||
|
||||
const AppRoutes = ({ children }: { children: ReactNode }) => {
|
||||
store.dispatch(getLocalToken());
|
||||
@ -18,6 +20,8 @@ const AppRoutes = ({ children }: { children: ReactNode }) => {
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/dashboard/new" element={<NewQueuePage />} />
|
||||
<Route path="/news" element={<NewsPage />} />
|
||||
<Route path="/news/new" element={<CreateNewsPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
17
frontend/app/src/pages/CreateNewsPage.tsx
Normal file
17
frontend/app/src/pages/CreateNewsPage.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import "./styles.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../config/store";
|
||||
import CreateNewsCard from "../components/news/CreateNewsCard";
|
||||
import NotFoundPage from "./NotFoundPage";
|
||||
|
||||
const CreateNewsPage = () => {
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
return user && user.username === "admin" ? (
|
||||
<CreateNewsCard />
|
||||
) : (
|
||||
<NotFoundPage />
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateNewsPage;
|
||||
9
frontend/app/src/pages/NewsPage.tsx
Normal file
9
frontend/app/src/pages/NewsPage.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
import "./styles.css";
|
||||
import NewsListCard from "../components/news/NewsListCard";
|
||||
|
||||
const NewsPage = () => {
|
||||
return <NewsListCard />;
|
||||
};
|
||||
|
||||
export default NewsPage;
|
||||
43
frontend/app/src/slice/NewsApi.ts
Normal file
43
frontend/app/src/slice/NewsApi.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseUrl } from "../config/baseUrl";
|
||||
import { RootState } from "../config/store";
|
||||
|
||||
export type CreateNewsRequest = {
|
||||
title: string;
|
||||
content: string | null;
|
||||
};
|
||||
|
||||
export type News = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
created: string;
|
||||
};
|
||||
|
||||
export const NewsApi = createApi({
|
||||
reducerPath: "NewsApi",
|
||||
baseQuery: fetchBaseQuery({
|
||||
baseUrl: `${baseUrl}/news`,
|
||||
prepareHeaders: (headers, { getState }) => {
|
||||
const token = (getState() as RootState).auth.token;
|
||||
if (token) {
|
||||
headers.set("authorization", `Bearer ${token}`);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
}),
|
||||
endpoints: (builder) => ({
|
||||
getNews: builder.query({
|
||||
query: () => "/",
|
||||
}),
|
||||
createNews: builder.mutation({
|
||||
query: (data: CreateNewsRequest) => ({
|
||||
url: "/",
|
||||
method: "POST",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const { useGetNewsQuery, useCreateNewsMutation } = NewsApi;
|
||||
Reference in New Issue
Block a user