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
@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": {}}}},

View File

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

View File

@ -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:

View File

@ -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"
},

View File

@ -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>

View File

@ -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;

View File

@ -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>
);

View File

@ -1 +1,2 @@
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) {
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";
}
});
}),

View File

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

View File

@ -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": "Вы уже присоединились к этой очереди"
}
}

View File

@ -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;

View File

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