upd
This commit is contained in:
18
frontend/app/package-lock.json
generated
18
frontend/app/package-lock.json
generated
@ -1259,12 +1259,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -2045,10 +2046,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@ -2661,6 +2663,7 @@
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@ -4527,6 +4530,7 @@
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import "../styles.css";
|
||||
import { Button, Spin } from "antd";
|
||||
import { Button, Input, Spin } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
FileTextOutlined,
|
||||
@ -15,6 +15,7 @@ import {
|
||||
useJoinQueueMutation,
|
||||
} from "../../slice/QueueApi";
|
||||
import { MessageContext } from "../../App";
|
||||
import { usePatchAnonMutation } from "../../slice/AuthApi";
|
||||
|
||||
const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
||||
const navigate = useNavigate();
|
||||
@ -27,6 +28,8 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
||||
}
|
||||
);
|
||||
const [joinQueue, { isLoading }] = useJoinQueueMutation();
|
||||
const [patchAnon] = usePatchAnonMutation();
|
||||
const [newName, setNewName] = useState("");
|
||||
|
||||
const onJoinButtonClick = () => {
|
||||
joinQueue(props.id)
|
||||
@ -37,6 +40,13 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
||||
.catch((e) => messageApi.error(tr(e.data.detail)));
|
||||
};
|
||||
|
||||
const patchName = () => {
|
||||
patchAnon({ name: newName })
|
||||
.unwrap()
|
||||
.then(() => messageApi.success(tr("Successfully changed name")))
|
||||
.catch(() => messageApi.error(tr("Error changing name")));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<Spin spinning={isFetching}>
|
||||
@ -67,7 +77,23 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
|
||||
{data?.participants?.remaining} / {data?.participants?.total}
|
||||
</p>
|
||||
<Spin spinning={isLoading}>
|
||||
<Button icon={<PlusOutlined />} onClick={onJoinButtonClick}>
|
||||
<Title level={4}>{tr("Update your name")}</Title>
|
||||
<div style={{ display: "flex", flexFlow: "row", width: "30vw" }}>
|
||||
<Input
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
placeholder={tr("Enter new name")}
|
||||
/>
|
||||
<Button style={{ marginLeft: "1rem" }} onClick={patchName}>
|
||||
{tr("Update")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
style={{ width: "100%", marginTop: "2rem" }}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onJoinButtonClick}
|
||||
>
|
||||
{tr("Join")}
|
||||
</Button>
|
||||
</Spin>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React, { useContext } from "react";
|
||||
import React, { useContext, useState } from "react";
|
||||
import {
|
||||
useGetQueueDetailQuery,
|
||||
useKickFirstActionMutation,
|
||||
useStartQueueActionMutation,
|
||||
} from "../../slice/QueueApi";
|
||||
import "../styles.css";
|
||||
import { Button, Spin } from "antd";
|
||||
import { Button, QRCode, Spin } from "antd";
|
||||
import {
|
||||
FieldTimeOutlined,
|
||||
FileTextOutlined,
|
||||
@ -15,6 +15,8 @@ import {
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleOutlined,
|
||||
UserOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Title from "antd/es/typography/Title";
|
||||
import tr from "../../config/translation";
|
||||
@ -23,6 +25,7 @@ import AnonUserCard from "../user/AnonUserCard";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../../config/store";
|
||||
import { MessageContext } from "../../App";
|
||||
import { baseClientUrl } from "../../config/baseUrl";
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
@ -68,13 +71,16 @@ const QueueCard = (): JSX.Element => {
|
||||
const messageApi = useContext(MessageContext);
|
||||
|
||||
const { queueId } = useParams();
|
||||
const { data, isFetching, refetch } = useGetQueueDetailQuery(queueId, {
|
||||
const { data, isFetching, refetch, error } = useGetQueueDetailQuery(queueId, {
|
||||
skip: !queueId,
|
||||
});
|
||||
const user = useSelector((state: StorePrototype) => state.auth.user);
|
||||
const [kickFirstAction] = useKickFirstActionMutation();
|
||||
const [startQueueAction] = useStartQueueActionMutation();
|
||||
|
||||
const [qrShown, setQrShown] = useState(false);
|
||||
const [largeQr, setLargeQr] = useState(false);
|
||||
|
||||
const kickFirst = () => {
|
||||
if (queueId) {
|
||||
kickFirstAction(queueId)
|
||||
@ -95,46 +101,103 @@ const QueueCard = (): JSX.Element => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
|
||||
spinning={isFetching}
|
||||
>
|
||||
<div className="queue-info">
|
||||
<Title level={3} style={{ textAlign: "left" }}>
|
||||
{data?.name}
|
||||
</Title>
|
||||
<p>
|
||||
<FileTextOutlined />
|
||||
{" "}
|
||||
{data?.description}
|
||||
</p>
|
||||
<p>
|
||||
<UserOutlined />
|
||||
{" "}
|
||||
{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>
|
||||
)}
|
||||
const getJoinLink = () => {
|
||||
if (data) {
|
||||
return baseClientUrl + `/queue/join/${data.id}`;
|
||||
}
|
||||
return baseClientUrl;
|
||||
};
|
||||
|
||||
const copyJoinLink = async () => {
|
||||
if (data) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(getJoinLink());
|
||||
messageApi.success(tr("Copied!"));
|
||||
} catch (error) {
|
||||
messageApi.error(tr("Error occured!"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!error) {
|
||||
return (
|
||||
<div className="card">
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
|
||||
spinning={isFetching}
|
||||
>
|
||||
<div className="queue-info">
|
||||
<Title level={3} style={{ textAlign: "left" }}>
|
||||
{data?.name}
|
||||
</Title>
|
||||
<p>
|
||||
<FileTextOutlined />
|
||||
{" "}
|
||||
{data?.description}
|
||||
</p>
|
||||
<p>
|
||||
<UserOutlined />
|
||||
{" "}
|
||||
{data?.participants?.remaining} / {data?.participants?.total}
|
||||
</p>
|
||||
{data && getStatusText(data.status)}
|
||||
{data && user && user.id === data.owner_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>
|
||||
)}
|
||||
<Button onClick={copyJoinLink}>{tr("Copy join link")}</Button>
|
||||
{qrShown ? (
|
||||
<Button onClick={() => setQrShown(false)}>
|
||||
{tr("Hide QR-code")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => setQrShown(true)}>
|
||||
{tr("Show QR-code")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{data && qrShown && (
|
||||
<div style={{ display: "flex", flexFlow: "row nowrap" }}>
|
||||
<QRCode
|
||||
errorLevel="H"
|
||||
value={getJoinLink()}
|
||||
icon={
|
||||
baseClientUrl + "/static/image/android-chrome-512x512.png"
|
||||
}
|
||||
size={largeQr ? 320 : 160}
|
||||
/>
|
||||
<Button
|
||||
icon={largeQr ? <ZoomOutOutlined /> : <ZoomInOutlined />}
|
||||
onClick={() => setLargeQr((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Title level={3} style={{ textAlign: "left" }}>
|
||||
{tr("Queue participants")}
|
||||
</Title>
|
||||
{data?.participants.users_list.map((v) => {
|
||||
return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
|
||||
})}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Title level={3} style={{ textAlign: "left" }}>
|
||||
{tr("Queue participants")}
|
||||
</Title>
|
||||
{data?.participants.users_list.map((v) => {
|
||||
return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
|
||||
})}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ width: "100%", marginTop: "3rem" }}>
|
||||
<QuestionCircleOutlined style={{ fontSize: "5rem" }} />
|
||||
</div>
|
||||
<Title>{tr("Queue not found")}</Title>
|
||||
<Title level={3}>404</Title>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default QueueCard;
|
||||
|
||||
@ -5,7 +5,7 @@ import tr from "../../config/translation";
|
||||
import { useSelector } from "react-redux";
|
||||
import { StorePrototype } from "../../config/store";
|
||||
import { Button } from "antd";
|
||||
import { Queue, usePassQueueActionMutation } from "../../slice/QueueApi";
|
||||
import { QueueDetail, usePassQueueActionMutation } from "../../slice/QueueApi";
|
||||
import { MessageContext } from "../../App";
|
||||
|
||||
const UUIDToColor = (uuid: string): string => {
|
||||
@ -24,7 +24,7 @@ const getProfileText = (name: string): string => {
|
||||
|
||||
const AnonUserCard = (props: {
|
||||
queueUser: QueueUser;
|
||||
queue: Queue | undefined;
|
||||
queue: QueueDetail | undefined;
|
||||
}): JSX.Element => {
|
||||
const messageApi = useContext(MessageContext);
|
||||
|
||||
@ -72,7 +72,8 @@ const AnonUserCard = (props: {
|
||||
)}
|
||||
{props.queueUser &&
|
||||
clientId === props.queueUser.user.id &&
|
||||
props.queueUser.position === 0 && (
|
||||
props.queueUser.position === 0 &&
|
||||
props.queue?.status === "active" && (
|
||||
<span
|
||||
style={{
|
||||
marginLeft: "1rem",
|
||||
@ -86,10 +87,13 @@ const AnonUserCard = (props: {
|
||||
</span>
|
||||
)}
|
||||
{props.queueUser &&
|
||||
clientId === props.queueUser.user.id &&
|
||||
props.queueUser.position === 0 && (
|
||||
<Button onClick={() => passQueue()}>{tr("Pass")}</Button>
|
||||
)}
|
||||
clientId === props.queueUser.user.id &&
|
||||
props.queueUser.position === 0 &&
|
||||
props.queue?.status === "active" ? (
|
||||
<Button onClick={() => passQueue()}>{tr("Pass")}</Button>
|
||||
) : (
|
||||
<Button onClick={() => passQueue()}>{tr("Leave")}</Button>
|
||||
)}
|
||||
<p></p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export const baseUrl = `${window.location.protocol}//${window.location.host}/api`;
|
||||
export const baseClientUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
|
||||
@ -114,8 +114,9 @@ export const store = configureStore({
|
||||
if (theme) {
|
||||
state.theme = theme;
|
||||
} else {
|
||||
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
state.theme = darkThemeMq.matches ? "dark" : "light";
|
||||
// const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); // TODO
|
||||
// state.theme = darkThemeMq.matches ? "dark" : "light";
|
||||
state.theme = "dark";
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
@ -15,6 +15,7 @@ export const darkTheme: ThemeConfig = {
|
||||
components: {
|
||||
Input: {
|
||||
activeBorderColor: "#001529",
|
||||
colorTextPlaceholder: "grey",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -164,7 +164,46 @@
|
||||
"It is your turn!": {
|
||||
"ru": "Сейчас ваша очередь!"
|
||||
},
|
||||
"Start queue": {
|
||||
"ru": "Начать очередь"
|
||||
},
|
||||
"Copy join link": {
|
||||
"ru": "Скопировать ссылку для вступления"
|
||||
},
|
||||
"Show QR-code": {
|
||||
"ru": "Показать QR-код"
|
||||
},
|
||||
"Hide QR-code": {
|
||||
"ru": "Спрятать QR-код"
|
||||
},
|
||||
"Copied!": {
|
||||
"ru": "Скопировано!"
|
||||
},
|
||||
"Pass": {
|
||||
"ru": "Пройти"
|
||||
"ru": "Пройти очередь"
|
||||
},
|
||||
"Leave": {
|
||||
"ru": "Покинуть очередь"
|
||||
},
|
||||
"Queue not found": {
|
||||
"ru": "Очередь не найдена"
|
||||
},
|
||||
"Update your name": {
|
||||
"ru": "Обновите свое имя"
|
||||
},
|
||||
"Enter new name": {
|
||||
"ru": "Введите новое имя"
|
||||
},
|
||||
"Update": {
|
||||
"ru": "Обновить"
|
||||
},
|
||||
"Successfully joined queue": {
|
||||
"ru": "Успешное присоединение к очереди"
|
||||
},
|
||||
"Successfully changed name": {
|
||||
"ru": "Успешное изменение имени"
|
||||
},
|
||||
"Already joined": {
|
||||
"ru": "Вы уже присоединились к этой очереди"
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,10 @@ export type AnonUser = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AnonUserPatch = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const AuthApi = createApi({
|
||||
reducerPath: "AuthApi",
|
||||
baseQuery: fetchBaseQuery({
|
||||
@ -76,6 +80,13 @@ export const AuthApi = createApi({
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
patchAnon: builder.mutation({
|
||||
query: (data: AnonUserPatch) => ({
|
||||
url: "/anon",
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@ -84,4 +95,5 @@ export const {
|
||||
useGetClientQuery,
|
||||
useLoginMutation,
|
||||
useRegisterMutation,
|
||||
usePatchAnonMutation,
|
||||
} = AuthApi;
|
||||
|
||||
@ -19,6 +19,7 @@ export type QueueDetail = {
|
||||
name: string;
|
||||
description: string | null;
|
||||
status: string;
|
||||
owner_id: string;
|
||||
participants: {
|
||||
total: number;
|
||||
remaining: number;
|
||||
|
||||
Reference in New Issue
Block a user