This commit is contained in:
2024-06-12 15:54:56 +03:00
parent 3fb38cac3a
commit 2951a559bc
13 changed files with 245 additions and 61 deletions

View File

@ -86,6 +86,13 @@ async def get_anon_user(
return 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( @router.get(
"/captcha/{captcha_id}", "/captcha/{captcha_id}",
responses={200: {"content": {"image/png": {}}}}, responses={200: {"content": {"image/png": {}}}},

View File

@ -48,3 +48,7 @@ class AnonUser(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class AnonUserPatch(BaseModel):
name: str

View File

@ -147,6 +147,27 @@ def get_anon_user(
return create_anon_user(db) 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( def get_captcha(
captcha_id: uuid.UUID, db: Annotated[Session, Depends(get_db)] captcha_id: uuid.UUID, db: Annotated[Session, Depends(get_db)]
) -> BytesIO: ) -> BytesIO:

View File

@ -1259,12 +1259,13 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -2045,10 +2046,11 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -2661,6 +2663,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -4527,6 +4530,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },

View File

@ -1,6 +1,6 @@
import React, { useContext } from "react"; import React, { useContext, useState } from "react";
import "../styles.css"; import "../styles.css";
import { Button, Spin } from "antd"; import { Button, Input, Spin } from "antd";
import { import {
ArrowLeftOutlined, ArrowLeftOutlined,
FileTextOutlined, FileTextOutlined,
@ -15,6 +15,7 @@ import {
useJoinQueueMutation, useJoinQueueMutation,
} from "../../slice/QueueApi"; } from "../../slice/QueueApi";
import { MessageContext } from "../../App"; import { MessageContext } from "../../App";
import { usePatchAnonMutation } from "../../slice/AuthApi";
const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => { const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -27,6 +28,8 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
} }
); );
const [joinQueue, { isLoading }] = useJoinQueueMutation(); const [joinQueue, { isLoading }] = useJoinQueueMutation();
const [patchAnon] = usePatchAnonMutation();
const [newName, setNewName] = useState("");
const onJoinButtonClick = () => { const onJoinButtonClick = () => {
joinQueue(props.id) joinQueue(props.id)
@ -37,6 +40,13 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
.catch((e) => messageApi.error(tr(e.data.detail))); .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 ( return (
<div className="card"> <div className="card">
<Spin spinning={isFetching}> <Spin spinning={isFetching}>
@ -67,7 +77,23 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
{data?.participants?.remaining} / {data?.participants?.total} {data?.participants?.remaining} / {data?.participants?.total}
</p> </p>
<Spin spinning={isLoading}> <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")} {tr("Join")}
</Button> </Button>
</Spin> </Spin>

View File

@ -1,11 +1,11 @@
import React, { useContext } from "react"; import React, { useContext, useState } from "react";
import { import {
useGetQueueDetailQuery, useGetQueueDetailQuery,
useKickFirstActionMutation, useKickFirstActionMutation,
useStartQueueActionMutation, useStartQueueActionMutation,
} from "../../slice/QueueApi"; } from "../../slice/QueueApi";
import "../styles.css"; import "../styles.css";
import { Button, Spin } from "antd"; import { Button, QRCode, Spin } from "antd";
import { import {
FieldTimeOutlined, FieldTimeOutlined,
FileTextOutlined, FileTextOutlined,
@ -15,6 +15,8 @@ import {
PlusCircleOutlined, PlusCircleOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
UserOutlined, UserOutlined,
ZoomInOutlined,
ZoomOutOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Title from "antd/es/typography/Title"; import Title from "antd/es/typography/Title";
import tr from "../../config/translation"; import tr from "../../config/translation";
@ -23,6 +25,7 @@ import AnonUserCard from "../user/AnonUserCard";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { StorePrototype } from "../../config/store"; import { StorePrototype } from "../../config/store";
import { MessageContext } from "../../App"; import { MessageContext } from "../../App";
import { baseClientUrl } from "../../config/baseUrl";
const getStatusText = (status: string) => { const getStatusText = (status: string) => {
switch (status) { switch (status) {
@ -68,13 +71,16 @@ const QueueCard = (): JSX.Element => {
const messageApi = useContext(MessageContext); const messageApi = useContext(MessageContext);
const { queueId } = useParams(); const { queueId } = useParams();
const { data, isFetching, refetch } = useGetQueueDetailQuery(queueId, { const { data, isFetching, refetch, error } = useGetQueueDetailQuery(queueId, {
skip: !queueId, skip: !queueId,
}); });
const user = useSelector((state: StorePrototype) => state.auth.user); const user = useSelector((state: StorePrototype) => state.auth.user);
const [kickFirstAction] = useKickFirstActionMutation(); const [kickFirstAction] = useKickFirstActionMutation();
const [startQueueAction] = useStartQueueActionMutation(); const [startQueueAction] = useStartQueueActionMutation();
const [qrShown, setQrShown] = useState(false);
const [largeQr, setLargeQr] = useState(false);
const kickFirst = () => { const kickFirst = () => {
if (queueId) { if (queueId) {
kickFirstAction(queueId) kickFirstAction(queueId)
@ -95,46 +101,103 @@ const QueueCard = (): JSX.Element => {
} }
}; };
return ( const getJoinLink = () => {
<div className="card"> if (data) {
<Spin return baseClientUrl + `/queue/join/${data.id}`;
indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />} }
spinning={isFetching} return baseClientUrl;
> };
<div className="queue-info">
<Title level={3} style={{ textAlign: "left" }}> const copyJoinLink = async () => {
{data?.name} if (data) {
</Title> try {
<p> await navigator.clipboard.writeText(getJoinLink());
<FileTextOutlined /> messageApi.success(tr("Copied!"));
{" "} } catch (error) {
{data?.description} messageApi.error(tr("Error occured!"));
</p> }
<p> }
<UserOutlined /> };
{" "}
{data?.participants?.remaining} / {data?.participants?.total} if (!error) {
</p> return (
{data && getStatusText(data.status)} <div className="card">
{user && user.id && ( <Spin
<div style={{ display: "flex", flexFlow: "row wrap" }}> indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />}
<Button onClick={kickFirst}>{tr("Kick first")}</Button> spinning={isFetching}
{data?.status === "created" && ( >
<Button onClick={startQueue}>{tr("Start queue")}</Button> <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>
<div> <div>
<Title level={3} style={{ textAlign: "left" }}> <Title level={3} style={{ textAlign: "left" }}>
{tr("Queue participants")} {tr("Queue participants")}
</Title> </Title>
{data?.participants.users_list.map((v) => { {data?.participants.users_list.map((v) => {
return <AnonUserCard key={v.id} queueUser={v} queue={data} />; return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
})} })}
</div> </div>
</Spin> </Spin>
</div> </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; export default QueueCard;

View File

@ -5,7 +5,7 @@ import tr from "../../config/translation";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { StorePrototype } from "../../config/store"; import { StorePrototype } from "../../config/store";
import { Button } from "antd"; import { Button } from "antd";
import { Queue, usePassQueueActionMutation } from "../../slice/QueueApi"; import { QueueDetail, usePassQueueActionMutation } from "../../slice/QueueApi";
import { MessageContext } from "../../App"; import { MessageContext } from "../../App";
const UUIDToColor = (uuid: string): string => { const UUIDToColor = (uuid: string): string => {
@ -24,7 +24,7 @@ const getProfileText = (name: string): string => {
const AnonUserCard = (props: { const AnonUserCard = (props: {
queueUser: QueueUser; queueUser: QueueUser;
queue: Queue | undefined; queue: QueueDetail | undefined;
}): JSX.Element => { }): JSX.Element => {
const messageApi = useContext(MessageContext); const messageApi = useContext(MessageContext);
@ -72,7 +72,8 @@ const AnonUserCard = (props: {
)} )}
{props.queueUser && {props.queueUser &&
clientId === props.queueUser.user.id && clientId === props.queueUser.user.id &&
props.queueUser.position === 0 && ( props.queueUser.position === 0 &&
props.queue?.status === "active" && (
<span <span
style={{ style={{
marginLeft: "1rem", marginLeft: "1rem",
@ -86,10 +87,13 @@ const AnonUserCard = (props: {
</span> </span>
)} )}
{props.queueUser && {props.queueUser &&
clientId === props.queueUser.user.id && clientId === props.queueUser.user.id &&
props.queueUser.position === 0 && ( props.queueUser.position === 0 &&
<Button onClick={() => passQueue()}>{tr("Pass")}</Button> props.queue?.status === "active" ? (
)} <Button onClick={() => passQueue()}>{tr("Pass")}</Button>
) : (
<Button onClick={() => passQueue()}>{tr("Leave")}</Button>
)}
<p></p> <p></p>
</div> </div>
); );

View File

@ -1 +1,2 @@
export const baseUrl = `${window.location.protocol}//${window.location.host}/api`; export const baseUrl = `${window.location.protocol}//${window.location.host}/api`;
export const baseClientUrl = `${window.location.protocol}//${window.location.host}`;

View File

@ -114,8 +114,9 @@ export const store = configureStore({
if (theme) { if (theme) {
state.theme = theme; state.theme = theme;
} else { } else {
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); // const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); // TODO
state.theme = darkThemeMq.matches ? "dark" : "light"; // state.theme = darkThemeMq.matches ? "dark" : "light";
state.theme = "dark";
} }
}); });
}), }),

View File

@ -15,6 +15,7 @@ export const darkTheme: ThemeConfig = {
components: { components: {
Input: { Input: {
activeBorderColor: "#001529", activeBorderColor: "#001529",
colorTextPlaceholder: "grey",
}, },
}, },
}; };

View File

@ -164,7 +164,46 @@
"It is your turn!": { "It is your turn!": {
"ru": "Сейчас ваша очередь!" "ru": "Сейчас ваша очередь!"
}, },
"Start queue": {
"ru": "Начать очередь"
},
"Copy join link": {
"ru": "Скопировать ссылку для вступления"
},
"Show QR-code": {
"ru": "Показать QR-код"
},
"Hide QR-code": {
"ru": "Спрятать QR-код"
},
"Copied!": {
"ru": "Скопировано!"
},
"Pass": { "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": "Вы уже присоединились к этой очереди"
} }
} }

View File

@ -37,6 +37,10 @@ export type AnonUser = {
name: string; name: string;
}; };
export type AnonUserPatch = {
name: string;
};
export const AuthApi = createApi({ export const AuthApi = createApi({
reducerPath: "AuthApi", reducerPath: "AuthApi",
baseQuery: fetchBaseQuery({ baseQuery: fetchBaseQuery({
@ -76,6 +80,13 @@ export const AuthApi = createApi({
body: data, body: data,
}), }),
}), }),
patchAnon: builder.mutation({
query: (data: AnonUserPatch) => ({
url: "/anon",
method: "PATCH",
body: data,
}),
}),
}), }),
}); });
@ -84,4 +95,5 @@ export const {
useGetClientQuery, useGetClientQuery,
useLoginMutation, useLoginMutation,
useRegisterMutation, useRegisterMutation,
usePatchAnonMutation,
} = AuthApi; } = AuthApi;

View File

@ -19,6 +19,7 @@ export type QueueDetail = {
name: string; name: string;
description: string | null; description: string | null;
status: string; status: string;
owner_id: string;
participants: { participants: {
total: number; total: number;
remaining: number; remaining: number;