From 96935de335680c499cd066f65b58ed9a85741c77 Mon Sep 17 00:00:00 2001 From: Nikoloz Naskidashvili Date: Tue, 3 Oct 2023 22:46:13 +0400 Subject: [PATCH] feat: actual booking --- README.md | 15 +++ frontend/package-lock.json | 16 +++ frontend/package.json | 1 + frontend/{src/assets => public}/images/1.jpg | Bin frontend/{src/assets => public}/images/10.jpg | Bin frontend/{src/assets => public}/images/2.jpg | Bin frontend/{src/assets => public}/images/3.jpg | Bin frontend/{src/assets => public}/images/4.jpg | Bin frontend/{src/assets => public}/images/5.jpg | Bin frontend/{src/assets => public}/images/6.jpg | Bin frontend/{src/assets => public}/images/7.jpg | Bin frontend/{src/assets => public}/images/8.jpg | Bin frontend/{src/assets => public}/images/9.jpg | Bin frontend/src/components/VenueCard.jsx | 35 +------ frontend/src/main.jsx | 16 +-- frontend/src/pages/BookingConfirm.jsx | 49 +++++++++ frontend/src/pages/BookingIndex.jsx | 93 ++++++++++++------ .../versions/5371171eb23e_edit_booking.py | 38 +++++++ server/src/api/endpoints/booking.py | 5 +- server/src/models/booking.py | 11 +-- server/src/schemas/booking.py | 7 +- server/src/utils/dummy_data.py | 20 ++-- 22 files changed, 214 insertions(+), 92 deletions(-) rename frontend/{src/assets => public}/images/1.jpg (100%) rename frontend/{src/assets => public}/images/10.jpg (100%) rename frontend/{src/assets => public}/images/2.jpg (100%) rename frontend/{src/assets => public}/images/3.jpg (100%) rename frontend/{src/assets => public}/images/4.jpg (100%) rename frontend/{src/assets => public}/images/5.jpg (100%) rename frontend/{src/assets => public}/images/6.jpg (100%) rename frontend/{src/assets => public}/images/7.jpg (100%) rename frontend/{src/assets => public}/images/8.jpg (100%) rename frontend/{src/assets => public}/images/9.jpg (100%) create mode 100644 frontend/src/pages/BookingConfirm.jsx create mode 100644 server/migrations/versions/5371171eb23e_edit_booking.py diff --git a/README.md b/README.md index e204a67..0d6d2f9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,21 @@ ### Description +### Tech Stack & Libraries + +##### Client + - React - [Documentation](https://react.dev) + - Vite - [Documentation](https://vitejs.dev/guide/) + - TailwindCSS - [Documentation](https://tailwindcss.com/docs) + +##### Server + - Python + - FastAPI - Async web framework [Documentation](https://fastapi.tiangolo.com/) + - Aiogram - Async Telegram Bot API framework [Documentation](https://docs.aiogram.dev/en/latest/) + - Pydantic - Validation Library [Documentation](https://docs.pydantic.dev/latest/) + - SQLAlchemy - ORM [Documentation](https://docs.sqlalchemy.org/en/20/) + - SQLite - Database + ### Project Structure ``` ... diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f56e9b9..f8ddeb3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.47.0", "react-router-dom": "^6.16.0", "sort-by": "^0.0.2" }, @@ -5910,6 +5911,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz", + "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 292943f..e371cff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.47.0", "react-router-dom": "^6.16.0", "sort-by": "^0.0.2" }, diff --git a/frontend/src/assets/images/1.jpg b/frontend/public/images/1.jpg similarity index 100% rename from frontend/src/assets/images/1.jpg rename to frontend/public/images/1.jpg diff --git a/frontend/src/assets/images/10.jpg b/frontend/public/images/10.jpg similarity index 100% rename from frontend/src/assets/images/10.jpg rename to frontend/public/images/10.jpg diff --git a/frontend/src/assets/images/2.jpg b/frontend/public/images/2.jpg similarity index 100% rename from frontend/src/assets/images/2.jpg rename to frontend/public/images/2.jpg diff --git a/frontend/src/assets/images/3.jpg b/frontend/public/images/3.jpg similarity index 100% rename from frontend/src/assets/images/3.jpg rename to frontend/public/images/3.jpg diff --git a/frontend/src/assets/images/4.jpg b/frontend/public/images/4.jpg similarity index 100% rename from frontend/src/assets/images/4.jpg rename to frontend/public/images/4.jpg diff --git a/frontend/src/assets/images/5.jpg b/frontend/public/images/5.jpg similarity index 100% rename from frontend/src/assets/images/5.jpg rename to frontend/public/images/5.jpg diff --git a/frontend/src/assets/images/6.jpg b/frontend/public/images/6.jpg similarity index 100% rename from frontend/src/assets/images/6.jpg rename to frontend/public/images/6.jpg diff --git a/frontend/src/assets/images/7.jpg b/frontend/public/images/7.jpg similarity index 100% rename from frontend/src/assets/images/7.jpg rename to frontend/public/images/7.jpg diff --git a/frontend/src/assets/images/8.jpg b/frontend/public/images/8.jpg similarity index 100% rename from frontend/src/assets/images/8.jpg rename to frontend/public/images/8.jpg diff --git a/frontend/src/assets/images/9.jpg b/frontend/public/images/9.jpg similarity index 100% rename from frontend/src/assets/images/9.jpg rename to frontend/public/images/9.jpg diff --git a/frontend/src/components/VenueCard.jsx b/frontend/src/components/VenueCard.jsx index 6810926..59ca26f 100644 --- a/frontend/src/components/VenueCard.jsx +++ b/frontend/src/components/VenueCard.jsx @@ -3,48 +3,19 @@ import { useNavigate } from "react-router-dom"; import { Card, CardHeader, CardFooter, Image, Button } from "@nextui-org/react"; -import Image1 from "@/assets/images/1.jpg"; -import Image2 from "@/assets/images/2.jpg"; -import Image3 from "@/assets/images/3.jpg"; -import Image4 from "@/assets/images/4.jpg"; -import Image5 from "@/assets/images/5.jpg"; -import Image6 from "@/assets/images/6.jpg"; -import Image7 from "@/assets/images/7.jpg"; -import Image8 from "@/assets/images/8.jpg"; -import Image9 from "@/assets/images/9.jpg"; -import Image10 from "@/assets/images/10.jpg"; - const VenueCard = ({ venue }) => { const navigate = useNavigate(); - const [selectedImageUrl, setSelectedImageUrl] = useState(null); - const imageUrls = [ - Image1, - Image2, - Image3, - Image4, - Image5, - Image6, - Image7, - Image8, - Image9, - Image10, - ]; - - useEffect(() => { - const randomIndex = Math.floor(Math.random() * imageUrls.length); - setSelectedImageUrl(imageUrls[randomIndex]); - }, []); - return ( {venue.name} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 6ac55eb..897ac88 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,15 +1,15 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' +import React from "react"; +import ReactDOM from "react-dom/client"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { NextUIProvider } from "@nextui-org/react"; -import './index.css' +import "./index.css"; // Route components -import BaseLayout from '@/pages/BaseLayout.jsx'; -import Index from '@/pages/Index.jsx'; -import BookingIndex from '@/pages/BookingIndex.jsx'; +import BaseLayout from "@/pages/BaseLayout.jsx"; +import Index from "@/pages/Index.jsx"; +import BookingIndex from "@/pages/BookingIndex.jsx"; // Initialize react router const router = createBrowserRouter([ @@ -18,8 +18,8 @@ const router = createBrowserRouter([ element: , children: [ { path: "/", element: }, - { path: "/book/:venueId", element: } - ] + { path: "/book/:venueId", element: }, + ], }, ]); diff --git a/frontend/src/pages/BookingConfirm.jsx b/frontend/src/pages/BookingConfirm.jsx new file mode 100644 index 0000000..e66f11c --- /dev/null +++ b/frontend/src/pages/BookingConfirm.jsx @@ -0,0 +1,49 @@ +import { useState, useEffect, useCallback } from "react"; +import { useParams, useLocation, useNavigate } from "react-router-dom"; +import { useForm } from "react-hook-form"; + +import { Image, Input } from "@nextui-org/react"; + +import axiosInstance from "@/services/api"; +import { useTelegram } from "@/hooks/useTelegram"; + +const BookingConfirm = () => { + const { tg } = useTelegram(); + const navigate = useNavigate(); + const location = useLocation(); + + const { venueId } = useParams(); + const venueAddress = location.state.venueAddress; + const venueName = location.state.venueName; + + const { register, handleSubmit } = useForm(); + const onSubmit = useCallback((data) => { + console.log(data); + + // axiosInstance.post(`/bookings/${venueId}`, { + // signal: abortController.signal, + // _auth: tg.initData, + // queryId: queryId, + // }); + }, []); + + // handle back button click + tg.onEvent("backButtonClicked", () => { + navigate(`/book/${venueId}`); + }); + + useEffect(() => { + tg.MainButton.text = "Confirm Booking"; + }, [tg]); + + + + return ( +
+ + +
+ ); +}; + +export default BookingConfirm; diff --git a/frontend/src/pages/BookingIndex.jsx b/frontend/src/pages/BookingIndex.jsx index 5acb0f9..a6c3c54 100644 --- a/frontend/src/pages/BookingIndex.jsx +++ b/frontend/src/pages/BookingIndex.jsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from "react"; -import { useParams, useLocation, useNavigate } from "react-router-dom"; +import { useState, useEffect, useCallback } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { useForm } from "react-hook-form"; -import { Image, Spinner } from "@nextui-org/react"; +import { Image, Spinner, Input } from "@nextui-org/react"; import axiosInstance from "@/services/api"; import { useTelegram } from "@/hooks/useTelegram"; @@ -14,14 +15,42 @@ const STATUS = { }; const BookingIndex = () => { - const { tg, queryId } = useTelegram(); + const { tg } = useTelegram(); const navigate = useNavigate(); - const location = useLocation(); const { venueId } = useParams(); const [venue, setVenue] = useState(null); const [status, setStatus] = useState(STATUS.IDLE); + const { register, handleSubmit } = useForm(); + const onSubmit = useCallback( + (data) => { + const abortController = new AbortController(); + + // Send required fields + axiosInstance.post(`/bookings/${venueId}`, { + signal: abortController.signal, + _auth: tg.initData, + queryId: tg.initData.queryId, + under_name: data.under_name, + date: data.date, + comment: data.comment, + }); + }, + [venueId, tg] + ); + + useEffect(() => { + const abortController = new AbortController(); + tg.onEvent("mainButtonClicked", () => { + handleSubmit(onSubmit)(); + }); + + return () => { + abortController.abort(); + }; + }, [tg, handleSubmit, onSubmit]); + useEffect(() => { setStatus(STATUS.LOADING); @@ -44,23 +73,6 @@ const BookingIndex = () => { tg.BackButton.show(); }, [venueId]); - // handle main button click - useEffect(() => { - const abortController = new AbortController(); - - tg.onEvent("mainButtonClicked", () => { - axiosInstance.post(`/bookings/${venueId}`, { - signal: abortController.signal, - _auth: tg.initData, - queryId: queryId, - }); - }); - - return () => { - abortController.abort(); - }; - }, [tg, venueId]); - // handle back button click tg.onEvent("backButtonClicked", () => { navigate("/"); @@ -70,14 +82,33 @@ const BookingIndex = () => {
{status === STATUS.SUCCESS ? ( <> - {location.state && ( - - )} -

{venue.name}

-

- {venue.address}, {venue.city} -

- {venue.description} +
+ +
+ {venue.name} + + {venue.address}, {venue.city} + + {venue.description} +
+
+
+ + + +
) : status === STATUS.LOADING ? (
@@ -85,7 +116,7 @@ const BookingIndex = () => {
) : (
- Error. + Error
)}
diff --git a/server/migrations/versions/5371171eb23e_edit_booking.py b/server/migrations/versions/5371171eb23e_edit_booking.py new file mode 100644 index 0000000..52c8e40 --- /dev/null +++ b/server/migrations/versions/5371171eb23e_edit_booking.py @@ -0,0 +1,38 @@ +"""edit booking + +Revision ID: 5371171eb23e +Revises: 75668988960f +Create Date: 2023-10-03 11:07:05.857324 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5371171eb23e' +down_revision: Union[str, None] = '75668988960f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('bookings', sa.Column('under_name', sa.String(), nullable=True)) + op.add_column('bookings', sa.Column('date', sa.Date(), nullable=True)) + op.add_column('bookings', sa.Column('comment', sa.String(), nullable=True)) + op.drop_column('bookings', 'first_name') + op.drop_column('bookings', 'last_name') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('bookings', sa.Column('last_name', sa.VARCHAR(), nullable=True)) + op.add_column('bookings', sa.Column('first_name', sa.VARCHAR(), nullable=True)) + op.drop_column('bookings', 'comment') + op.drop_column('bookings', 'date') + op.drop_column('bookings', 'under_name') + # ### end Alembic commands ### diff --git a/server/src/api/endpoints/booking.py b/server/src/api/endpoints/booking.py index f8fb405..0f7cd1d 100644 --- a/server/src/api/endpoints/booking.py +++ b/server/src/api/endpoints/booking.py @@ -39,8 +39,9 @@ async def book_venue(venue_id: int, request: Request): booking = BookingCreate( venue_id=venue_id, user_id=user.id, - first_name=user.first_name, - last_name=user.last_name + under_name=json_data.get("under_name"), + date=json_data.get("date"), + comment=json_data.get("comment") ) db_obj = Booking(**booking.model_dump()) diff --git a/server/src/models/booking.py b/server/src/models/booking.py index 3791997..9931976 100644 --- a/server/src/models/booking.py +++ b/server/src/models/booking.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, ForeignKey +from sqlalchemy import Column, String, Date, ForeignKey from sqlalchemy.orm import relationship from src.models.base import PkBase @@ -11,9 +11,6 @@ class Booking(PkBase): venue_id = Column(String, ForeignKey('venues.id')) venue = relationship('Venue', lazy="selectin") user_id = Column(String) - first_name = Column(String, nullable=True) - last_name = Column(String, nullable=True) - - - - + under_name = Column(String) + date = Column(Date) + comment = Column(String) diff --git a/server/src/schemas/booking.py b/server/src/schemas/booking.py index 5359645..1acdf50 100644 --- a/server/src/schemas/booking.py +++ b/server/src/schemas/booking.py @@ -1,3 +1,5 @@ +from datetime import date + from pydantic import BaseModel, ConfigDict from src.schemas.venue import VenueItem @@ -17,5 +19,6 @@ class BookingItem(BaseModel): class BookingCreate(BaseModel): venue_id: int user_id: int - first_name: str - last_name: str + under_name: str + date: date + comment: str diff --git a/server/src/utils/dummy_data.py b/server/src/utils/dummy_data.py index 50c7198..cee1f88 100644 --- a/server/src/utils/dummy_data.py +++ b/server/src/utils/dummy_data.py @@ -7,61 +7,61 @@ async def create_dummy_data(): venue1 = Venue( name="Rusty Bar", - description="In rust we trust! Only BROgrammers allowed! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="In rust we trust! Only BROgrammers allowed!", address="Rust Street 1", city="San Francisco" ) venue2 = Venue( name="Bash Bar", - description="Cool venue for cool people, YOLO! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="Cool venue for cool people, YOLO!", address="Bash Street 1", city="San Francisco" ) venue3 = Venue( name="Python Bar", - description="Cool venue for cool people, Python is the best! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ", + description="Cool venue for cool people, Python is the best! ", address="Python Street 1", city="San Francisco" ) venue4 = Venue( name="Java Bar", - description="A cup of Java for everyone! We have coffee too :D Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="A cup of Java for everyone! We have coffee too :D", address="Java Street 1", city="San Francisco" ) venue5 = Venue( name="C++ Bar", - description="Oops, buffer overflow! Just kidding, we are safe :D Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="Oops, buffer overflow! Just kidding, we are safe :D", address="Compiler Street 1", city="San Francisco" ) venue6 = Venue( name="JavaScript Bar", - description="Building another framework. Come back tomorrow for a new one! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="Building another framework. Come back tomorrow for a new one!", address="Node Street 1", city="San Francisco" ) venue7 = Venue( name="PHP Bar", - description="We are dead but still alive! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="We are dead but still alive!", address="PHP Street 1", city="San Francisco" ) venue8 = Venue( name="Ruby Bar", - description="Found random gems in the street? Come here! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="Found random gems in the street? Come here!", address="Ruby Street 1", city="San Francisco" ) venue9 = Venue( name="Go Bar", - description="We are fast and concurrent! Gofers are welcome! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="We are fast and concurrent! Gofers are welcome!", address="Go Street 1", city="San Francisco" ) venue10 = Venue( name="Assembly Bar", - description="We are low level, but we are not low quality! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + description="We are low level, but we are not low quality!", address="Assembly Street 1", city="San Francisco" )