This commit is contained in:
2024-06-09 20:41:57 +03:00
parent 175d2f9cd4
commit 3fb38cac3a
17 changed files with 549 additions and 39 deletions

View File

@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, types
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
import uuid import uuid
@ -7,6 +7,21 @@ import datetime
from .database import Base from .database import Base
class ChoiceType(types.TypeDecorator):
impl = types.String
def __init__(self, choices, **kw):
self.choices = dict(choices)
super(ChoiceType, self).__init__(**kw)
def process_bind_param(self, value, dialect):
return [k for k, v in self.choices.items() if v == value][0]
def process_result_value(self, value, dialect):
return self.choices[value]
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
@ -47,8 +62,20 @@ class Queue(Base):
description = Column(String, index=True) description = Column(String, index=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id"))
start_time = Column(DateTime, nullable=True) start_time = Column(DateTime, nullable=True)
status = Column(
ChoiceType(
{
"created": "created",
"waiting": "waiting",
"active": "active",
"finished": "finished",
}
),
nullable=False,
)
users = relationship("QueueUser", backref="queue", lazy="dynamic") users = relationship("QueueUser", backref="queue", lazy="dynamic")
logs = relationship("QueueLog", backref="queue", lazy="dynamic")
class QueueUser(Base): class QueueUser(Base):
@ -66,6 +93,7 @@ class QueueLog(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
action = Column(String) action = Column(String)
queue_id = Column(UUID(as_uuid=True), ForeignKey("queues.id"))
created = Column(DateTime, default=datetime.datetime.utcnow) created = Column(DateTime, default=datetime.datetime.utcnow)

View File

@ -72,8 +72,10 @@ async def register(
@router.get("/me") @router.get("/me")
async def read_users_me( async def read_users_me(
current_user: Annotated[schemas.User, Depends(services.get_current_active_user)], current_user: Annotated[
) -> schemas.User: schemas.UserInDB, Depends(services.get_current_active_user)
],
) -> schemas.UserInDB:
return current_user return current_user

View File

@ -1,6 +1,7 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Annotated, Union from typing import Annotated, Union
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
import redis
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
@ -9,7 +10,8 @@ from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel from pydantic import BaseModel
from ...config import jwt_config from ...config import jwt_config
from ...dependencies import get_db from ...dependencies import get_db, get_redis
from ...db import models
from . import schemas from . import schemas
from . import services from . import services
@ -25,8 +27,15 @@ router = APIRouter(
) )
@router.get("/owned")
async def owned_queues_list(
queues: Annotated[schemas.Queue, Depends(services.get_owned_queues)],
) -> list[schemas.QueueInDb]:
return queues
@router.get("/") @router.get("/")
async def user_queues_list( async def anonuser_queues_list(
queues: Annotated[schemas.Queue, Depends(services.get_user_queues)], queues: Annotated[schemas.Queue, Depends(services.get_user_queues)],
) -> list[schemas.QueueInDb]: ) -> list[schemas.QueueInDb]:
return queues return queues
@ -60,3 +69,10 @@ async def listen_queue(
updated_queue: Annotated[schemas.QueueDetail, Depends(services.set_queue_listener)] updated_queue: Annotated[schemas.QueueDetail, Depends(services.set_queue_listener)]
) -> schemas.QueueDetail: ) -> schemas.QueueDetail:
return updated_queue return updated_queue
@router.post("/{queue_id}/action/{action}")
async def perform_queue_action(
result: Annotated[schemas.ActionResult, Depends(services.action_wrapper)]
):
return result

View File

@ -41,4 +41,14 @@ class QueueInDb(Queue):
class QueueDetail(Queue): class QueueDetail(Queue):
id: UUID id: UUID
status: str
owner_id: UUID
participants: ParticipantInfo participants: ParticipantInfo
class ActionResult(BaseModel):
action: str
result: str
class Config:
from_attributes = True

View File

@ -14,24 +14,38 @@ from ..auth import schemas as auth_schemas
from . import schemas from . import schemas
def get_queue_by_id(queue_id: UUID, db: Session) -> models.Queue: def get_queue_by_id(
queue_id: UUID, db: Annotated[Session, Depends(get_db)]
) -> models.Queue:
q = db.query(models.Queue).filter(models.Queue.id == queue_id).first() q = db.query(models.Queue).filter(models.Queue.id == queue_id).first()
return q return q
def get_user_queues( def get_owned_queues(
current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)] current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)]
) -> list[schemas.QueueInDb]: ) -> list[schemas.QueueInDb]:
return [schemas.QueueInDb.model_validate(q) for q in current_user.owns_queues] return [schemas.QueueInDb.model_validate(q) for q in current_user.owns_queues]
def get_user_queues(
current_user: Annotated[auth_schemas.User, Depends(auth_services.get_anon_user)]
) -> list[schemas.QueueInDb]:
return [
schemas.QueueInDb.model_validate(q.queue)
for q in current_user.parts_in_queues.filter(models.QueueUser.passed == False)
]
def create_queue( def create_queue(
new_queue: schemas.Queue, new_queue: schemas.Queue,
current_user: auth_schemas.UserInDB, current_user: auth_schemas.UserInDB,
db: Session, db: Session,
) -> schemas.QueueInDb: ) -> schemas.QueueInDb:
q = models.Queue( q = models.Queue(
name=new_queue.name, description=new_queue.description, owner_id=current_user.id name=new_queue.name,
description=new_queue.description,
owner_id=current_user.id,
status="created",
) )
db.add(q) db.add(q)
db.commit() db.commit()
@ -48,6 +62,8 @@ def get_detailed_queue(
id=q.id, id=q.id,
name=q.name, name=q.name,
description=q.description, description=q.description,
status=q.status,
owner_id=q.owner_id,
participants=schemas.ParticipantInfo( participants=schemas.ParticipantInfo(
total=q.users.count(), total=q.users.count(),
remaining=q.users.filter(models.QueueUser.passed == False).count(), remaining=q.users.filter(models.QueueUser.passed == False).count(),
@ -102,3 +118,132 @@ async def set_queue_listener(
await ps.unsubscribe() await ps.unsubscribe()
new_queue = get_detailed_queue(queue_id=queue_id, db=db) new_queue = get_detailed_queue(queue_id=queue_id, db=db)
return new_queue return new_queue
async def get_queue_owner(
queue: Annotated[models.Queue, Depends(get_queue_by_id)]
) -> auth_schemas.UserInDB:
return queue.owner if queue else None
async def verify_queue_owner(
queue_owner: Annotated[auth_schemas.UserInDB, Depends(get_queue_owner)],
current_user: Annotated[
auth_schemas.UserInDB, Depends(auth_services.get_current_user)
],
) -> bool:
return queue_owner.id == current_user.id if queue_owner and current_user else False
async def rebuild_queue(
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
db: Annotated[Session, Depends(get_db)],
):
for i, qu in enumerate(
queue.users.filter(models.QueueUser.passed == False).order_by(
models.QueueUser.position.asc()
)
):
setattr(qu, "position", i)
db.commit()
async def kick_first(
is_owner: Annotated[bool, Depends(verify_queue_owner)],
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
db: Annotated[Session, Depends(get_db)],
):
if is_owner:
first_user = (
queue.users.filter(models.QueueUser.passed == False)
.order_by(models.QueueUser.position.asc())
.first()
)
if first_user:
setattr(first_user, "passed", True)
db.commit()
await rebuild_queue(queue=queue, db=db)
return
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No first user",
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You are not a queue owner!",
)
async def start_queue(
is_owner: Annotated[bool, Depends(verify_queue_owner)],
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
db: Annotated[Session, Depends(get_db)],
):
if queue and is_owner:
setattr(queue, "status", "active")
db.commit()
await rebuild_queue(queue=queue, db=db)
return
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="You are not a queue owner!",
)
async def pass_queueuser(
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
anon_user: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)],
db: Annotated[Session, Depends(get_db)],
):
if anon_user:
qu = (
db.query(models.QueueUser)
.filter(
models.QueueUser.queue_id == queue.id,
models.QueueUser.user_id == anon_user.id,
)
.first()
)
if qu:
setattr(qu, "passed", True)
db.commit()
await rebuild_queue(queue=queue, db=db)
return
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found!",
)
async def action_wrapper(
action: str,
is_owner: Annotated[bool, Depends(verify_queue_owner)],
queue: Annotated[models.Queue, Depends(get_queue_by_id)],
anon_user: Annotated[auth_schemas.AnonUser, Depends(auth_services.get_anon_user)],
db: Annotated[Session, Depends(get_db)],
r: Annotated[redis.client.Redis, Depends(get_redis)],
) -> schemas.ActionResult:
if queue:
if action == "kick-first":
await kick_first(is_owner=is_owner, queue=queue, db=db)
await r.publish(str(queue.id), "updated")
return {"action": action, "status": "success"}
if action == "pass":
await pass_queueuser(queue=queue, anon_user=anon_user, db=db)
await r.publish(str(queue.id), "updated")
return {"action": action, "status": "success"}
if action == "start":
await start_queue(queue=queue, is_owner=is_owner, db=db)
await r.publish(str(queue.id), "updated")
return {"action": action, "status": "success"}
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Action not found!",
)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Queue not found!",
)

