diff --git a/backend/app/db/models.py b/backend/app/db/models.py index 6b5b9ac..9fffc5c 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -14,3 +14,21 @@ class User(Base): username = Column(String(length=24), unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) + + owns_queues = relationship("Queue", backref="owner", lazy="dynamic") + + +class AnonymousUser(Base): + __tablename__ = "anonymoususers" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, index=True) + + +class Queue(Base): + __tablename__ = "queues" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) diff --git a/backend/app/main.py b/backend/app/main.py index abd815d..11acbbe 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -6,10 +6,12 @@ from .db.database import SessionLocal, engine from .dependencies import get_db from .views.auth.api import router as auth_router +from .views.queue.api import router as queue_router app = FastAPI(dependencies=[Depends(get_db)]) models.Base.metadata.create_all(bind=engine) +app.include_router(queue_router) app.include_router(auth_router) diff --git a/backend/app/views/auth/api.py b/backend/app/views/auth/api.py index f0f0a26..4271e2c 100644 --- a/backend/app/views/auth/api.py +++ b/backend/app/views/auth/api.py @@ -68,10 +68,3 @@ async def read_users_me( current_user: Annotated[schemas.User, Depends(services.get_current_active_user)], ): return current_user - - -# @app.get("/users/me/items/") -# async def read_own_items( -# current_user: Annotated[User, Depends(get_current_active_user)], -# ): -# return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/backend/app/views/auth/schemas.py b/backend/app/views/auth/schemas.py index 1497421..1774fec 100644 --- a/backend/app/views/auth/schemas.py +++ b/backend/app/views/auth/schemas.py @@ -1,5 +1,6 @@ from typing import Union from pydantic import BaseModel +from uuid import UUID class User(BaseModel): @@ -8,7 +9,7 @@ class User(BaseModel): class UserInDB(User): - hashed_password: str + id: UUID class Config: from_attributes = True diff --git a/backend/app/views/auth/services.py b/backend/app/views/auth/services.py index 88e9f74..35c31c1 100644 --- a/backend/app/views/auth/services.py +++ b/backend/app/views/auth/services.py @@ -20,15 +20,15 @@ def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) -def get_password_hash(password): +def get_password_hash(password) -> str: return pwd_context.hash(password) -def get_user_by_id(db: Session, user_id: uuid.uuid4): +def get_user_by_id(db: Session, user_id: uuid.uuid4) -> models.User: return db.query(models.User).filter(models.User.id == user_id).first() -def get_user_by_username(db: Session, username: int): +def get_user_by_username(db: Session, username: int) -> models.User: return db.query(models.User).filter(models.User.username == username).first() @@ -62,15 +62,13 @@ def create_user(db: Session, user_data: schemas.UserRegister) -> schemas.UserInD ) db.add(user) db.commit() - return schemas.UserInDB( - username=user.username, name=user.name, hashed_password=user.hashed_password - ) + return schemas.UserInDB.model_validate(user) async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], db: Annotated[Session, Depends(get_db)], -) -> schemas.User: +) -> schemas.UserInDB: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", diff --git a/backend/app/views/queue/__init__.py b/backend/app/views/queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/views/queue/api.py b/backend/app/views/queue/api.py new file mode 100644 index 0000000..ee25683 --- /dev/null +++ b/backend/app/views/queue/api.py @@ -0,0 +1,41 @@ +from datetime import datetime, timedelta, timezone +from typing import Annotated, Union +from sqlalchemy.orm import Session + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm + + +from pydantic import BaseModel + +from ...config import jwt_config +from ...dependencies import get_db +from . import schemas +from . import services + +from ..auth import services as auth_services +from ..auth import schemas as auth_schemas + + +router = APIRouter( + prefix="/queue", + tags=["queue"], + dependencies=[Depends(get_db)], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/") +async def user_queues_list( + queues: Annotated[schemas.Queue, Depends(services.get_user_queues)], +) -> list[schemas.QueueInDb]: + return queues + + +@router.post("/") +async def create_queue( + new_queue: schemas.Queue, + current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)], + db: Annotated[Session, Depends(get_db)], +) -> schemas.QueueInDb: + return services.create_queue(new_queue=new_queue, current_user=current_user, db=db) diff --git a/backend/app/views/queue/schemas.py b/backend/app/views/queue/schemas.py new file mode 100644 index 0000000..3abca79 --- /dev/null +++ b/backend/app/views/queue/schemas.py @@ -0,0 +1,27 @@ +from typing import Union +from pydantic import BaseModel +from uuid import UUID + + +class ParticipantInfo(BaseModel): + total: int + remaining: int + + +class Queue(BaseModel): + name: str + description: Union[str, None] = None + + +class QueueInList(Queue): + participants: ParticipantInfo + + class Config: + from_attributes = True + + +class QueueInDb(Queue): + id: UUID + + class Config: + from_attributes = True diff --git a/backend/app/views/queue/services.py b/backend/app/views/queue/services.py new file mode 100644 index 0000000..f13fdba --- /dev/null +++ b/backend/app/views/queue/services.py @@ -0,0 +1,30 @@ +from fastapi import Depends +from typing import Annotated +from sqlalchemy.orm import Session + +from ...dependencies import get_db +from ...db import models + +from ..auth import services as auth_services +from ..auth import schemas as auth_schemas + +from . import schemas + + +def get_user_queues( + current_user: Annotated[auth_schemas.User, Depends(auth_services.get_current_user)] +) -> list[schemas.QueueInDb]: + return [schemas.QueueInDb.model_validate(q) for q in current_user.owns_queues] + + +def create_queue( + new_queue: schemas.Queue, + current_user: auth_schemas.UserInDB, + db: Session, +) -> schemas.QueueInDb: + q = models.Queue( + name=new_queue.name, description=new_queue.description, owner_id=current_user.id + ) + db.add(q) + db.commit() + return schemas.QueueInDb.model_validate(q) diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json index 34da32a..a67ef3a 100644 --- a/frontend/app/package-lock.json +++ b/frontend/app/package-lock.json @@ -11,6 +11,7 @@ "@ant-design/icons": "^5.3.5", "@reduxjs/toolkit": "^2.2.2", "antd": "^5.15.4", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.1.0", @@ -3051,7 +3052,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3336,7 +3336,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3346,8 +3345,7 @@ "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/punycode": { "version": "2.3.1", diff --git a/frontend/app/package.json b/frontend/app/package.json index a9e8f08..c389a51 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -11,6 +11,7 @@ "@ant-design/icons": "^5.3.5", "@reduxjs/toolkit": "^2.2.2", "antd": "^5.15.4", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.1.0", diff --git a/frontend/app/rsbuild.config.ts b/frontend/app/rsbuild.config.ts index 98b2860..e202dfc 100644 --- a/frontend/app/rsbuild.config.ts +++ b/frontend/app/rsbuild.config.ts @@ -1,10 +1,15 @@ -import { defineConfig } from '@rsbuild/core'; -import { pluginReact } from '@rsbuild/plugin-react'; +import { defineConfig } from "@rsbuild/core"; +import { pluginReact } from "@rsbuild/plugin-react"; export default defineConfig({ plugins: [pluginReact()], server: { - host: '0.0.0.0', + host: "0.0.0.0", port: 3000, }, + html: { + favicon: "./static/logo-square.png", + title: "queueful!", + template: "./static/index.html", + }, }); diff --git a/frontend/app/src/App.tsx b/frontend/app/src/App.tsx index 51eb2c0..ddfa0a5 100644 --- a/frontend/app/src/App.tsx +++ b/frontend/app/src/App.tsx @@ -21,8 +21,9 @@ const App = () => {
{contextHolder} - - + + +
diff --git a/frontend/app/src/components/HeaderComponent.tsx b/frontend/app/src/components/HeaderComponent.tsx index 841ee91..d3c72e2 100644 --- a/frontend/app/src/components/HeaderComponent.tsx +++ b/frontend/app/src/components/HeaderComponent.tsx @@ -1,11 +1,17 @@ -import { LogoutOutlined, UserOutlined } from "@ant-design/icons"; -import { Button, Layout, Menu, MenuProps, Popover, Tooltip } from "antd"; -import React, { useEffect, useState } from "react"; +import { + DesktopOutlined, + LogoutOutlined, + SettingOutlined, + UserOutlined, +} from "@ant-design/icons"; +import { Layout, Menu, MenuProps, Popover } from "antd"; +import React, { useState } from "react"; import AuthModal from "./AuthModal"; import "./styles.css"; import { StorePrototype, logOut, store } from "../config/store"; import { useSelector } from "react-redux"; import tr from "../config/translation"; +import { Link } from "react-router-dom"; const { Header } = Layout; @@ -20,15 +26,27 @@ const HeaderComponent = () => { ); const userMenuItems: MenuProps["items"] = [ + { + label: {tr("Settings")}, + key: "settings", + icon: , + }, { label: tr("Log out"), key: "logout", icon: , + danger: true, onClick: () => store.dispatch(logOut()), }, ]; const items: MenuProps["items"] = [ + { + label: {tr("Dashboard")}, + key: "dashboard", + icon: , + disabled: !user, + }, { label: user ? ( { +const AppRoutes = ({ children }: { children: ReactNode }) => { store.dispatch(getLocalToken()); store.dispatch(loadLanguage()); return ( + {children} - + } /> + } /> ); }; - +AppRoutes.propTypes = { + children: PropTypes.node, +}; export default AppRoutes; diff --git a/frontend/app/src/pages/DashboardPage.tsx b/frontend/app/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..42bfe9a --- /dev/null +++ b/frontend/app/src/pages/DashboardPage.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import "./styles.css"; +import logoSquare from "../../static/logo-square.png"; +import { Button } from "antd"; +import tr from "../config/translation"; + +const DashboardPage = () => { + return ( +
+ logo +

+ {tr("Queuing has never been so simple")} +

+
+ + +
+
+ ); +}; + +export default DashboardPage; diff --git a/frontend/app/src/pages/MainPage.tsx b/frontend/app/src/pages/MainPage.tsx index 865dd19..98096de 100644 --- a/frontend/app/src/pages/MainPage.tsx +++ b/frontend/app/src/pages/MainPage.tsx @@ -1,13 +1,13 @@ import React from "react"; import "./styles.css"; -import logoSquare from "../../static/logo-square.png"; +import logo from "../../static/logo-full.png"; import { Button } from "antd"; import tr from "../config/translation"; const MainPage = () => { return (
- logo + logo

{tr("Queuing has never been so simple")}

diff --git a/frontend/app/src/pages/styles.css b/frontend/app/src/pages/styles.css index 2eee661..7090353 100644 --- a/frontend/app/src/pages/styles.css +++ b/frontend/app/src/pages/styles.css @@ -14,7 +14,7 @@ } .image { - height: 10vw; + height: 100px; width: auto; } diff --git a/frontend/app/static/index.html b/frontend/app/static/index.html new file mode 100644 index 0000000..5139ad8 --- /dev/null +++ b/frontend/app/static/index.html @@ -0,0 +1,14 @@ + + + + + + + + +
+ + diff --git a/frontend/app/static/logo-full.png b/frontend/app/static/logo-full.png new file mode 100644 index 0000000..8e363f6 Binary files /dev/null and b/frontend/app/static/logo-full.png differ