diff --git a/backend/app/views/auth/api.py b/backend/app/views/auth/api.py
index 22cd634..bd0e893 100644
--- a/backend/app/views/auth/api.py
+++ b/backend/app/views/auth/api.py
@@ -86,6 +86,13 @@ async def get_anon_user(
return anon_user
+@router.patch("/anon")
+async def get_anon_user(
+ anon_user: Annotated[schemas.AnonUser, Depends(services.patch_anon_name)]
+) -> schemas.AnonUser:
+ return anon_user
+
+
@router.get(
"/captcha/{captcha_id}",
responses={200: {"content": {"image/png": {}}}},
diff --git a/backend/app/views/auth/schemas.py b/backend/app/views/auth/schemas.py
index 98d2cae..826ef08 100644
--- a/backend/app/views/auth/schemas.py
+++ b/backend/app/views/auth/schemas.py
@@ -48,3 +48,7 @@ class AnonUser(BaseModel):
class Config:
from_attributes = True
+
+
+class AnonUserPatch(BaseModel):
+ name: str
diff --git a/backend/app/views/auth/services.py b/backend/app/views/auth/services.py
index 92a81a5..112d7e6 100644
--- a/backend/app/views/auth/services.py
+++ b/backend/app/views/auth/services.py
@@ -147,6 +147,27 @@ def get_anon_user(
return create_anon_user(db)
+def patch_anon_name(
+ data: schemas.AnonUserPatch,
+ db: Annotated[Session, Depends(get_db)],
+ x_client_id: Annotated[Union[str, None], Header()] = None,
+) -> schemas.AnonUser:
+ if x_client_id:
+ anon = (
+ db.query(models.AnonymousUser)
+ .filter(models.AnonymousUser.id == x_client_id)
+ .first()
+ )
+ if anon:
+ setattr(anon, "name", data.name)
+ db.commit()
+ return anon
+ raise HTTPException(
+ status_code=status.HTTP_418_IM_A_TEAPOT,
+ )
+ return create_anon_user(db)
+
+
def get_captcha(
captcha_id: uuid.UUID, db: Annotated[Session, Depends(get_db)]
) -> BytesIO:
diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json
index 5dc3000..4596ed6 100644
--- a/frontend/app/package-lock.json
+++ b/frontend/app/package-lock.json
@@ -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"
},
diff --git a/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx b/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx
index 13c8822..905c612 100644
--- a/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx
+++ b/frontend/app/src/components/queue/ApproveQueueJoinCard.tsx
@@ -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 (
@@ -67,7 +77,23 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
{data?.participants?.remaining} / {data?.participants?.total}
- } onClick={onJoinButtonClick}>
+ {tr("Update your name")}
+
+ setNewName(e.target.value)}
+ placeholder={tr("Enter new name")}
+ />
+
+
+ }
+ onClick={onJoinButtonClick}
+ >
{tr("Join")}
diff --git a/frontend/app/src/components/queue/QueueCard.tsx b/frontend/app/src/components/queue/QueueCard.tsx
index 58a14ba..f50fee0 100644
--- a/frontend/app/src/components/queue/QueueCard.tsx
+++ b/frontend/app/src/components/queue/QueueCard.tsx
@@ -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 (
-
-
}
- spinning={isFetching}
- >
-
-
- {data?.name}
-
-
-
- {" "}
- {data?.description}
-
-
-
- {" "}
- {data?.participants?.remaining} / {data?.participants?.total}
-
- {data && getStatusText(data.status)}
- {user && user.id && (
-
-
- {data?.status === "created" && (
-
- )}
+ 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 (
+
+
}
+ spinning={isFetching}
+ >
+
+
+ {data?.name}
+
+
+
+ {" "}
+ {data?.description}
+
+
+
+ {" "}
+ {data?.participants?.remaining} / {data?.participants?.total}
+
+ {data && getStatusText(data.status)}
+ {data && user && user.id === data.owner_id && (
+
+
+ {data?.status === "created" && (
+
+ )}
+
+ {qrShown ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {data && qrShown && (
+
+
+ : }
+ onClick={() => setLargeQr((v) => !v)}
+ />
)}
-
-
-
- {tr("Queue participants")}
-
- {data?.participants.users_list.map((v) => {
- return
;
- })}
-
-
-
+
+
+
+ {tr("Queue participants")}
+
+ {data?.participants.users_list.map((v) => {
+ return
;
+ })}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
{tr("Queue not found")}
+
404
+ >
);
};
export default QueueCard;
diff --git a/frontend/app/src/components/user/AnonUserCard.tsx b/frontend/app/src/components/user/AnonUserCard.tsx
index 1f0389d..4b94f14 100644
--- a/frontend/app/src/components/user/AnonUserCard.tsx
+++ b/frontend/app/src/components/user/AnonUserCard.tsx
@@ -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" && (
)}
{props.queueUser &&
- clientId === props.queueUser.user.id &&
- props.queueUser.position === 0 && (
-
- )}
+ clientId === props.queueUser.user.id &&
+ props.queueUser.position === 0 &&
+ props.queue?.status === "active" ? (
+
+ ) : (
+
+ )}
);
diff --git a/frontend/app/src/config/baseUrl.ts b/frontend/app/src/config/baseUrl.ts
index 78bd332..0604e5e 100644
--- a/frontend/app/src/config/baseUrl.ts
+++ b/frontend/app/src/config/baseUrl.ts
@@ -1 +1,2 @@
export const baseUrl = `${window.location.protocol}//${window.location.host}/api`;
+export const baseClientUrl = `${window.location.protocol}//${window.location.host}`;
diff --git a/frontend/app/src/config/store.ts b/frontend/app/src/config/store.ts
index ef37368..6afacc2 100644
--- a/frontend/app/src/config/store.ts
+++ b/frontend/app/src/config/store.ts
@@ -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";
}
});
}),
diff --git a/frontend/app/src/config/style.ts b/frontend/app/src/config/style.ts
index a46e01a..5b240bb 100644
--- a/frontend/app/src/config/style.ts
+++ b/frontend/app/src/config/style.ts
@@ -15,6 +15,7 @@ export const darkTheme: ThemeConfig = {
components: {
Input: {
activeBorderColor: "#001529",
+ colorTextPlaceholder: "grey",
},
},
};
diff --git a/frontend/app/src/config/translationMap.json b/frontend/app/src/config/translationMap.json
index e91d10f..74d7a94 100644
--- a/frontend/app/src/config/translationMap.json
+++ b/frontend/app/src/config/translationMap.json
@@ -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": "Вы уже присоединились к этой очереди"
}
}
diff --git a/frontend/app/src/slice/AuthApi.ts b/frontend/app/src/slice/AuthApi.ts
index 99f0182..cd4d488 100644
--- a/frontend/app/src/slice/AuthApi.ts
+++ b/frontend/app/src/slice/AuthApi.ts
@@ -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;
diff --git a/frontend/app/src/slice/QueueApi.ts b/frontend/app/src/slice/QueueApi.ts
index 33256be..75fd0a6 100644
--- a/frontend/app/src/slice/QueueApi.ts
+++ b/frontend/app/src/slice/QueueApi.ts
@@ -19,6 +19,7 @@ export type QueueDetail = {
name: string;
description: string | null;
status: string;
+ owner_id: string;
participants: {
total: number;
remaining: number;