View File

@ -1,4 +1,5 @@
import { import {
ClockCircleOutlined,
DesktopOutlined, DesktopOutlined,
GlobalOutlined, GlobalOutlined,
LogoutOutlined, LogoutOutlined,
@ -113,6 +114,11 @@ const HeaderComponent = () => {
icon: <DesktopOutlined />, icon: <DesktopOutlined />,
disabled: !user, disabled: !user,
}, },
{
label: <Link to="/parting">{tr("Current queues")}</Link>,
key: "parting",
icon: <ClockCircleOutlined />,
},
{ {
label: <Link to="/news">{tr("News")}</Link>, label: <Link to="/news">{tr("News")}</Link>,
key: "news", key: "news",
@ -171,6 +177,11 @@ const HeaderComponent = () => {
disabled: !user, disabled: !user,
onClick: () => setDrawerOpen(false), onClick: () => setDrawerOpen(false),
}, },
{
label: <Link to="/parting">{tr("Current queues")}</Link>,
key: "parting",
icon: <ClockCircleOutlined />,
},
{ {
label: <Link to="/news">{tr("News")}</Link>, label: <Link to="/news">{tr("News")}</Link>,
key: "news", key: "news",

View File

@ -31,11 +31,9 @@ const ApproveQueueJoinCard = (props: { id: string }): JSX.Element => {
const onJoinButtonClick = () => { const onJoinButtonClick = () => {
joinQueue(props.id) joinQueue(props.id)
.unwrap() .unwrap()
.then(() =>
messageApi.success(tr("Successfully joined queue ") + data.name)
)
.then(() => navigate(`/queue/${props.id}`)) .then(() => navigate(`/queue/${props.id}`))
.then(() => refetch()) .then(() => refetch())
.then(() => messageApi.success(tr("Successfully joined queue")))
.catch((e) => messageApi.error(tr(e.data.detail))); .catch((e) => messageApi.error(tr(e.data.detail)));
}; };

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

View File

@ -1,26 +1,99 @@
import React from "react"; import React, { useContext } from "react";
import { useGetQueueDetailQuery } from "../../slice/QueueApi"; import {
useGetQueueDetailQuery,
useKickFirstActionMutation,
useStartQueueActionMutation,
} from "../../slice/QueueApi";
import "../styles.css"; import "../styles.css";
import { Button, Spin } from "antd"; import { Button, Spin } from "antd";
import { import {
ArrowUpOutlined, FieldTimeOutlined,
FileTextOutlined, FileTextOutlined,
FlagOutlined,
HourglassOutlined,
LoadingOutlined, LoadingOutlined,
PlusCircleOutlined,
QuestionCircleOutlined,
UserOutlined, UserOutlined,
} 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";
import { Link, useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
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 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 QueueCard = (): JSX.Element => {
const user = useSelector((state: StorePrototype) => state.auth.user); const messageApi = useContext(MessageContext);
const { queueId } = useParams(); const { queueId } = useParams();
const { data, isFetching } = useGetQueueDetailQuery(queueId, { const { data, isFetching, refetch } = useGetQueueDetailQuery(queueId, {
skip: !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 ( return (
<div className="card"> <div className="card">
@ -42,13 +115,22 @@ const QueueCard = (): JSX.Element => {
{" "} {" "}
{data?.participants?.remaining} / {data?.participants?.total} {data?.participants?.remaining} / {data?.participants?.total}
</p> </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>
<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} user={v} />; return <AnonUserCard key={v.id} queueUser={v} queue={data} />;
})} })}
</div> </div>
</Spin> </Spin>

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useGetQueuesQuery } from "../../slice/QueueApi"; import { useGetOwnedQueuesQuery } from "../../slice/QueueApi";
import "../styles.css"; import "../styles.css";
import { Button, Spin } from "antd"; import { Button, Spin } from "antd";
import { import {
@ -21,7 +21,7 @@ type Queue = {
const QueuesList = (): JSX.Element => { const QueuesList = (): JSX.Element => {
const user = useSelector((state: StorePrototype) => state.auth.user); const user = useSelector((state: StorePrototype) => state.auth.user);
const { data, refetch, isLoading } = useGetQueuesQuery({}); const { data, refetch, isLoading } = useGetOwnedQueuesQuery({});
useEffect(() => { useEffect(() => {
user && refetch(); user && refetch();
}, [user]); }, [user]);

View File

@ -1,9 +1,12 @@
import React from "react"; import React, { useContext } from "react";
import { AnonUser } from "../../slice/AuthApi"; import { QueueUser } from "../../slice/AuthApi";
import "../styles.css"; import "../styles.css";
import tr from "../../config/translation"; import tr from "../../config/translation";
import Title from "antd/es/typography/Title"; import { useSelector } from "react-redux";
import Paragraph from "antd/es/typography/Paragraph"; import { StorePrototype } from "../../config/store";
import { Button } from "antd";
import { Queue, usePassQueueActionMutation } from "../../slice/QueueApi";
import { MessageContext } from "../../App";
const UUIDToColor = (uuid: string): string => { const UUIDToColor = (uuid: string): string => {
return ( return (
@ -20,24 +23,74 @@ const getProfileText = (name: string): string => {
}; };
const AnonUserCard = (props: { const AnonUserCard = (props: {
user: AnonUser; queueUser: QueueUser;
backlight?: "self" | "active" | undefined; queue: Queue | undefined;
}): JSX.Element => { }): 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 ( return (
<div className="anon-card"> <div className="anon-card">
<span style={{ marginRight: "1rem" }}>#{props.queueUser.position}</span>
<div <div
className="anon-circle" className="anon-circle"
style={{ background: UUIDToColor(props.user.id) }} style={{ background: UUIDToColor(props.queueUser.user.id) }}
> >
{props.user.name {props.queueUser.user.name
? getProfileText(props.user.name) ? getProfileText(props.queueUser.user.name)
: props.user.id.substring(0, 2)} : props.queueUser.id.substring(0, 2)}
</div> </div>
<p color="white" style={{ marginLeft: "10px" }}> <p color="white" style={{ marginLeft: "10px" }}>
{props.user.name {props.queueUser.user.name
? props.user.name ? props.queueUser.user.name
: tr("Anonymous") + " #" + props.user.id.substring(0, 4)} : tr("Anonymous") + " #" + props.queueUser.id.substring(0, 4)}
</p> </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> </div>
); );
}; };

View File

@ -12,7 +12,7 @@ import { NewsApi } from "../slice/NewsApi";
export type AuthDataType = { export type AuthDataType = {
token: string | null; token: string | null;
clientId: 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 = { const initialAuthDataState: AuthDataType = {

View File

@ -115,5 +115,56 @@
}, },
"Invalid captcha": { "Invalid captcha": {
"ru": "Неверная капча" "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": "Пройти"
} }
} }

View File

@ -17,6 +17,7 @@ import CreateNewsPage from "./CreateNewsPage";
import QueueCard from "../components/queue/QueueCard"; import QueueCard from "../components/queue/QueueCard";
import JoinQueuePage from "./JoinQueuePage"; import JoinQueuePage from "./JoinQueuePage";
import ApproveQueueJoinPage from "./ApproveQueueJoinPage"; import ApproveQueueJoinPage from "./ApproveQueueJoinPage";
import PartingQueuesPage from "./PartingQueuesPage";
const AppRoutes = ({ children }: { children: ReactNode }) => { const AppRoutes = ({ children }: { children: ReactNode }) => {
store.dispatch(getLocalToken()); store.dispatch(getLocalToken());
@ -30,6 +31,7 @@ const AppRoutes = ({ children }: { children: ReactNode }) => {
<Routes> <Routes>
<Route path="/" element={<MainPage />} /> <Route path="/" element={<MainPage />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
<Route path="/parting" element={<PartingQueuesPage />} />
<Route path="/queue/:queueId" element={<QueueCard />} /> <Route path="/queue/:queueId" element={<QueueCard />} />
<Route path="/queue/join" element={<JoinQueuePage />} /> <Route path="/queue/join" element={<JoinQueuePage />} />
<Route path="/queue/join/:queueId" element={<ApproveQueueJoinPage />} /> <Route path="/queue/join/:queueId" element={<ApproveQueueJoinPage />} />

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

View File

@ -3,6 +3,7 @@ import { baseUrl } from "../config/baseUrl";
import { RootState } from "../config/store"; import { RootState } from "../config/store";
export interface User { export interface User {
id: string;
username: string; username: string;
name: string; name: string;
} }
@ -24,6 +25,13 @@ export type TokenResponse = {
token_type: string; token_type: string;
}; };
export type QueueUser = {
id: string;
position: number;
passed: boolean;
user: AnonUser;
};
export type AnonUser = { export type AnonUser = {
id: string; id: string;
name: string; name: string;

View File

@ -1,7 +1,7 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { baseUrl } from "../config/baseUrl"; import { baseUrl } from "../config/baseUrl";
import { RootState } from "../config/store"; import { RootState } from "../config/store";
import { AnonUser } from "./AuthApi"; import { QueueUser } from "./AuthApi";
export type CreateQueueRequest = { export type CreateQueueRequest = {
name: string; name: string;
@ -18,10 +18,11 @@ export type QueueDetail = {
id: string; id: string;
name: string; name: string;
description: string | null; description: string | null;
status: string;
participants: { participants: {
total: number; total: number;
remaining: number; remaining: number;
users_list: [AnonUser]; users_list: [QueueUser];
}; };
}; };
@ -42,9 +43,12 @@ export const QueueApi = createApi({
}, },
}), }),
endpoints: (builder) => ({ endpoints: (builder) => ({
getQueues: builder.query<[Queue], undefined>({ getQueues: builder.query<[Queue], unknown>({
query: () => "/", query: () => "/",
}), }),
getOwnedQueues: builder.query<[Queue], unknown>({
query: () => "/owned",
}),
getQueueDetail: builder.query<QueueDetail, string | undefined>({ getQueueDetail: builder.query<QueueDetail, string | undefined>({
query: (queueId: string | undefined) => `/${queueId}`, query: (queueId: string | undefined) => `/${queueId}`,
}), }),
@ -58,12 +62,34 @@ export const QueueApi = createApi({
body: data, 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 { export const {
useGetQueuesQuery, useGetQueuesQuery,
useGetOwnedQueuesQuery,
useGetQueueDetailQuery, useGetQueueDetailQuery,
useJoinQueueMutation, useJoinQueueMutation,
useCreateQueueMutation, useCreateQueueMutation,
usePassQueueActionMutation,
useKickFirstActionMutation,
useStartQueueActionMutation,
} = QueueApi; } = QueueApi;