upd
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
DesktopOutlined,
|
||||
GlobalOutlined,
|
||||
LogoutOutlined,
|
||||
@ -113,6 +114,11 @@ const HeaderComponent = () => {
|
||||
icon: <DesktopOutlined />,
|
||||
disabled: !user,
|
||||
},
|
||||
{
|
||||
label: <Link to="/parting">{tr("Current queues")}</Link>,
|
||||
key: "parting",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
{
|
||||
label: <Link to="/news">{tr("News")}</Link>,
|
||||
key: "news",
|
||||
@ -171,6 +177,11 @@ const HeaderComponent = () => {
|
||||
disabled: !user,
|
||||
onClick: () => setDrawerOpen(false),
|
||||
},
|
||||
{
|
||||
label: <Link to="/parting">{tr("Current queues")}</Link>,
|
||||
key: "parting",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
{
|
||||
label: <Link to="/news">{tr("News")}</Link>,
|
||||
key: "news",
|
||||
|
||||
@ -31,11 +31,9 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
||||
const onJoinButtonClick = () => {
|
||||
joinQueue(props.id)
|
||||
.unwrap()
|
||||
.then(() =>
|
||||
messageApi.success(tr("Successfully joined queue ") + data.name)
|
||||
)
|
||||
.then(() => navigate(`/queue/${props.id}`))
|
||||
.then(() => refetch())
|
||||
.then(() => messageApi.success(tr("Successfully joined queue")))
|
||||
.catch((e) => messageApi.error(tr(e.data.detail)));
|
||||
};
|
||||
|
||||
|
||||
60
frontend/app/src/components/queue/PartingQueuesList.tsx
Normal file
60
frontend/app/src/components/queue/PartingQueuesList.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useGetQueuesQuery } from "../../slice/QueueApi";
|
||||
import "../styles.css";
|
||||
import { Button, Spin } from "antd";
|
||||
import { LoadingOutlined, RightOutlined } from "@ant-design/icons";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import tr from "../../config/translation";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../../config/store";
|
||||
|
||||
type Queue = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const PartingQueuesList = (): JSX.Element => {
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
const { data, refetch, isLoading } = useGetQueuesQuery({});
|
||||
useEffect(() => {
|
||||
user && refetch();
|
||||
}, [user]);
|
||||
return (
|
||||
<div className="card">
|
||||
<Title level={2}>{tr("Queues you are in")}</Title>
|
||||
<br />
|
||||
<br />
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
|
||||
spinning={isLoading}
|
||||
>
|
||||
{data?.length ? (
|
||||
data?.map((ele: Queue) => (
|
||||
<div className="card secondary queue-in-list" key={ele.id}>
|
||||
<Title level={4}>{ele.name}</Title>
|
||||
<Link to={`/queue/${ele.id}`}>
|
||||
<Button
|
||||
icon={<RightOutlined />}
|
||||
type="primary"
|
||||
size="large"
|
||||
style={{ height: "100%", width: "4rem" }}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Title level={3}>{tr("You are not parting in any queues!")}</Title>
|
||||
<div className="button-box">
|
||||
<Link to="/queue/join">
|
||||
<Button size="large">{tr("Join a queue")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PartingQueuesList;
|
||||
@ -1,26 +1,99 @@
|
||||
import React from "react";
|
||||
import { useGetQueueDetailQuery } from "../../slice/QueueApi";
|
||||
import React, { useContext } from "react";
|
||||
import {
|
||||
useGetQueueDetailQuery,
|
||||
useKickFirstActionMutation,
|
||||
useStartQueueActionMutation,
|
||||
} from "../../slice/QueueApi";
|
||||
import "../styles.css";
|
||||
import { Button, Spin } from "antd";
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileTextOutlined,
|
||||
FlagOutlined,
|
||||
HourglassOutlined,
|
||||
LoadingOutlined,
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import tr from "../../config/translation";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import AnonUserCard from "../user/AnonUserCard";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../../config/store";
|
||||
import AnonUserCard from "../user/AnonUserCard";
|
||||
import { MessageContext } from "../../App";
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case "created":
|
||||
return (
|
||||
<p>
|
||||
<PlusCircleOutlined /> {" "}
|
||||
{tr("Created")}
|
||||
</p>
|
||||
);
|
||||
case "waiting":
|
||||
return (
|
||||
<p>
|
||||
<HourglassOutlined /> {" "}
|
||||
{tr("Waiting for start")}
|
||||
</p>
|
||||
);
|
||||
case "active":
|
||||
return (
|
||||
<p>
|
||||
<FieldTimeOutlined /> {" "}
|
||||
{tr("In progress")}
|
||||
</p>
|
||||
);
|
||||
case "finished":
|
||||
return (
|
||||
<p>
|
||||
<FlagOutlined /> {" "}
|
||||
{tr("Finished")}
|
||||
</p>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>
|
||||
<QuestionCircleOutlined /> {" "}
|
||||
{tr("Unknown status")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const QueueCard = (): JSX.Element => {
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
const messageApi = useContext(MessageContext);
|
||||
|
||||
const { queueId } = useParams();
|
||||
const { data, isFetching } = useGetQueueDetailQuery(queueId, {
|
||||
const { data, isFetching, refetch } = useGetQueueDetailQuery(queueId, {
|
||||
skip: !queueId,
|
||||
});
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
const [kickFirstAction] = useKickFirstActionMutation();
|
||||
const [startQueueAction] = useStartQueueActionMutation();
|
||||
|
||||
const kickFirst = () => {
|
||||
if (queueId) {
|
||||
kickFirstAction(queueId)
|
||||
.unwrap()
|
||||
.then(() => refetch())
|
||||
.then(() => messageApi.success(tr("First user in queue kicked!")))
|
||||
.catch(() => messageApi.error(tr("Action error")));
|
||||
}
|
||||
};
|
||||
|
||||
const startQueue = () => {
|
||||
if (queueId) {
|
||||
startQueueAction(queueId)
|
||||
.unwrap()
|
||||
.then(() => refetch())
|
||||
.then(() => messageApi.success(tr("Queue has started!")))
|
||||
.catch(() => messageApi.error(tr("Action error")));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
@ -42,13 +115,22 @@ const QueueCard = (): JSX.Element => {
|
||||
{" "}
|
||||
{data?.participants?.remaining} / {data?.participants?.total}
|
||||
</p>
|
||||
{data && getStatusText(data.status)}
|
||||
{user && user.id && (
|
||||
<div style={{ display: "flex", flexFlow: "row wrap" }}>
|
||||
<Button onClick={kickFirst}>{tr("Kick first")}</Button>
|
||||
{data?.status === "created" && (
|
||||
<Button onClick={startQueue}>{tr("Start queue")}</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Title level={3} style={{ textAlign: "left" }}>
|
||||
{tr("Queue participants")}
|
||||
</Title>
|
||||
{data?.participants.users_list.map((v) => {
|
||||
return <AnonUserCard key={v.id} user={v} />;
|
||||
return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
|
||||
})}
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useGetQueuesQuery } from "../../slice/QueueApi";
|
||||
import { useGetOwnedQueuesQuery } from "../../slice/QueueApi";
|
||||
import "../styles.css";
|
||||
import { Button, Spin } from "antd";
|
||||
import {
|
||||
@ -21,7 +21,7 @@ type Queue = {
|
||||
|
||||
const QueuesList = (): JSX.Element => {
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
const { data, refetch, isLoading } = useGetQueuesQuery({});
|
||||
const { data, refetch, isLoading } = useGetOwnedQueuesQuery({});
|
||||
useEffect(() => {
|
||||
user && refetch();
|
||||
}, [user]);
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React from "react";
|
||||
import { AnonUser } from "../../slice/AuthApi";
|
||||
import React, { useContext } from "react";
|
||||
import { QueueUser } from "../../slice/AuthApi";
|
||||
import "../styles.css";
|
||||
import tr from "../../config/translation";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import Paragraph from "antd/es/typography/Paragraph";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../../config/store";
|
||||
import { Button } from "antd";
|
||||
import { Queue, usePassQueueActionMutation } from "../../slice/QueueApi";
|
||||
import { MessageContext } from "../../App";
|
||||
|
||||
const UUIDToColor = (uuid: string): string => {
|
||||
return (
|
||||
@ -20,24 +23,74 @@ const getProfileText = (name: string): string => {
|
||||
};
|
||||
|
||||
const AnonUserCard = (props: {
|
||||
user: AnonUser;
|
||||
backlight?: "self" | "active" | undefined;
|
||||
queueUser: QueueUser;
|
||||
queue: Queue | undefined;
|
||||
}): JSX.Element => {
|
||||
const messageApi = useContext(MessageContext);
|
||||
|
||||
const clientId = useSelector((state: StorePrototype) => state.auth.clientId);
|
||||
const [passAction] = usePassQueueActionMutation();
|
||||
|
||||
const passQueue = () => {
|
||||
if (props.queue) {
|
||||
passAction(props.queue.id)
|
||||
.unwrap()
|
||||
.then(() => messageApi.success(tr("You passed")))
|
||||
.catch(() => messageApi.error(tr("Failed to pass")));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="anon-card">
|
||||
<span style={{ marginRight: "1rem" }}>#{props.queueUser.position}</span>
|
||||
<div
|
||||
className="anon-circle"
|
||||
style={{ background: UUIDToColor(props.user.id) }}
|
||||
style={{ background: UUIDToColor(props.queueUser.user.id) }}
|
||||
>
|
||||
{props.user.name
|
||||
? getProfileText(props.user.name)
|
||||
: props.user.id.substring(0, 2)}
|
||||
{props.queueUser.user.name
|
||||
? getProfileText(props.queueUser.user.name)
|
||||
: props.queueUser.id.substring(0, 2)}
|
||||
</div>
|
||||
<p color="white" style={{ marginLeft: "10px" }}>
|
||||
{props.user.name
|
||||
? props.user.name
|
||||
: tr("Anonymous") + " #" + props.user.id.substring(0, 4)}
|
||||
{props.queueUser.user.name
|
||||
? props.queueUser.user.name
|
||||
: tr("Anonymous") + " #" + props.queueUser.id.substring(0, 4)}
|
||||
</p>
|
||||
{props.queueUser && clientId === props.queueUser.user.id && (
|
||||
<span
|
||||
style={{
|
||||
background: "#00d8a4",
|
||||
marginLeft: "1rem",
|
||||
marginRight: "1rem",
|
||||
marginBottom: "0",
|
||||
borderRadius: "5px",
|
||||
padding: "2px",
|
||||
}}
|
||||
>
|
||||
{tr("YOU")}
|
||||
</span>
|
||||
)}
|
||||
{props.queueUser &&
|
||||
clientId === props.queueUser.user.id &&
|
||||
props.queueUser.position === 0 && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "1rem",
|
||||
marginRight: "1rem",
|
||||
marginBottom: "0",
|
||||
borderRadius: "5px",
|
||||
padding: "2px",
|
||||
}}
|
||||
>
|
||||
{tr("It is your turn!")}
|
||||
</span>
|
||||
)}
|
||||
{props.queueUser &&
|
||||
clientId === props.queueUser.user.id &&
|
||||
props.queueUser.position === 0 && (
|
||||
<Button onClick={() => passQueue()}>{tr("Pass")}</Button>
|
||||
)}
|
||||
<p></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ import { NewsApi } from "../slice/NewsApi";
|
||||
export type AuthDataType = {
|
||||
token: string | null;
|
||||
clientId: string | null;
|
||||
user: { name: string | null; username: string } | null;
|
||||
user: { id: string | null; name: string | null; username: string } | null;
|
||||
};
|
||||
|
||||
const initialAuthDataState: AuthDataType = {
|
||||
|
||||
@ -115,5 +115,56 @@
|
||||
},
|
||||
"Invalid captcha": {
|
||||
"ru": "Неверная капча"
|
||||
},
|
||||
"YOU": {
|
||||
"ru": "ВЫ"
|
||||
},
|
||||
"Current queues": {
|
||||
"ru": "Текущие очереди"
|
||||
},
|
||||
"Queues you are in": {
|
||||
"ru": "Очереди, в которых вы участвуете"
|
||||
},
|
||||
"Queue participants": {
|
||||
"ru": "Участники очереди"
|
||||
},
|
||||
"Anonymous": {
|
||||
"ru": "Аноним"
|
||||
},
|
||||
"Join": {
|
||||
"ru": "Присоединиться"
|
||||
},
|
||||
"QR-code scanner in development": {
|
||||
"ru": "Сканер QR-кодов в разработке"
|
||||
},
|
||||
"OR": {
|
||||
"ru": "ИЛИ"
|
||||
},
|
||||
"Join queue by link or id": {
|
||||
"ru": "Присоединиться к очереди по ссылке или айди"
|
||||
},
|
||||
"You are not parting in any queues!": {
|
||||
"ru": "Вы не принимаете участие ни в одной очереди!"
|
||||
},
|
||||
"Created": {
|
||||
"ru": "Создана"
|
||||
},
|
||||
"Waiting": {
|
||||
"ru": "Ожидает"
|
||||
},
|
||||
"In progress": {
|
||||
"ru": "В процессе"
|
||||
},
|
||||
"Finished": {
|
||||
"ru": "Завершена"
|
||||
},
|
||||
"Kick first": {
|
||||
"ru": "Кикнуть первого"
|
||||
},
|
||||
"It is your turn!": {
|
||||
"ru": "Сейчас ваша очередь!"
|
||||
},
|
||||
"Pass": {
|
||||
"ru": "Пройти"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import CreateNewsPage from "./CreateNewsPage";
|
||||
import QueueCard from "../components/queue/QueueCard";
|
||||
import JoinQueuePage from "./JoinQueuePage";
|
||||
import ApproveQueueJoinPage from "./ApproveQueueJoinPage";
|
||||
import PartingQueuesPage from "./PartingQueuesPage";
|
||||
|
||||
const AppRoutes = ({ children }: { children: ReactNode }) => {
|
||||
store.dispatch(getLocalToken());
|
||||
@ -30,6 +31,7 @@ const AppRoutes = ({ children }: { children: ReactNode }) => {
|
||||
<Routes>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/dashboard" element={<DashboardPage />} />
|
||||
<Route path="/parting" element={<PartingQueuesPage />} />
|
||||
<Route path="/queue/:queueId" element={<QueueCard />} />
|
||||
<Route path="/queue/join" element={<JoinQueuePage />} />
|
||||
<Route path="/queue/join/:queueId" element={<ApproveQueueJoinPage />} />
|
||||
|
||||
18
frontend/app/src/pages/PartingQueuesPage.tsx
Normal file
18
frontend/app/src/pages/PartingQueuesPage.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import "./styles.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../config/store";
|
||||
import tr from "../config/translation";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import PartingQueuesList from "../components/queue/PartingQueuesList";
|
||||
|
||||
const PartingQueuesPage = () => {
|
||||
const clientId = useSelector((state: StorePrototype) => state.auth.clientId);
|
||||
return clientId ? (
|
||||
<PartingQueuesList />
|
||||
) : (
|
||||
<Title level={2}>{tr("Whoops!")}</Title>
|
||||
);
|
||||
};
|
||||
|
||||
export default PartingQueuesPage;
|
||||
@ -3,6 +3,7 @@ import { baseUrl } from "../config/baseUrl";
|
||||
import { RootState } from "../config/store";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
name: string;
|
||||
}
|
||||
@ -24,6 +25,13 @@ export type TokenResponse = {
|
||||
token_type: string;
|
||||
};
|
||||
|
||||
export type QueueUser = {
|
||||
id: string;
|
||||
position: number;
|
||||
passed: boolean;
|
||||
user: AnonUser;
|
||||
};
|
||||
|
||||
export type AnonUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
|
||||
import { baseUrl } from "../config/baseUrl";
|
||||
import { RootState } from "../config/store";
|
||||
import { AnonUser } from "./AuthApi";
|
||||
import { QueueUser } from "./AuthApi";
|
||||
|
||||
export type CreateQueueRequest = {
|
||||
name: string;
|
||||
@ -18,10 +18,11 @@ export type QueueDetail = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
participants: {
|
||||
total: number;
|
||||
remaining: number;
|
||||
users_list: [AnonUser];
|
||||
users_list: [QueueUser];
|
||||
};
|
||||
};
|
||||
|
||||
@ -42,9 +43,12 @@ export const QueueApi = createApi({
|
||||
},
|
||||
}),
|
||||
endpoints: (builder) => ({
|
||||
getQueues: builder.query<[Queue], undefined>({
|
||||
getQueues: builder.query<[Queue], unknown>({
|
||||
query: () => "/",
|
||||
}),
|
||||
getOwnedQueues: builder.query<[Queue], unknown>({
|
||||
query: () => "/owned",
|
||||
}),
|
||||
getQueueDetail: builder.query<QueueDetail, string | undefined>({
|
||||
query: (queueId: string | undefined) => `/${queueId}`,
|
||||
}),
|
||||
@ -58,12 +62,34 @@ export const QueueApi = createApi({
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
passQueueAction: builder.mutation({
|
||||
query: (queueId: string) => ({
|
||||
url: `/${queueId}/action/pass`,
|
||||
method: "POST",
|
||||
}),
|
||||
}),
|
||||
kickFirstAction: builder.mutation({
|
||||
query: (queueId: string) => ({
|
||||
url: `/${queueId}/action/kick-first`,
|
||||
method: "POST",
|
||||
}),
|
||||
}),
|
||||
startQueueAction: builder.mutation({
|
||||
query: (queueId: string) => ({
|
||||
url: `/${queueId}/action/start`,
|
||||
method: "POST",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetQueuesQuery,
|
||||
useGetOwnedQueuesQuery,
|
||||
useGetQueueDetailQuery,
|
||||
useJoinQueueMutation,
|
||||
useCreateQueueMutation,
|
||||
usePassQueueActionMutation,
|
||||
useKickFirstActionMutation,
|
||||
useStartQueueActionMutation,
|
||||
} = QueueApi;
|
||||
|
||||
Reference in New Issue
Block a user