diff --git a/Backend/bookmyvenue/.env b/Backend/bookmyvenue/.env new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Backend/bookmyvenue/.env @@ -0,0 +1 @@ + diff --git a/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/__pycache__/tasks.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/__pycache__/tasks.cpython-311.pyc new file mode 100644 index 000000000..1e9b7a934 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/__pycache__/tasks.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/tasks.py b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/tasks.py new file mode 100644 index 000000000..348b3e504 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/Owner/tasks.py @@ -0,0 +1,102 @@ +import base64 +import random + +import structlog +from typing import List + +from celery import shared_task +from sqlalchemy import select +from src.bookmyvenue.models.common import Venue +from src.bookmyvenue.core.database import session +from src.bookmyvenue.utils.image_kit import imagekit +from src.bookmyvenue.worker import app +logger = structlog.get_logger() + + +@app.task +def upload_media_to_imagekit(venue_id:int,cover_image,gallery:List,venue_name:str): + db = session() + + #we need to get the venue record first + #update its status to uploading + #start the image uploads to imagekit + #on success update the status to uploaded + + try: + current_venue_fetching_statement = select(Venue).where(Venue.id == venue_id) + venue = db.execute(current_venue_fetching_statement).scalar_one_or_none() #scalar is uses=d to return as python object + + if not venue: + logger.error("venue not found for the given id" , venueid = venue_id) + return { + "status_code":400, + "message": "venue not found" + } + + venue.task_status = 'Uploading' + + cover_image_url = '' + gallery_urls = [] + #First we upload the images to the imagekit storage and then return the urls to the route control + + logger.info("trying to upload the cover image through imagekit" , venueid = venue_id, task_id = venue.celery_task_ID) + + + try: + base64_str = cover_image + + if "base64," in base64_str: + base64_str = base64_str.split("base64,")[1] + + # Convert the string to raw image bytes + cover_image_bytes = base64.b64decode(base64_str.encode("utf-8")) + uploaded_media = imagekit.files.upload( + file=cover_image_bytes, + file_name=f'{venue_name}-cover-{random.randint(0,10000)}', + folder='/covers' + ) + + if uploaded_media.url: + logger.info("successfully uploaded the cover image" , url = uploaded_media.url, venueid = venue_id, task_id = venue.celery_task_ID) + cover_image_url = uploaded_media.url + + idx=0 + for img in gallery: + + base64_str = img + if "base64," in base64_str: + base64_str = base64_str.split("base64,")[1] + + gallery_image_bytes = base64.b64decode(base64_str.encode("utf-8")) + uploaded_media_gallery = imagekit.files.upload( + file=gallery_image_bytes, + file_name= f'{venue_name}-gallery-idx', + folder='/gallery' + ) + if uploaded_media_gallery.url: + logger.info("successfully uploaded the gallery image" , url = uploaded_media_gallery.url, venueid = venue_id, task_id = venue.celery_task_ID) + gallery_urls.append(uploaded_media_gallery.url) ## appending the gallery urls back. + + idx += 1 + + venue.cover_image = cover_image_url + venue.gallery = gallery_urls + venue.task_status = 'Completed' + db.commit() + + except Exception as e: + logger.info("Can't upload the media", venueid = venue_id, task_id = venue.celery_task_ID) + venue.task_status = "Failed" + raise e + + + + except Exception as e: + db.rollback() + logger.info("owner media upload task error", venueid = venue_id) + raise e + finally: + db.close() #closing the opened connection + + + diff --git a/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..a89ce8bdd Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/BackgroundWorker/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/__pycache__/main.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/__pycache__/main.cpython-311.pyc new file mode 100644 index 000000000..80d7711e0 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/__pycache__/main.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/__pycache__/worker.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/__pycache__/worker.cpython-311.pyc new file mode 100644 index 000000000..16e51975d Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/__pycache__/worker.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..6dd4bf31c Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/deps.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/deps.cpython-311.pyc new file mode 100644 index 000000000..39017585a Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/__pycache__/deps.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/deps.py b/Backend/bookmyvenue/src/bookmyvenue/api/deps.py new file mode 100644 index 000000000..639060539 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/api/deps.py @@ -0,0 +1,76 @@ +import os +import structlog +import redis.asyncio as aioredis +from dotenv import load_dotenv +from fastapi import Depends, HTTPException, Request, status +from sqlalchemy.orm import Session +from clerk_backend_api import Clerk +from clerk_backend_api.security import authenticate_request +from clerk_backend_api.security.types import AuthenticateRequestOptions + + +from src.bookmyvenue.core.database import session +from src.bookmyvenue.models.admin import Admin +from src.bookmyvenue.repositories.admin.repository import adminRepository +from src.bookmyvenue.repositories.owner.repository import ownerRepository +load_dotenv() + +clerk_SDK = Clerk(bearer_auth=os.getenv("CLERK_API_KEY")) +logger = structlog.get_logger() + +def get_the_db_Session(): + db = session() #generating the new sectio + try: + yield db #yield is used to get the current session pause the function pass the session to the respective function calling the db session and once it completes the job the contol comes back here and closes the connection + finally: + db.close() + + +def get_the_redis_client(request: Request) -> aioredis.Redis: + return request.app.state.redis #accessing the redis client stored in the app.state of the request. + + + + +# this function is used to check the incoming request with siginied in or not +def get_the_current_user(request: Request): + + request_state = clerk_SDK.authenticate_request( + request, + AuthenticateRequestOptions( + authorized_parties=['http://localhost:3000'] + ) + ) + + if not request_state.is_signed_in: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="user is not signed in") + else: + request_payload = request_state.payload + user_id = request_payload['sub'] # user ID + return user_id + + +# Role based checking of the Admin +def admin_only_route(request: Request,db:Session = Depends(get_the_db_Session)): + user_id = get_the_current_user(request=request) #passing the request to the function + admin_user = adminRepository.get_the_admin_by_user(db,user_id) + + if not admin_user: + logger.error("Forbidden request, only allowed for the admin to access" ,clerk_id=user_id) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail="Forbidden, only admin can access this endpoint") + + return admin_user + + +#Role based check for owner +def owner_only_route(request: Request,db:Session = Depends(get_the_db_Session)): + user_id = get_the_current_user(request=request) #passing the request to the function + owner_user = ownerRepository.get_owner_record_by_ID(db,user_id) + + if not owner_user: + logger.error("Forbidden request, only allowed for the owners to access" ,clerk_id=user_id) + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail="Forbidden, only owners can access this endpoint") + + return owner_user + + diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..86589b13b Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..b71aa85f9 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/admin.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/admin.cpython-311.pyc new file mode 100644 index 000000000..0d907396f Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/__pycache__/admin.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/admin.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/admin.py new file mode 100644 index 000000000..ae32b5044 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/api/v1/admin/admin.py @@ -0,0 +1,61 @@ +from typing import List + +import structlog +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +import redis.asyncio as aioredis +from src.bookmyvenue.schema.admin.admin import AmenitesSchema, CategoriesSchema, CategorySchema +from src.bookmyvenue.models.admin import Admin +from src.bookmyvenue.api.deps import admin_only_route, get_the_db_Session, get_the_redis_client +from src.bookmyvenue.services.adminService import adminservice + +logger = structlog.get_logger() +router = APIRouter( + prefix='/admin' +) + +@router.post('/category') +async def add_category(category:CategoriesSchema,db:Session = Depends(get_the_db_Session), admin_user:Admin = Depends(admin_only_route), redis:aioredis.Redis = Depends(get_the_redis_client)): + """ + this endpoint is protected with the role based access such that only admins can access this route. + """ + + category_addition_acknowledment = adminservice.add_category(db=db, categories=category.category) + + if category_addition_acknowledment: + logger.info("successfully created the categories...." , admin_user = admin_user.user_id) + await redis.delete('bmv:categories:all') + raise HTTPException( + status_code=status.HTTP_201_CREATED, + detail=f"successfully created the categories" + ) + else: + logger.error("cant complete the categories addition...." , admin_user = admin_user.user_id) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"some errors occured and cant complete the creation of categories." + ) + + + +@router.post('/amenity') +async def add_amenity(amenity:AmenitesSchema,db:Session = Depends(get_the_db_Session), admin_user:Admin = Depends(admin_only_route), redis:aioredis.Redis = Depends(get_the_redis_client)): + """ + this endpoint is protected with the role based access such that only admins can access this route. + """ + + amenity_addition_acknowledment = adminservice.add_amenity(db=db, amenities=amenity.amenity) + + if amenity_addition_acknowledment: + logger.info("successfully created the amenities...." , admin_user = admin_user.user_id) + await redis.delete('bmv:amenities:all') + raise HTTPException( + status_code=status.HTTP_201_CREATED, + detail=f"successfully created the amenities" + ) + else: + logger.error("cant complete the amenitiy addition...." , admin_user = admin_user.user_id) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"some errors occured and cant complete the creation of amenities." + ) \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/__pycache__/common.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/__pycache__/common.cpython-311.pyc new file mode 100644 index 000000000..50dada6a9 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/__pycache__/common.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/common.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/common.py new file mode 100644 index 000000000..7ce5e038b --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/api/v1/common/common.py @@ -0,0 +1,98 @@ +import json +from typing import List + +from pydantic import TypeAdapter +import structlog +import redis.asyncio as aioredis +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from src.bookmyvenue.schema.admin.admin import AmenitySchema, CategorySchema +from src.bookmyvenue.schema.responce.responces import AmenityFetchedResponce, CategoryFetchedResponce, ResponceAmenitySchema, ResponceCategorySchema +from src.bookmyvenue.api.deps import get_the_db_Session, get_the_redis_client +from src.bookmyvenue.services.commonService import commonService + +logger = structlog.get_logger() +router = APIRouter() + +@router.get("/categories", response_model=CategoryFetchedResponce) +async def get_the_categories( + db: Session = Depends(get_the_db_Session), + redis: aioredis.Redis = Depends(get_the_redis_client) +): + categories_adapter = TypeAdapter(List[ResponceCategorySchema]) + #key used for accessing the categories + CACHE_KEY = 'bmv:categories:all' + cached_categories = await redis.get(CACHE_KEY) + if cached_categories: + logger.info("successfully fetched the categories from cache") + decoded_redis_data = json.loads(cached_categories) + return { + "status_code": 200, + "message": "fetched the categories successfully", + "data": categories_adapter.validate_json(decoded_redis_data) + } + + #get the categories listed in the categories table when it is not found in the redis + categories = commonService.get_the_categories(db) + logger.info("successfully fetched the categories") + + + pydantic_categories = categories_adapter.validate_python( + categories, + from_attributes=True + ) + + # 5. Dump directly to a JSON string using Pydantic's high-performance encoder + # (This avoids using standard json.dumps which crashes on custom objects) + json_string = categories_adapter.dump_json(pydantic_categories).decode("utf-8") + + logger.info("successfully fetched the categories from cache") + await redis.set(CACHE_KEY, json.dumps(json_string)) #No TTL is given since this has to be invalidated based on the event if category creation or deletion + + return { + "status_code": 200, + "message": "fetched the categories successfully", + "data": categories + } + + +@router.get("/amenities", response_model=AmenityFetchedResponce) +async def get_the_amenities( + db: Session = Depends(get_the_db_Session), + redis: aioredis.Redis = Depends(get_the_redis_client) +): + amenities_adapter = TypeAdapter(List[ResponceAmenitySchema]) # type adapter is used to validate a list of objects + #get the categories listed in the categories table + CACHE_KEY = 'bmv:amenities:all' + await redis.delete(CACHE_KEY) + logger.info("successfully fetched the amenities") + cached_amenities = await redis.get(CACHE_KEY) + if cached_amenities: + logger.info("successfully fetched the categories from cache") + decoded_redis_data = json.loads(cached_amenities) + return { + "status_code": 200, + "message": "fetched the categories successfully", + "data": amenities_adapter.validate_json(decoded_redis_data) + } + amenities = commonService.get_the_amenities(db) + + pydantic_amenities = amenities_adapter.validate_python( + amenities, + from_attributes=True + ) + + # 5. Dump directly to a JSON string using Pydantic's high-performance encoder + # (This avoids using standard json.dumps which crashes on custom objects) + json_string = amenities_adapter.dump_json(pydantic_amenities).decode("utf-8") + + logger.info("successfully fetched the categories from cache") + await redis.set(CACHE_KEY, json.dumps(json_string)) #No TTL is given since this has to be invalidated based on the event if category creation or deletion + + return { + "status_code": 200, + "message": "fetched the amenities successfully", + "data": amenities + } + + \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..7c36d0815 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/owner.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/owner.cpython-311.pyc new file mode 100644 index 000000000..a799b5723 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/__pycache__/owner.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/owner.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/owner.py new file mode 100644 index 000000000..3bd1085d2 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/api/v1/owner/owner.py @@ -0,0 +1,93 @@ +from typing import List + +from pydantic import Json, ValidationError + +from src.bookmyvenue.models.owners import Owner +from src.bookmyvenue.schema.common.common import VenueSchema +from fastapi import APIRouter, Depends, Form, File, HTTPException, UploadFile, status +from sqlalchemy.orm import Session +from dotenv import load_dotenv +import structlog +from src.bookmyvenue.services import commonService +from src.bookmyvenue.schema.owner.owner import OwnerOnboardingSchema +from src.bookmyvenue.schema.responce.responces import CreatedVenueResponce, UserCreatedResponce, UserUpdatedResponce +from src.bookmyvenue.api.deps import get_the_current_user, get_the_db_Session, owner_only_route +from src.bookmyvenue.services.ownerServices import ownerservice +from src.bookmyvenue.services.adminService import adminservice + + +load_dotenv() +logger = structlog.get_logger() + + +router = APIRouter( + prefix='/owner' #the prefix that joins `api/v1` prefix with api/v1/user + / => api/v1/user/ +) #this is used to create the routes with reference of app in the main.py of src folder + +@router.get('/check') +def check_owner_profile( + db:Session = Depends(get_the_db_Session), + current_user_id:str = Depends(get_the_current_user) +): + + ownerservice.get_owner_record(db,current_user_id) + logger.info(f"Owner has onboarded!" , clerk_id=current_user_id) + return UserUpdatedResponce(status_code=200,message="Owner has onboarded!") + + + + + + +@router.post('/onboarding') +def complete_onboarding( + onboard:OwnerOnboardingSchema, + db:Session = Depends(get_the_db_Session), + current_user_id:str = Depends(get_the_current_user) +): + + ownerservice.complete_owner_onboarding(db,current_user_id,onboard) + logger.info(f"onboarded the owner successfully" , clerk_id=current_user_id) + return UserUpdatedResponce(status_code=200,message="successfully updated the profile") + + +@router.post('/venue' , response_model=CreatedVenueResponce) +async def create_venue_lisiting( + payload: str = Form(...), + cover_image: UploadFile = File(...), + gallery: List[UploadFile] = File(...), + db:Session = Depends(get_the_db_Session), + owner:Owner = Depends(owner_only_route) +): + + + try: + payload_data =VenueSchema.model_validate_json(payload) + except ValidationError as e: + # If there are any field typos or missing items, this will catch them + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=e.errors() + ) + + #first create a venue table with metadata , along with task_status = 'pending' + #call the media upload task + #save the task id in the venue table + #return added to queue responce + + + + created_venue_instance = ownerservice.create_new_venue(db=db,owner_user=owner, payload=payload_data, cover_image=cover_image, gallery=gallery) + + if not created_venue_instance: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="cant create new venue instance" + ) + + return { + "status_code": 200, + "message": "Successfully added the venue to the queue", + "data": f"{created_venue_instance.name}-{created_venue_instance.task_status}" + } + \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..74aa43a36 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/users.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/users.cpython-311.pyc new file mode 100644 index 000000000..ba6f499df Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/__pycache__/users.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/users.py b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/users.py new file mode 100644 index 000000000..a2e2867cf --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/api/v1/users/users.py @@ -0,0 +1,111 @@ +import os +from fastapi import APIRouter, Depends, HTTPException, Request, Header, status +from sqlalchemy.orm import Session +from dotenv import load_dotenv +from svix.webhooks import Webhook, WebhookVerificationError +import structlog +from src.bookmyvenue.schema.responce.responces import UserCreatedResponce, UserUpdatedResponce +from src.bookmyvenue.schema.user.user import ClerkWebhookEvent, PhoneOnboardingSchema, UserSchema +from src.bookmyvenue.api.deps import admin_only_route, get_the_current_user, get_the_db_Session +from src.bookmyvenue.services.userServices import userservice + + +load_dotenv() +logger = structlog.get_logger() +WEBHOOK_CLERK_SECRET = os.getenv('CLERK_WEBHOOK_SECRET') or '' + +router = APIRouter( + prefix='/user' #the prefix that joins `api/v1` prefix with api/v1/user + / => api/v1/user/ +) #this is used to create the routes with reference of app in the main.py of src folder + + +@router.get("/") +def home(): + logger.info( + "Attempting to sync Clerk user to database", + clerk_id=1, + email="test@gmail" + ) + return UserUpdatedResponce( + status_code=201, + message="new user generated successfully", + ) + + +@router.get("/test") +def test(): + logger.info( + "Attempting to sync Clerk user to database", + clerk_id=1, + email="test@gmail" + ) + return UserUpdatedResponce( + status_code=201, + message="new user generated successfully", + ) + + +@router.post('/webhook/create-user') #this route is used to register the user with the weebhook comming from clerk +async def clerk_webhook_handler( + request: Request, + db: Session = Depends(get_the_db_Session), + svix_id: str = Header(None, alias="svix-id"), + svix_signature: str = Header(None, alias="svix-signature"), + svix_timestamp: str = Header(None, alias="svix-timestamp"), +): #Dependency injection in which the DB session is been needed for thos endpoimt + + + if not svix_id or not svix_signature or not svix_timestamp: + logger.error("Missing required Svix headers") + raise HTTPException(status_code=400, detail="Missing required Svix headers") + + payload = await request.body() #getting the webhook data that is been send + payload_str = payload.decode("utf-8") #stringfiying it + + try: + wh = Webhook(WEBHOOK_CLERK_SECRET) + headers = { + "svix-id": svix_id, + "svix-signature": svix_signature, + "svix-timestamp": svix_timestamp, + } + # This will raise an exception if the signature is invalid or forged + wh.verify(payload_str, headers) + except WebhookVerificationError as e: + logger.error("Invalid signature validation failed") + raise HTTPException(status_code=401, detail="Invalid signature validation failed") + + try: + event = ClerkWebhookEvent.model_validate_json(payload_str) #used to validate the incoming json with the pydantic model we defined + except Exception as e: + logger.error(f"Unparseable Clerk event data: {str(e)}") + raise HTTPException(status_code=422, detail=f"Unparseable Clerk event data: {str(e)}") + + if event.type == "user.created": + new_user = userservice.register_user(db=db,clerk_user=event.data) + validated_user = UserSchema.model_validate(new_user) #used to look on the given databade object , check wheter it matches with the schema and picks the required user fields and converts into pydantic model instance + + return UserCreatedResponce( + status_code=201, + message="new user generated successfully", + data=validated_user + ) + elif event.type == "user.deleted": + is_deleted = userservice.delete_user(db=db,clerk_user=event.data) + if not is_deleted: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,detail="cant delete the user.") + + return UserUpdatedResponce(status_code=200,message="successfully deleted the user") + + +@router.post('/onboarding') +def complete_onboarding( + phone:PhoneOnboardingSchema, + db:Session = Depends(get_the_db_Session), + current_user_id:str = Depends(admin_only_route) +): + + userservice.complete_user_onboarding(db=db,phone=phone,current_user_id=current_user_id) + logger.info(f"onboarded the user successfully" , clerk_id=current_user_id) + return UserUpdatedResponce(status_code=200,message="successfully updated the profile") + \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..5655de7d1 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/config.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/config.cpython-311.pyc new file mode 100644 index 000000000..7d4c413c2 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/config.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/database.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/database.cpython-311.pyc new file mode 100644 index 000000000..ba0b32439 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/database.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/logging.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/logging.cpython-311.pyc new file mode 100644 index 000000000..30236c0bc Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/core/__pycache__/logging.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/config.py b/Backend/bookmyvenue/src/bookmyvenue/core/config.py new file mode 100644 index 000000000..4a4c92e8a --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/core/config.py @@ -0,0 +1,35 @@ +from functools import lru_cache +import os +from dotenv import load_dotenv +from typing import Literal + +from pydantic_settings import BaseSettings, SettingsConfigDict + +load_dotenv() +database_url = os.getenv('DATABASE_URL') or '' + +class Settings(BaseSettings): + APP_NAME: str = "BookMyVenue" + ENVIRONMENT: Literal["development", "staging", "production"] = "production" + DEBUG: bool = True + + DATABASE_URL: str | None = database_url + + UPSTASH_REDIS_CELERY_URL: str = os.getenv("UPSTASH_REDIS_CELERY_URL") or "" + CELERY_RESULT_BACKEND: str = f"db+{DATABASE_URL}" + + model_config = SettingsConfigDict( + # Read from a local environment file if variables are not in env + env_file=".env", + # Ignore extra environment variables passed to the container + extra="ignore", + # Case-insensitive environment matching + case_sensitive=False # Changed to False for looser, safer cross-platform container deploys + ) + +@lru_cache # this decorator is used to cache the expensive function calls , should be used in the fuctions that retrns sam thing again and again , not the functions that generatees dynamic content. +def get_settings() -> Settings: + return Settings() + +# Instantiate for easy structural access across your app components +settings = get_settings() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/database.py b/Backend/bookmyvenue/src/bookmyvenue/core/database.py new file mode 100644 index 000000000..a18513ff2 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/core/database.py @@ -0,0 +1,12 @@ + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase + +from src.bookmyvenue.core.config import settings +#Just a common base class +class Base(DeclarativeBase): + pass + +DATABASE_URL = settings.DATABASE_URL or '' +engine = create_engine(DATABASE_URL) +session = sessionmaker(autocommit = False, autoflush=True, bind=engine) \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/logging.py b/Backend/bookmyvenue/src/bookmyvenue/core/logging.py new file mode 100644 index 000000000..2becb4cea --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/core/logging.py @@ -0,0 +1,38 @@ +import logging +import sys +# import os +import structlog +from src.bookmyvenue.core.config import settings + +def setup_logging() -> None: + + is_production = False #this is used to track whether the + + processors = [ + structlog.contextvars.merge_contextvars, # Pulls metadata like request_id automatically + structlog.processors.add_log_level, # Adds "level": "INFO" + structlog.processors.TimeStamper(fmt="iso"), # Adds ISO 8601 UTC timestamp + structlog.processors.StackInfoRenderer(), # Captures call stack on errors + structlog.processors.format_exc_info, # Gracefully serializes traceback objects + ] + + if is_production: + # Production Pipeline: Stream pure JSON strings + processors.append(structlog.processors.JSONRenderer()) + else: + # Development Pipeline: Stream colored, readable lines + processors.append(structlog.dev.ConsoleRenderer(colors=True)) + + + structlog.configure( + processors=processors, + logger_factory=structlog.PrintLoggerFactory(), + wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), + cache_logger_on_first_use=True, + ) + + logging.basicConfig( + format="%(message)s", + stream=sys.stdout, #output the log in the terminal + level=logging.INFO, + ) \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/middleware/__pycache__/logging.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/core/middleware/__pycache__/logging.cpython-311.pyc new file mode 100644 index 000000000..3ac571144 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/core/middleware/__pycache__/logging.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/core/middleware/logging.py b/Backend/bookmyvenue/src/bookmyvenue/core/middleware/logging.py new file mode 100644 index 000000000..4edee9851 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/core/middleware/logging.py @@ -0,0 +1,23 @@ +import uuid +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +import structlog + +class LoggingContextMiddleware(BaseHTTPMiddleware): + """ + Global middleware to attach unique request tracking IDs + to all log messages asynchronously. + """ + async def dispatch(self, request: Request, call_next): + request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) + + + structlog.contextvars.clear_contextvars() + structlog.contextvars.bind_contextvars( + request_id=request_id, + path=request.url.path, + method=request.method, + ) + + response = await call_next(request) + return response \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/main.py b/Backend/bookmyvenue/src/bookmyvenue/main.py new file mode 100644 index 000000000..0f0ed3152 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/main.py @@ -0,0 +1,94 @@ +import os +import structlog + +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException, Request, Response +from fastapi.responses import JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from upstash_redis.asyncio import Redis + +from src.bookmyvenue.schema.responce.responces import HealthStatusResponce +from src.bookmyvenue.core.logging import setup_logging +from src.bookmyvenue.core.middleware.logging import LoggingContextMiddleware +from src.bookmyvenue.api.v1.users import users +from src.bookmyvenue.api.v1.owner import owner +from src.bookmyvenue.api.v1.admin import admin +from src.bookmyvenue.api.v1.common import common + +setup_logging() +logger = structlog.get_logger() + +UPSTASH_REDIS_REST_URL = os.getenv('UPSTASH_REDIS_REST_URL') or "" +UPSTASH_REDIS_REST_TOKEN = os.getenv('UPSTASH_REDIS_REST_TOKEN') or "" + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + this lifespan function is used to initialize a open connection when the app is up and will close that connection once the app stops running + """ + logger.info("App has started, setting up the redis client") + app.state.redis = Redis(url=UPSTASH_REDIS_REST_URL, token=UPSTASH_REDIS_REST_TOKEN) #app.state is used to pass this variable to anywhree in the program. + yield + + logger.info("Detected that App has been closed, closing the initialized redis.........") + await app.state.redis.close() + + +app = FastAPI(lifespan=lifespan) #initializing the app with the lifespan + +origins = [ + "http://localhost:3000", +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(LoggingContextMiddleware) + + + +@app.exception_handler(HTTPException) +async def custom_http_exception_handler(request: Request, exc: HTTPException): + + origin = request.headers.get("origin") + + response = JSONResponse( + status_code=exc.status_code, + content={ + "status_code": exc.status_code, + "message": exc.detail, + "data": None + } + ) + + # Manually re-attach headers so the browser doesn't block the error message + if origin: + response.headers["Access-Control-Allow-Origin"] = origin + response.headers["Access-Control-Allow-Credentials"] = "true" + response.headers["Access-Control-Allow-Methods"] = "*" + response.headers["Access-Control-Allow-Headers"] = "*" + + return response + + + +@app.get('/health-check') +def start(): + logger.info("the server is running strong") + return HealthStatusResponce(message=" well!" , status_code=200) + +#Including the apis used in the project +app.include_router(users.router , prefix='/api/v1') +app.include_router(owner.router , prefix='/api/v1') +app.include_router(admin.router , prefix='/api/v1') +app.include_router(common.router , prefix='/api/v1') + +def main(): + print("Hello from bookmyvenue!") + + +if __name__ == "__main__": + main() diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/models/__init__.py new file mode 100644 index 000000000..d39477928 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/models/__init__.py @@ -0,0 +1,14 @@ +# src/models/__init__.py + +#when the src.model is called all the imports listed here will get grouped together and can access them easily, (MOST NEEDED FOR DB MIGRATIONS) + + + +# Import every model class here so they register with the central Base +from src.bookmyvenue.models.user import User +from src.bookmyvenue.models.owners import Owner, PriceManager +from src.bookmyvenue.models.admin import Admin, Category, Amenity +from src.bookmyvenue.models.common import Venue + +# Package them up cleanly +__all__ = ["User", "Owner", "Admin", "Category", "Amenity", "PriceManager", "Venue"] \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..5cd76e976 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/admin.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/admin.cpython-311.pyc new file mode 100644 index 000000000..4a19bec6d Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/admin.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/common.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/common.cpython-311.pyc new file mode 100644 index 000000000..3fda0ee44 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/common.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/owners.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/owners.cpython-311.pyc new file mode 100644 index 000000000..6a2d0759d Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/owners.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/user.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 000000000..093124a29 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/models/__pycache__/user.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/admin.py b/Backend/bookmyvenue/src/bookmyvenue/models/admin.py new file mode 100644 index 000000000..7a1d6bb71 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/models/admin.py @@ -0,0 +1,34 @@ +from typing import Optional, List + +from sqlalchemy import ForeignKey, String, Text + +from src.bookmyvenue.core.database import Base +from sqlalchemy.orm import Mapped, mapped_column, relationship + + +class Admin(Base): + __tablename__ = "admin" + + id: Mapped[int] = mapped_column( primary_key=True , index=True , nullable=False , autoincrement=True) + user_id: Mapped[int] = mapped_column(ForeignKey('users.id')) #go to users table, pick it's ID and place it in this column as foreign key relation + + user: Mapped[Optional["User"]] = relationship( + back_populates="admin" + ) + +class Category(Base): + __tablename__ = "categories" + id: Mapped[int] = mapped_column( primary_key=True , index=True , nullable=False , autoincrement=True) + icon_name:Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + name:Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + + venues:Mapped[List["Venue"]] = relationship(secondary="venue_category_table",back_populates="categories") + +class Amenity(Base): + __tablename__ = "amenities" + id: Mapped[int] = mapped_column( primary_key=True , index=True , nullable=False , autoincrement=True) + icon_name:Mapped[str] = mapped_column(String(20), unique=True, nullable=False) + name:Mapped[str] = mapped_column(String(100), unique=True, nullable=False) + + + venues:Mapped[List["Venue"]] = relationship(secondary="venue_amenity_table",back_populates="amenities") \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/common.py b/Backend/bookmyvenue/src/bookmyvenue/models/common.py new file mode 100644 index 000000000..5ecb49d39 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/models/common.py @@ -0,0 +1,86 @@ +from decimal import Decimal +from typing import List, Optional +from sqlalchemy import ARRAY, Boolean, Column, DateTime, ForeignKey, Numeric, String, Table, Text, false, func +from sqlalchemy.orm import Mapped, mapped_column, relationship +from datetime import datetime +from src.bookmyvenue.core.database import Base + +venue_category_table = Table( + "venue_category_table", + Base.metadata, + Column("venue_id", ForeignKey("venues.id"), primary_key=True), + Column("category_id", ForeignKey("categories.id"), primary_key=True), +) + +venue_amenity_table = Table( + "venue_amenity_table", + Base.metadata, + Column("venue_id", ForeignKey("venues.id"), primary_key=True), + Column("amenity_id", ForeignKey("amenities.id"), primary_key=True), +) + + +class Venue(Base): + __tablename__ = "venues" + + id: Mapped[int] = mapped_column( primary_key=True , index=True , nullable=False , autoincrement=True) + categories: Mapped[List['Category']] = relationship( + secondary=venue_category_table, + back_populates='venues' + ) + amenities: Mapped[List['Amenity']] = relationship( + secondary=venue_amenity_table, + back_populates='venues' + ) + owner_id: Mapped[int] = mapped_column(ForeignKey('owners.id')) + owner: Mapped['Owner'] = relationship(back_populates="venues") + name: Mapped[str] = mapped_column(String(255), nullable=False) + max_capacity: Mapped[int] = mapped_column(nullable=False, default=0) + city: Mapped[str] = mapped_column(String(255), nullable=False) + district: Mapped[str] = mapped_column(String(255), nullable=False) + state: Mapped[str] = mapped_column(String(255), nullable=False) + country: Mapped[str] = mapped_column(String(255), nullable=False) + location_url: Mapped[str] = mapped_column(Text, nullable=False) + description: Mapped[str] = mapped_column(Text, nullable=False) + cover_image: Mapped[str] = mapped_column(Text, nullable=False, default='https://www.magnific.com/vectors/venue-booking') + cancellation: Mapped[bool] = mapped_column(Boolean , default=False) + cancellation_percentage: Mapped[int] = mapped_column( default=0) + street_address: Mapped[str] = mapped_column(Text, nullable=False) + minimum_slot_duration: Mapped[int] = mapped_column(default=2, nullable=False) + cancellation_time_limit: Mapped[int] = mapped_column(default=0, nullable=False) + total_reviews: Mapped[int] = mapped_column(default=0, nullable=False) + hourly_rent: Mapped[int] = mapped_column(default=1000, nullable=False) + approval_status: Mapped[bool] = mapped_column(default=False, nullable=False) + rejection_reason: Mapped[str] = mapped_column(Text, nullable=True) + task_status: Mapped[str] = mapped_column(String(255), default='Pending' , nullable=True) #options available ---> *Pending , *Uploading, *Completed, *Failed + celery_task_ID: Mapped[str] = mapped_column(Text, nullable=True) + overall_rating: Mapped[Decimal] = mapped_column( + Numeric(precision=5,scale=2), #precision refers with the total numbers exist with scale which determines no of digits after decimal point + default=Decimal("0.00"), + nullable=False, + ) + gallery: Mapped[List[str]] = mapped_column(ARRAY(String), default=list, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now() # when a user is created add the server's that respective time in the created_at column + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), # when a user is created add the server's that respective time in the created_at column + onupdate=func.now() # when a user updates the exisiting model + ) + + price_manager: Mapped[Optional['PriceManager']] = relationship( + back_populates='venue', + cascade="all, delete-orphan" + ) + + + def __repr__(self) -> str: + return f"" + + + + + + \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/owners.py b/Backend/bookmyvenue/src/bookmyvenue/models/owners.py new file mode 100644 index 000000000..26be2a8f3 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/models/owners.py @@ -0,0 +1,46 @@ +from typing import List, Optional + +from sqlalchemy import DateTime, ForeignKey, String, Text, func, Integer +from sqlalchemy.orm import Mapped , mapped_column , DeclarativeBase, relationship +from src.bookmyvenue.core.database import Base #Importing the unified Base from the main database.py file +from datetime import datetime + + +class Owner(Base): + __tablename__ = "owners" + + id: Mapped[int] = mapped_column( primary_key=True , nullable=False , autoincrement=True) + user_id: Mapped[int] = mapped_column(ForeignKey('users.id')) #go to users table, pick it's ID and place it in this column as foreign key relation + profession: Mapped[str] = mapped_column(String(100), nullable=False) + promise: Mapped[str] = mapped_column(Text, nullable=False) + intro_descp: Mapped[str] = mapped_column(Text, nullable=True) + organization: Mapped[str] = mapped_column(String(200), nullable=False) + venues_listed: Mapped[int] = mapped_column(Integer , default=0) + overall_ratting: Mapped[int] = mapped_column(Integer , default=0) + joined_on: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now() # when a user is created add the server's that respective time in the created_at column + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), # when a user is created add the server's that respective time in the created_at column + onupdate=func.now() # when a user updates the exisiting model + ) + user: Mapped[Optional["User"]] = relationship( + back_populates="owner" + ) + venues: Mapped[Optional[List['Venue']]] = relationship(back_populates='owner',cascade="all, delete-orphan") + + def __repr__(self) -> str: + return f"" + + + +class PriceManager(Base): + __tablename__ = "price_manager" + + id: Mapped[int] = mapped_column( primary_key=True , nullable=False , autoincrement=True) + venue_id: Mapped[int] = mapped_column(ForeignKey('venues.id')) + venue:Mapped['Venue'] = relationship(back_populates="price_manager") + standerd_price: Mapped[int] = mapped_column(default=1000, nullable=False) + \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/models/user.py b/Backend/bookmyvenue/src/bookmyvenue/models/user.py new file mode 100644 index 000000000..da88ce0a7 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/models/user.py @@ -0,0 +1,41 @@ +from typing import Optional + +from sqlalchemy import DateTime, String, func +from sqlalchemy.orm import Mapped , mapped_column , DeclarativeBase, relationship +from src.bookmyvenue.core.database import Base #Importing the unified Base from the main database.py file +from datetime import datetime + + + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column( primary_key=True , index=True , nullable=False , autoincrement=True) + clerkUserID: Mapped[str] = mapped_column( index=True , nullable=False ) + username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) + email: Mapped[str] = mapped_column(nullable=False , unique=True) + fullname: Mapped[str] = mapped_column(nullable=False) + phone: Mapped[Optional[str]] = mapped_column(String(10)) #we need phone number which only contains 10 digits + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now() # when a user is created add the server's that respective time in the created_at column + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), # when a user is created add the server's that respective time in the created_at column + onupdate=func.now() # when a user updates the exisiting model + ) + + owner: Mapped[Optional["Owner"]] = relationship( + back_populates="user", + cascade="all, delete-orphan" + ) + + admin: Mapped[Optional["Admin"]] = relationship( + back_populates="user", + cascade="all, delete-orphan" + ) + + def __repr__(self) -> str: + return f"" \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..dca9a76dc Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..042084570 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/repository.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/repository.cpython-311.pyc new file mode 100644 index 000000000..ae56ea4ad Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/__pycache__/repository.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/repository.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/repository.py new file mode 100644 index 000000000..f34bd2880 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/repositories/admin/repository.py @@ -0,0 +1,105 @@ +from fastapi import HTTPException,status +from sqlalchemy import insert +from sqlalchemy.exc import SQLAlchemyError +import structlog +from typing import List, Optional +from sqlalchemy.orm import Session + +from src.bookmyvenue.schema.admin.admin import AmenitySchema, CategorySchema +from src.bookmyvenue.schema.user.user import PhoneOnboardingSchema +from src.bookmyvenue.models.user import User +from src.bookmyvenue.models.admin import Admin, Amenity, Category + +logger = structlog.get_logger() +class AdminRepository: + def get_the_admin_by_user(self, db:Session, clerk_id:str) -> Optional[Admin]: + clerk_user = db.query(User).filter_by(clerkUserID=clerk_id).first() #filter based search is fast for getting a spevific element + if not clerk_user: + logger.error("user record not found for checking whether he/she is admin" ,clerk_id=clerk_id) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="user not found in DB") + return db.query(Admin).filter_by(user=clerk_user).first() + + def check_the_existing_category(self, db:Session, categories:List[CategorySchema]): + category_names = [cat.name for cat in categories] #collecting all the category names first + + return db.query(Category).filter(Category.name.in_(category_names)).all() # .in_ checks with the entire data present in the DB with the given names + + def check_the_existing_amenity(self, db:Session, amenities:List[AmenitySchema]): + amenities_names = [amenity.name for amenity in amenities] #collecting all the category names first + + return db.query(Amenity).filter(Amenity.name.in_(amenities_names)).all() # .in_ checks with the entire data present in the DB with the given names + + def add_the_categories(self, db:Session, categories:List[CategorySchema]) -> bool: + #performing bulk insert in the db + + #convert the categories into plain list of dicts + + dict_categoryies_to_add = [] + + for category in categories: + dict_item = { + "name" : category.name, + "icon_name" : category.icon_name + } + dict_categoryies_to_add.append(dict_item) + + # this execute is used to bulk add the categories + try: + + db.execute( + insert(Category), + dict_categoryies_to_add + ) + + db.commit() + except SQLAlchemyError as e: + db.rollback() # is used to roll back the updates if any problem with the bulk addition + + logger.error("something went wrong with the adding of the categories....") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Transaction failed and was entirely reversed. Error: {str(e)}" + ) + + return True + + def add_the_amenities(self, db:Session, amenities:List[AmenitySchema]) -> bool: + #performing bulk insert in the db + + #convert the categories into plain list of dicts + + dict_amenities_to_add = [] + + for amenity in amenities: + dict_item = { + "name" : amenity.name, + "icon_name" : amenity.iconName + } + dict_amenities_to_add.append(dict_item) + + # this execute is used to bulk add the categories + try: + + db.execute( + insert(Amenity), + dict_amenities_to_add + ) + + db.commit() + except SQLAlchemyError as e: + db.rollback() # is used to roll back the updates if any problem with the bulk addition + + logger.error("something went wrong with the adding of the amenities....") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Transaction failed and was entirely reversed. Error: {str(e)}" + ) + + return True + + + + + + +adminRepository = AdminRepository() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..a55e9d7aa Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/repository.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/repository.cpython-311.pyc new file mode 100644 index 000000000..45dfe2cbf Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/__pycache__/repository.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/common/repository.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/repository.py new file mode 100644 index 000000000..b665a8290 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/repositories/common/repository.py @@ -0,0 +1,17 @@ +from fastapi import HTTPException,status +import structlog +from typing import List, Optional +from sqlalchemy.orm import Session + +from src.bookmyvenue.models.admin import Amenity, Category + + +logger = structlog.get_logger() + +class CommonRepository: + def fetch_all_the_categories(self, db:Session) -> Optional[List[Category]]: + return db.query(Category).all() + def fetch_all_the_amenities(self, db:Session) -> Optional[List[Amenity]]: + return db.query(Amenity).all() + +commonRepository = CommonRepository() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..6587fe960 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/repository.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/repository.cpython-311.pyc new file mode 100644 index 000000000..dee3d0036 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/__pycache__/repository.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/repository.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/repository.py new file mode 100644 index 000000000..ed0705863 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/repositories/owner/repository.py @@ -0,0 +1,95 @@ +from fastapi import File, HTTPException, UploadFile,status +from pydantic import Json +from sqlalchemy import Select +import structlog +from typing import List, Optional +from sqlalchemy.orm import Session + +from src.bookmyvenue.models.admin import Amenity, Category +from src.bookmyvenue.schema.common.common import ImageKitVenueUrls, VenueSchema +from src.bookmyvenue.models.common import Venue +from src.bookmyvenue.schema.owner.owner import OwnerOnboardingSchema +from src.bookmyvenue.schema.responce.responces import UserNotFoundResponce +from src.bookmyvenue.schema.user.user import PhoneOnboardingSchema +from src.bookmyvenue.models.user import User +from src.bookmyvenue.models.owners import Owner + +logger = structlog.get_logger() + +class OwnerRepository: + def get_owner_record_by_ID(self, db:Session, current_user_id:str) -> Optional[Owner]: + return db.query(Owner).join(User).filter(User.clerkUserID == current_user_id).first() + + def onboard_owner(self, db:Session,clerk_user:User, onboard:OwnerOnboardingSchema) -> Owner: + logger.info("creating the owner record forthe user" , userid = clerk_user.id) + owner = Owner( + user = clerk_user, + profession = onboard.profession, + promise = onboard.promise, + intro_descp = onboard.self_info, + organization = onboard.organization + ) + db.add(owner) + db.commit() + db.refresh(owner) + return owner + + def duplicate_venue_checker(self, db:Session, owner:Owner, name:str, city:str, street_address:str) -> bool: + duplicate_venue = db.query(Venue).filter( + Venue.owner == owner, + Venue.name.ilike(name), + Venue.city.ilike(city), + Venue.street_address.ilike(street_address), + ).first() + + if duplicate_venue: + logger.info("already an venue exists with same name in same city at same location for the same owner.", owner = owner.user_id, venue_name = name, venue_city = city, venue_street_address = street_address) + raise HTTPException(status_code=status.HTTP_409_CONFLICT , detail="already an venue exists with same name in same city at same location") + + return False + def create_venue_record(self, db:Session, owner:Owner, payload:Json[VenueSchema]) -> Venue: + # we have to decode the categories and amenities first sice we have to apply relation with them + + category_command = Select(Category).where(Category.id.in_(payload.categories)) + categories_instances =db.execute(category_command) + list_of_categories = categories_instances.scalars().all() #returns the instance as a python list + logger.info("collecting the categories" , ids = payload.categories) + + amenities_command = Select(Amenity).where(Amenity.id.in_(payload.amenities)) + amenities_instances = db.execute(amenities_command) + list_of_amenities = amenities_instances.scalars().all() #returns the instance as a python list + logger.info("collecting the amenities" , ids = payload.amenities) + + # now we are creating the venue listings with the collected documents. + logger.info("creating the venue with the details" , owner = owner.user_id, venue_name = payload.name, venue_city = payload.city, venue_street_address = payload.street_address) + venue_instance = Venue( + categories = list_of_categories, + amenities = list_of_amenities, + owner = owner, + name = payload.name, + max_capacity = payload.max_capacity, + street_address = payload.street_address, + city = payload.city, + district = payload.district, + state = payload.state, + country = payload.country, + location_url = payload.location_url, + description = payload.description, + cancellation_percentage = payload.cancellation_percentage, + minimum_slot_duration = payload.minimum_slot_duration, + cancellation_time_limit = payload.cancellation_time_limit, + hourly_rent = payload.hourly_rent, + task_status = "Pending" + ) + + db.add(venue_instance) + db.commit() + logger.info("commiting the venue in the db" , owner = owner.user_id, venue_name = payload.name, venue_city = payload.city, venue_street_address = payload.street_address) + db.refresh(venue_instance) + + return venue_instance + + + + +ownerRepository = OwnerRepository() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/users/__pycache__/repository.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/repositories/users/__pycache__/repository.cpython-311.pyc new file mode 100644 index 000000000..be7cabac7 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/repositories/users/__pycache__/repository.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/repositories/users/repository.py b/Backend/bookmyvenue/src/bookmyvenue/repositories/users/repository.py new file mode 100644 index 000000000..a9348c6fa --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/repositories/users/repository.py @@ -0,0 +1,52 @@ +from fastapi import HTTPException,status +import structlog +from typing import Optional +from sqlalchemy.orm import Session + +from src.bookmyvenue.schema.responce.responces import UserNotFoundResponce +from src.bookmyvenue.schema.user.user import PhoneOnboardingSchema +from src.bookmyvenue.models.user import User + +logger = structlog.get_logger() +class UserRepository: + def get_user_by_id(self, db:Session, clerk_id:str) -> Optional[User]: + return db.query(User).filter_by(clerkUserID=clerk_id).first() #filter based search is fast for getting a spevific element + + def create_clerk_user(self, db:Session, clerk_id:str, email:str, fullname:str, username:str) -> User: + logger.info("registering the user " , clerk_id=clerk_id, email = email) + dbOject = User( + clerkUserID=clerk_id, + email=email, + username=username, + fullname=fullname, + ) + db.add(dbOject) + logger.info("commiting the user to DB" , clerk_id=clerk_id, email = email) + db.commit() + db.refresh(dbOject) + logger.info("created the user in DB" , clerk_id=clerk_id, email = email) + return dbOject + + def update_user_onboarding(self, db:Session, clerk_id:str, phone:PhoneOnboardingSchema) -> User: + current_user = db.query(User).filter_by(clerkUserID = clerk_id).first() + if not current_user: + logger.error("user record not found" ,clerk_id=clerk_id) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="user not found in DB") + + current_user.phone = phone.phone + + logger.info("commiting the user onboarding to DB" , clerk_id=clerk_id) + db.commit() + db.refresh(current_user) + return current_user + + def detele_user(self, db:Session, clerk_user:User): + logger.info("deleting the user from DB" , clerk_id=clerk_user.clerkUserID) + try: + db.delete(clerk_user) + db.commit() + return True + except Exception as e: + return False + +userRepository = UserRepository() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/schema/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..dc0673586 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..c8a2ff873 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/admin.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/admin.cpython-311.pyc new file mode 100644 index 000000000..6cc860beb Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/__pycache__/admin.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/admin/admin.py b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/admin.py new file mode 100644 index 000000000..4684dd952 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/schema/admin/admin.py @@ -0,0 +1,19 @@ +from typing import List + +from pydantic import BaseModel, Field + + +class CategorySchema(BaseModel): + name: str + icon_name: str # this field validation helps to match the 'icon_name' column of the Category table to map 'iconName' coming from frontend + +class CategoriesSchema(BaseModel): + category: List[CategorySchema] + + +class AmenitySchema(BaseModel): + name: str + icon_name: str + +class AmenitesSchema(BaseModel): + amenity: List[AmenitySchema] \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/common/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/schema/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..d1a6e92fd Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/common.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/common.cpython-311.pyc new file mode 100644 index 000000000..d6e549e9d Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/common/__pycache__/common.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/common/common.py b/Backend/bookmyvenue/src/bookmyvenue/schema/common/common.py new file mode 100644 index 000000000..0336f1c58 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/schema/common/common.py @@ -0,0 +1,26 @@ +import decimal +from typing import List, Optional + +from pydantic import BaseModel + +class VenueSchema(BaseModel): + name: str + max_capacity: str + city: str + district: str + state: str + country: str + location_url: str + description: str + cancellation: bool + cancellation_percentage: Optional[int] + street_address: str + minimum_slot_duration: str + cancellation_time_limit: str + categories: List[int] = [] + amenities: List[int] = [] + hourly_rent: int + +class ImageKitVenueUrls(BaseModel): + cover_image_url: str + gallery_images: List[str] \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..5d9041881 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/owner.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/owner.cpython-311.pyc new file mode 100644 index 000000000..d9928b29b Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/__pycache__/owner.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/owner/owner.py b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/owner.py new file mode 100644 index 000000000..49d008150 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/schema/owner/owner.py @@ -0,0 +1,12 @@ +import datetime + +from pydantic import BaseModel, ConfigDict, EmailStr, Field +from typing import List, Dict, Any, Optional + + +class OwnerOnboardingSchema(BaseModel): + organization: str + profession: Optional[str] = None + promise: str + self_info: Optional[str] = None + diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/responce/__pycache__/responces.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/responce/__pycache__/responces.cpython-311.pyc new file mode 100644 index 000000000..b184e5109 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/responce/__pycache__/responces.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/responce/responces.py b/Backend/bookmyvenue/src/bookmyvenue/schema/responce/responces.py new file mode 100644 index 000000000..4d4e19f1e --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/schema/responce/responces.py @@ -0,0 +1,64 @@ + +from typing import List + +from pydantic import BaseModel, ConfigDict + + +from src.bookmyvenue.schema.common.common import VenueSchema +from src.bookmyvenue.schema.admin.admin import AmenitySchema, CategorySchema +from src.bookmyvenue.schema.user.user import UserSchema + + +class BaseResponceClass(BaseModel): + model_config = ConfigDict(from_attributes=True) + + status_code:int + message:str + + +class ResponceCategorySchema(BaseModel): + model_config = ConfigDict(from_attributes=True) + id:int + name: str + icon_name: str + +class ResponceAmenitySchema(BaseModel): + model_config = ConfigDict(from_attributes=True) + id:int + name: str + icon_name: str + +class UserCreatedResponce(BaseResponceClass): + model_config = ConfigDict(from_attributes=True) + data: UserSchema + + +class CategoryFetchedResponce(BaseResponceClass): + model_config = ConfigDict(from_attributes=True) + data: List[ResponceCategorySchema] + + +class AmenityFetchedResponce(BaseResponceClass): + model_config = ConfigDict(from_attributes=True) + data: List[ResponceAmenitySchema] + + +class CreatedVenueResponce(BaseResponceClass): + model_config = ConfigDict(from_attributes=True) + data: str + +class UserUpdatedResponce(BaseResponceClass): + responce_type:str = "ResourceUpdated" + + +class HealthStatusResponce(BaseResponceClass): + status_code:int + message:str + + +class UserNotFoundResponce(BaseResponceClass): + responce_type:str = "NotFound" + + +class UserNotAuthenticatedResponce(BaseResponceClass): + responce_type:str = "Unauthorized" \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/responce/suiiii.html b/Backend/bookmyvenue/src/bookmyvenue/schema/responce/suiiii.html new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/user/__pycache__/user.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/schema/user/__pycache__/user.cpython-311.pyc new file mode 100644 index 000000000..ea54a6970 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/schema/user/__pycache__/user.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/schema/user/user.py b/Backend/bookmyvenue/src/bookmyvenue/schema/user/user.py new file mode 100644 index 000000000..0e3ccda7f --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/schema/user/user.py @@ -0,0 +1,50 @@ +import datetime + +from pydantic import BaseModel, ConfigDict, EmailStr, Field +from typing import List, Dict, Any, Optional + +class ClerkEmailAddress(BaseModel): + email_address: EmailStr + id: str + +class ClerkWebhookData(BaseModel): + id: str # This is the Clerk User ID (e.g., user_2F...) + email_addresses: Optional[List[ClerkEmailAddress]] = None + first_name: Optional[str] = None + username: Optional[str] = None + last_name: Optional[str] = None + image_url: Optional[str] = None + primary_email_address_id: Optional[str] = None + profile_image_url: Optional[str] = None + deleted:Optional[bool] = True + object:str = "user" + + +class ClerkWebhookEvent(BaseModel): + data: ClerkWebhookData + object: str # Will be "event" + type: str # e.g., "user.created" or "user.updated" + + +class UserBase(BaseModel): + clerkUserID: str + username: str + email: EmailStr + fullname: str + phone: Optional[str] = Field(default=None, max_length=10) + +class UserSchema(UserBase): + """ + Schema for reading user data (Output/Response) + """ + model_config = ConfigDict(from_attributes=True) + + id: int + created_at: datetime.datetime + updated_at: datetime.datetime + + def __repr__(self) -> str: + return f"" + +class PhoneOnboardingSchema(BaseModel): + phone: str = Field(max_length=10) \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..6346aa4d5 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/adminService.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/adminService.cpython-311.pyc new file mode 100644 index 000000000..5ec06d804 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/adminService.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/commonService.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/commonService.cpython-311.pyc new file mode 100644 index 000000000..31f91bdf7 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/commonService.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownerServices.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownerServices.cpython-311.pyc new file mode 100644 index 000000000..4905bf722 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownerServices.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownertask.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownertask.cpython-311.pyc new file mode 100644 index 000000000..b16e70850 Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/ownertask.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/userServices.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/userServices.cpython-311.pyc new file mode 100644 index 000000000..b8e571a4d Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/services/__pycache__/userServices.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/adminService.py b/Backend/bookmyvenue/src/bookmyvenue/services/adminService.py new file mode 100644 index 000000000..c53ee81aa --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/services/adminService.py @@ -0,0 +1,82 @@ +from typing import List, Optional +import uuid + +from fastapi import File, HTTPException, UploadFile, status +import structlog +from sqlalchemy.orm import Session + + +from src.bookmyvenue.schema.common.common import ImageKitVenueUrls +from src.bookmyvenue.utils.image_kit import imagekit +from src.bookmyvenue.schema.admin import admin +from src.bookmyvenue.models.owners import Owner +from src.bookmyvenue.models.user import User +from src.bookmyvenue.repositories.users.repository import userRepository +from src.bookmyvenue.repositories.owner.repository import ownerRepository +from src.bookmyvenue.repositories.admin.repository import adminRepository +from src.bookmyvenue.schema.user import user +from src.bookmyvenue.schema.owner import owner + + +logger = structlog.get_logger() + +class AdminService: + def add_category(self, db:Session, categories: List[admin.CategorySchema]) : + #check if that same category exists , if so raise error + + existing_categories = adminRepository.check_the_existing_category(db, categories) + if existing_categories: + logger.error("some of the categories already exists" ,exisiting_categories=existing_categories) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="some of the categories already exists") + + return adminRepository.add_the_categories(db, categories) + + def add_amenity(self, db:Session, amenities: List[admin.AmenitySchema]): + #check if that same category exists , if so raise error + + existing_categories = adminRepository.check_the_existing_amenity(db, amenities) + if existing_categories: + logger.error("some of the amenities already exists" ,exisiting_categories=existing_categories) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,detail="some of the amenities already exists") + + return adminRepository.add_the_amenities(db, amenities) + + async def upload_venue_images(self,cover_image: UploadFile = File(...),gallery: List[UploadFile] = File(...)) -> ImageKitVenueUrls: + cover_image_url = '' + gallery_urls = [] + #First we upload the images to the imagekit storage and then return the urls to the route control + try: + logger.info("trying to upload the cover image") + cover_image_data = await cover_image.read() + uploaded_media = imagekit.files.upload( + file=cover_image_data, + file_name=cover_image.filename or '', + folder='/covers' + ) + if uploaded_media.url: + logger.info("successfully uploaded the cover image" , url = uploaded_media.url) + cover_image_url = uploaded_media.url + + + for img in gallery: + img_bytes = await img.read() + uploaded_media_gallery = imagekit.files.upload( + file=img_bytes, + file_name=img.filename or '', + folder='/gallery' + ) + if uploaded_media_gallery.url: + logger.info("successfully uploaded the cover image" , url = uploaded_media_gallery.url) + gallery_urls.append(uploaded_media_gallery.url) ## appending the gallery urls back. + + except Exception as e: + logger.error("some error occured while dealing with image upload" , e) + + return ImageKitVenueUrls( + cover_image_url=cover_image_url, + gallery_images=gallery_urls, + ) + + + +adminservice = AdminService() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/commonService.py b/Backend/bookmyvenue/src/bookmyvenue/services/commonService.py new file mode 100644 index 000000000..ca5894ee2 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/services/commonService.py @@ -0,0 +1,37 @@ +from typing import List, Optional +import uuid + +from fastapi import HTTPException, status +import structlog +from sqlalchemy.orm import Session + +from src.bookmyvenue.repositories.common.repository import commonRepository +from src.bookmyvenue.models.admin import Amenity, Category + + + + +logger = structlog.get_logger() + +class CommonService: + def get_the_categories(self, db:Session) -> List[Category]: + categories_list = commonRepository.fetch_all_the_categories(db) + if not categories_list: + logger.error("the categories table is empty , no category to show") + raise HTTPException(status.HTTP_404_NOT_FOUND ,detail="categories are not found in the database") + return categories_list + + def get_the_amenities(self, db:Session) -> List[Amenity]: + amenity_list = commonRepository.fetch_all_the_amenities(db) + if not amenity_list: + logger.error("the amenity table is empty , no category to show") + raise HTTPException(status.HTTP_404_NOT_FOUND ,detail="amenities are not found in the database") + return amenity_list + + + + + + + +commonService = CommonService() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/ownerServices.py b/Backend/bookmyvenue/src/bookmyvenue/services/ownerServices.py new file mode 100644 index 000000000..b8855daf6 --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/services/ownerServices.py @@ -0,0 +1,84 @@ + +import base64 +from typing import Optional,List +import uuid + +from fastapi import File, HTTPException, UploadFile, status +from pydantic import Json +import structlog +from sqlalchemy.orm import Session + + +from src.bookmyvenue.models.common import Venue +from src.bookmyvenue.schema.common.common import ImageKitVenueUrls, VenueSchema +from src.bookmyvenue.models.owners import Owner +from src.bookmyvenue.models.user import User +from src.bookmyvenue.repositories.users.repository import userRepository +from src.bookmyvenue.repositories.owner.repository import ownerRepository +from src.bookmyvenue.schema.user import user +from src.bookmyvenue.schema.owner import owner +from src.bookmyvenue.BackgroundWorker.Owner.tasks import upload_media_to_imagekit + +logger = structlog.get_logger() + +class OwnerService: + + def get_owner_record(self, db:Session, current_user_id:str) -> Optional[Owner]: + owner_profile = ownerRepository.get_owner_record_by_ID(db, current_user_id) + if not owner_profile: + logger.info(f"Owner Profile Not Found!" , clerk_id=current_user_id) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Owner Profile not found!") + return owner_profile + + + def complete_owner_onboarding(self, db:Session, current_user_id:str, onboard:owner.OwnerOnboardingSchema) -> Owner: + logger.info("trying to onboard owner" , clerk_id = current_user_id) + current_user = userRepository.get_user_by_id(db, current_user_id) + if not current_user: + logger.error("user record not found" ,clerk_id=current_user_id) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="user not found in DB") + owner_record = ownerRepository.get_owner_record_by_ID(db, current_user_id) + if owner_record: + logger.error("owner has already onboarded" ,clerk_id=current_user_id) + raise HTTPException(status_code=status.HTTP_201_CREATED,detail="Owner onboadred completed!") + + owner_record = ownerRepository.onboard_owner(db,current_user,onboard) + + return owner_record + + def create_new_venue(self, db:Session, owner_user:Owner,payload:Json[VenueSchema],cover_image: UploadFile = File(...), gallery: List[UploadFile] = File(...)) -> Venue: + #if a vnue with same name , city and same location exists alreday for the respective owner + + duplicate_checker = ownerRepository.duplicate_venue_checker(db=db, owner=owner_user, city=payload.city, street_address=payload.street_address, name=payload.name) + + venue_instance = ownerRepository.create_venue_record(db=db, owner=owner_user, payload=payload) + + if not venue_instance: + logger.error("cant create the venue for the owner" , ownerId = owner_user.id) + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="cant able to create the venue for the given data" + ) + + cover_bytes = cover_image.file.read() + cover_base64 = base64.b64encode(cover_bytes).decode('utf-8') + + gallery_base64_list = [ + base64.b64encode(file.file.read()).decode('utf-8') for file in gallery + ] + + celery_queue_worker = upload_media_to_imagekit.delay(venue_instance.id, cover_base64, gallery_base64_list, venue_instance.name) + venue_instance.celery_task_ID = celery_queue_worker.id + db.commit() + db.refresh(venue_instance) + + return venue_instance + + + + + + + + +ownerservice = OwnerService() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/services/userServices.py b/Backend/bookmyvenue/src/bookmyvenue/services/userServices.py new file mode 100644 index 000000000..c6dc76dbd --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/services/userServices.py @@ -0,0 +1,75 @@ +import uuid + +from fastapi import HTTPException +import structlog +from sqlalchemy.orm import Session + + +from src.bookmyvenue.models.user import User +from src.bookmyvenue.repositories.users.repository import userRepository +from src.bookmyvenue.schema.user import user + + +logger = structlog.get_logger() + +class UserService: + def register_user(self, db: Session, clerk_user: user.ClerkWebhookData) -> User: + + primary_email = None + for email_obj in clerk_user.email_addresses: + if email_obj.id == clerk_user.primary_email_address_id: + primary_email = email_obj.email_address + break + + # Fallback if primary ID matches nothing, grab the first available email + if not primary_email and clerk_user.email_addresses: + primary_email = clerk_user.email_addresses[0].email_address + + if not primary_email: + logger.error("user have no email address" , clerk_id=clerk_user.id) + raise ValueError(f"Clerk user {clerk_user.id} has no valid email address.") + + # 2. Check if the user already exists locally (idempotency check) + existing_user = userRepository.get_user_by_id(db=db, clerk_id=clerk_user.id) + + if existing_user: + logger.info("user with the given username or email exists" , clerk_id=clerk_user.id, email = primary_email) + raise HTTPException(status_code=409, detail=f"user with same ID exists") + + # 3. Formulate full name + first = clerk_user.first_name or "" + last = clerk_user.last_name or "" + username = clerk_user.username + fullname = f"{first} {last}".strip() or "Clerk User" + + if not username: + username = f"{primary_email.split('@')[0]}_{uuid.uuid4().hex[:4]}" + + new_user = userRepository.create_clerk_user( + db=db, + clerk_id=clerk_user.id, + email=primary_email.lower(), + fullname=fullname, + username=username + ) + + return new_user + + def delete_user(self, db: Session, clerk_user: user.ClerkWebhookData): + existing_user = userRepository.get_user_by_id(db=db, clerk_id=clerk_user.id) + + if not existing_user: + logger.info("user with the given username or email doent exists" , clerk_id=clerk_user.id) + raise HTTPException(status_code=409, detail=f"user with the given ID doesnt exist") + + return userRepository.detele_user(db, existing_user) + + + + def complete_user_onboarding(self, db:Session, current_user_id:str, phone:user.PhoneOnboardingSchema) -> User: + logger.info("trying to onboard user" , clerk_id = current_user_id) + updated_user = userRepository.update_user_onboarding(db=db,clerk_id=current_user_id,phone=phone) + return updated_user + + +userservice = UserService() \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/utils/__init__.py b/Backend/bookmyvenue/src/bookmyvenue/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/__init__.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 000000000..8db39b7ec Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/image_kit.cpython-311.pyc b/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/image_kit.cpython-311.pyc new file mode 100644 index 000000000..a0015f28b Binary files /dev/null and b/Backend/bookmyvenue/src/bookmyvenue/utils/__pycache__/image_kit.cpython-311.pyc differ diff --git a/Backend/bookmyvenue/src/bookmyvenue/utils/image_kit.py b/Backend/bookmyvenue/src/bookmyvenue/utils/image_kit.py new file mode 100644 index 000000000..112bd31ad --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/utils/image_kit.py @@ -0,0 +1,7 @@ +import os +from imagekitio import ImageKit + +#initializing the imageKit instance +imagekit = ImageKit( + private_key=os.getenv("IMAGEKIT_PRIVATE_KEY") +) \ No newline at end of file diff --git a/Backend/bookmyvenue/src/bookmyvenue/worker.py b/Backend/bookmyvenue/src/bookmyvenue/worker.py new file mode 100644 index 000000000..65923377e --- /dev/null +++ b/Backend/bookmyvenue/src/bookmyvenue/worker.py @@ -0,0 +1,24 @@ +import os +from celery import Celery +from src.bookmyvenue.core.config import settings + + +app = Celery( + app_name="bookmyvenue", + broker=os.environ.get("UPSTASH_REDIS_CELERY_URL"), + backend=settings.CELERY_RESULT_BACKEND +) + +#auto-discover is used to look into the tasks.py files present inside the src.bookmyvenue package which may contain many other sub modules. +app.autodiscover_tasks( + packages=['src.bookmyvenue.BackgroundWorker.Owner'] +) + +app.conf.update( + task_track_started=True, + task_serializer="json", + result_serializer="json", + accept_content=["json"], + broker_use_ssl={"ssl_cert_reqs": 0}, + redis_backend_use_ssl={"ssl_cert_reqs": 0}, +) \ No newline at end of file diff --git a/Frontend/bookmyvenue/.dockerignore b/Frontend/bookmyvenue/.dockerignore new file mode 100644 index 000000000..6650cbe8a --- /dev/null +++ b/Frontend/bookmyvenue/.dockerignore @@ -0,0 +1,43 @@ + + +# Next.js build output and cache +.next/ + +# Logs and OS files +npm-debug.log* +.DS_Store + +# Source Control +.git +.github +.gitignore + +# Dependencies (Let Docker install these inside the container) +node_modules/ +bower_components/ +vendor/ + +# Build Outputs +dist/ +build/ +out/ +*.exe +*.dll + +# Environment & Secrets +.env +.env.* +*.pem +*.key +aws_credentials + +# Debug & Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.logs/ +*.swp + +# OS Metadata +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Frontend/bookmyvenue/.gitignore b/Frontend/bookmyvenue/.gitignore new file mode 100644 index 000000000..f5ebfc60f --- /dev/null +++ b/Frontend/bookmyvenue/.gitignore @@ -0,0 +1,37 @@ +# dependencies +.env +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files +.env*.local + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Frontend/bookmyvenue/.prettierignore b/Frontend/bookmyvenue/.prettierignore new file mode 100644 index 000000000..461b00841 --- /dev/null +++ b/Frontend/bookmyvenue/.prettierignore @@ -0,0 +1,7 @@ +dist/ +node_modules/ +.next/ +.turbo/ +coverage/ +pnpm-lock.yaml +.pnpm-store/ \ No newline at end of file diff --git a/Frontend/bookmyvenue/.prettierrc b/Frontend/bookmyvenue/.prettierrc new file mode 100644 index 000000000..a8a2054a1 --- /dev/null +++ b/Frontend/bookmyvenue/.prettierrc @@ -0,0 +1,11 @@ +{ + "endOfLine": "lf", + "semi": false, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindStylesheet": "app/globals.css", + "tailwindFunctions": ["cn", "cva"] +} diff --git a/Frontend/bookmyvenue/README.md b/Frontend/bookmyvenue/README.md new file mode 100644 index 000000000..1e66186df --- /dev/null +++ b/Frontend/bookmyvenue/README.md @@ -0,0 +1,21 @@ +# Next.js template + +This is a Next.js template with shadcn/ui. + +## Adding components + +To add components to your app, run the following command: + +```bash +npx shadcn@latest add button +``` + +This will place the ui components in the `components` directory. + +## Using components + +To use the components in your app, import them as follows: + +```tsx +import { Button } from "@/components/ui/button"; +``` diff --git a/Frontend/bookmyvenue/app/(admin)/add/page.tsx b/Frontend/bookmyvenue/app/(admin)/add/page.tsx new file mode 100644 index 000000000..070353a61 --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/add/page.tsx @@ -0,0 +1,241 @@ +"use client" +import React, { useState } from 'react' +import { Badge } from '@/components/ui/badge' +import { IconCategory, IconHeartPlus, IconIcons } from '@tabler/icons-react' +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + InputGroupText, + InputGroupTextarea, +} from "@/components/ui/input-group" +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' +import { toast } from 'sonner' +import { useAuth } from '@clerk/nextjs' +import axios from 'axios' + + +interface categoriesProp { + name: string + icon_name: string +} + +interface Amenities { + name: string + icon_name: string +} + +function page() { + const [active, setActive] = useState('category') + const [name, setName] = useState("") + const [icon, setIcon] = useState("") + const [categories, setCategories] = useState([]) + const [amenities, setAmenities] = useState([]) + + const { getToken } = useAuth() + const HandleAddCategories = () => { + if (name == "" || icon == "") { + toast.info("add the required details") + } + else { + let cate = { + name: name, + icon_name: icon + } + + setCategories(prevCategories => [...prevCategories, cate]) + setName('') + setIcon('') + } + + } + + const HandleAddAmenities = () => { + if (name == "" || icon == "") { + toast.info("add the required details") + } + else { + let amenity = { + name: name, + icon_name: icon + } + + setAmenities(prevCategories => [...prevCategories, amenity]) + setName('') + setIcon('') + } + + } + + + const HandleCategorySubmisssion = async () => { + const jwtToken = await getToken() + axios.post(`${process.env.NEXT_PUBLIC_DOMAIN}/api/v1/admin/category`, { + category: categories + }, { + headers: { + // Headers are nested under the 'headers' key + authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json' + }, + }) + .then((res) => { + console.log(res) + if (res.data.status_code == 201) { + toast.success("successfully created the categories") + } + + setCategories([]) + }) + .catch((err) => { + console.log(err.response.data) + if (err.response.data.status_code == 400) { + toast.error(err.response.data.message) + } + }) + } + + const HandleAmenitySubmisssion = async () => { + const jwtToken = await getToken() + axios.post(`${process.env.NEXT_PUBLIC_DOMAIN}/api/v1/admin/amenity`, { + amenity: amenities + }, { + headers: { + // Headers are nested under the 'headers' key + authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json' + }, + }) + .then((res) => { + console.log(res) + if (res.data.status_code == 201) { + toast.success("successfully created the amenities") + } + + setCategories([]) + }) + .catch((err) => { + console.log(err.response.data) + if (err.response.data.status_code == 400) { + toast.error(err.response.data.message) + } + }) + } + + return ( +
+
+
+

Admin Control Panel

+

create the categories and amenities that servers the venues

+
+ Admin Panel +
+
+
{ + setName('') + setIcon('') + setActive('category') + }}> + + Add Category +
+
{ + setName('') + setIcon('') + setActive('amenity') + }}> + + Add amenity +
+
+ { + active == 'category' ? ( +
+
+ + { + setName(e.target.value) + }} /> + + + + +
+
+ + { + setIcon(e.target.value) + }} /> + + + + + +
+
+ { + categories.map(category => { + return ( + + {category.name} + + ) + }) + } +
+
+ + +
+
+ ) : ( +
+
+ + { + setName(e.target.value) + }} /> + + + + +
+
+ + { + setIcon(e.target.value) + }} /> + + + + + +
+
+ { + amenities.map(amenity => { + return ( + + {amenity.name} + + ) + }) + } +
+
+ + +
+
+ ) + } + + +
+ ) +} + +export default page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(admin)/allTransactions/page.tsx b/Frontend/bookmyvenue/app/(admin)/allTransactions/page.tsx new file mode 100644 index 000000000..2d7909a68 --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/allTransactions/page.tsx @@ -0,0 +1,174 @@ +import { Badge } from '@/components/ui/badge' +import React from 'react' +import { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { IconBuilding, IconCashBanknoteHeart, IconCashBanknoteMinus, IconCategoryMinus, IconTicket, IconUser } from '@tabler/icons-react' +import { Button } from '@/components/ui/button' +import { Alert } from '@/components/ui/alert' + +const invoices = [ + { + invoice: "INV001", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "$34.00", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV002", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "$34.00", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV003", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "-", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV004", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "$34.00", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV005", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "$34.00", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV006", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "$34.00", + commision: "-", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, + { + invoice: "INV007", + guest: "john Deo", + owner: "ziyau", + type: "Refund", + totalAmount: "$250.00", + refundAmount: "-", + commision: "$5.00", + paymentMethod: "Dodo Payments", + confrormTicket: "kjgg", + }, +] + + +function page() { + return ( +
+
+
+

Transactions Through BMV

+

Track the transactions and revenue generations

+
+ 1202 transactions +
+
+ + A list of your recent invoices. + + + Invoice + +
+ Guest +
+
+ +
+ Owner +
+
+ +
+ Type +
+
+ +
+ Amount Paid +
+
+ +
+ + + Refund Amount +
+
+ +
+ + + Confirmation +
+
+ Commision +
+
+ + {invoices.map((invoice) => ( + + {invoice.invoice} + {invoice.guest} + {invoice.owner} + {invoice.type} + {invoice.totalAmount} + {invoice.refundAmount} + + Confirmation Ticket + + {invoice.commision} + + ))} + + +
+
+
+ ) +} + +export default page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(admin)/allUsers/page.tsx b/Frontend/bookmyvenue/app/(admin)/allUsers/page.tsx new file mode 100644 index 000000000..0674ff493 --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/allUsers/page.tsx @@ -0,0 +1,147 @@ +'use client' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { IconUserCancel, IconUserDown, IconUsers, IconSearch, IconUser, IconBuilding, IconTicket } from '@tabler/icons-react' +import React, { useState } from 'react' +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + InputGroupText, + InputGroupTextarea, +} from "@/components/ui/input-group" +function page() { + const [searchQuery, setSearchQuery] = useState('') + + return ( +
+
+
+ + + + + + +
+
+
+
+

All The Users Signed Through BMV

+ 144 users +
+
+
+ All + Users + Owners +
+ +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+ +
+
+

Ziyaudheen MS

+
BMV Guest
+

since 3 years

+
+
+ + +
+
+
+
+
+ ) +} + +export default page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(admin)/allVenues/page.tsx b/Frontend/bookmyvenue/app/(admin)/allVenues/page.tsx new file mode 100644 index 000000000..b035872b2 --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/allVenues/page.tsx @@ -0,0 +1,112 @@ +'use client' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { IconUserCancel, IconUserDown, IconUsers, IconSearch, IconUser, IconBuilding, IconTicket, IconX, IconStarFilled } from '@tabler/icons-react' +import React, { useState } from 'react' +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, + InputGroupText, + InputGroupTextarea, +} from "@/components/ui/input-group" +import { Filters } from '@/components/filters' +import { Card, CardHeader } from '@/components/ui/card' +import Image from 'next/image' +import { VenueLister } from '@/components/venueLister' +function page() { + const [searchQuery, setSearchQuery] = useState('') + const [rating, setRating] = useState(0) + return ( +
+
+
+ + + + + + + +
+ +
+
+
+

All The Venues Listed

+ 144 venues +
+ +
+
+ All + Approved + Waiting +
+
+ {Array.from({ length: 5 }, (_, index) => { + const starValue = index + 1 + return ( + + ) + })} +
+
+ + +
+
+ ) +} + +export default page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(admin)/bookmyvenue/page.tsx b/Frontend/bookmyvenue/app/(admin)/bookmyvenue/page.tsx new file mode 100644 index 000000000..2f94b583a --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/bookmyvenue/page.tsx @@ -0,0 +1,584 @@ +'use client' + +import React, { useState } from 'react' +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + IconBuilding, + IconCash, + IconClock, + IconCheck, + IconStarFilled, + IconCalendar, + IconTrendingUp, + IconUsers, + IconArrowUpRight, + IconChevronRight, + IconSearch, + IconMapPin, + IconCreditCard, + IconPlus, + IconAlertCircle, + IconHistory, + IconUser, + IconSquareRoundedCheckFilled +} from '@tabler/icons-react' +import { VenueLister } from '@/components/venueLister' +import { UpComing } from '@/components/upComing' +import { Review } from '@/components/review' +import { Cancel } from '@/components/cancel' +import { Reject } from '@/components/reject' + +// --- Mock Data --- + +const MOCK_LISTED_VENUES = [ + { + id: 101, + name: "Al Saj Convention Center (Arena)", + location: "Kazhakkoottam, Trivandrum", + rating: 4.5, + cats: ["Wedding", "Conference", "Exhibition"], + image: "https://www.alsajconventioncenter.com/wp-content/uploads/2023/07/Arena.png", + status: "Active", + bookingsThisMonth: 14, + revenueThisMonth: 12400, + revenueThisYear: 88000, + }, + { + id: 102, + name: "Grand Palace Orchid Gardens", + location: "Nemom, Trivandrum", + rating: 4.2, + cats: ["Birthday", "Workshop", "Meetup"], + image: "https://images.unsplash.com/photo-1519167758481-83f550bb49b3?auto=format&fit=crop&q=80&w=600", + status: "Active", + bookingsThisMonth: 8, + revenueThisMonth: 7200, + revenueThisYear: 45000, + }, + { + id: 103, + name: "Whispering Palms Resort Hall", + location: "Varkala Beach, Trivandrum", + rating: 4.7, + cats: ["Wedding", "Party", "Conference"], + image: "https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?auto=format&fit=crop&q=80&w=600", + status: "Active", + bookingsThisMonth: 12, + revenueThisMonth: 9500, + revenueThisYear: 68000, + }, + { + id: 104, + name: "Lakeside Open Turf & Banquet", + location: "Akkulam, Trivandrum", + rating: 4.6, + cats: ["Sport", "Birthday", "Meetup"], + image: "https://images.unsplash.com/photo-1527529482837-4698179dc6ce?auto=format&fit=crop&q=80&w=600", + status: "Active", + bookingsThisMonth: 18, + revenueThisMonth: 5800, + revenueThisYear: 39000, + } +]; + +const MOCK_PENDING_APPROVAL_VENUES = [ + { + id: 201, + name: "Golden Pavilion Banquet Hall", + location: "Pattom, Trivandrum", + cats: ["Wedding", "Conference"], + submissionDate: "2026-05-28", + status: "Document Verification", + completionPercentage: 65, + notes: "Awaiting verification of the local building license certificate." + }, + { + id: 202, + name: "The Hive Rooftop & Cafe Workspace", + location: "Vazhuthacaud, Trivandrum", + cats: ["Workshop", "Meetup"], + submissionDate: "2026-06-02", + status: "Physical Inspection", + completionPercentage: 35, + notes: "Site inspection scheduled for tomorrow at 11:00 AM." + }, + { + id: 203, + name: "Coral Cove Beachfront Gardens", + location: "Kovalam, Trivandrum", + cats: ["Wedding", "Party"], + submissionDate: "2026-06-03", + status: "Admin Approval Sign-off", + completionPercentage: 90, + notes: "Inspection passed. Final approval from regional lead pending." + } +]; + +// Graph mock data matching filters +const MONTH_DATA = [ + { label: "W1", amount: 6200 }, + { label: "W2", amount: 8400 }, + { label: "W3", amount: 11200 }, + { label: "W4", amount: 9100 }, +]; + +const YEAR_DATA = [ + { label: "Jan", amount: 18000 }, + { label: "Feb", amount: 22000 }, + { label: "Mar", amount: 25000 }, + { label: "Apr", amount: 29000 }, + { label: "May", amount: 34900 }, + { label: "Jun", amount: 41000 }, + { label: "Jul", amount: 38000 }, + { label: "Aug", amount: 45000 }, + { label: "Sep", amount: 49000 }, + { label: "Oct", amount: 56000 }, + { label: "Nov", amount: 52000 }, + { label: "Dec", amount: 68000 }, +]; + +function Page() { + const [timeFilter, setTimeFilter] = useState<'month' | 'year'>('month'); + const [hoveredPoint, setHoveredPoint] = useState<{ index: number; x: number; y: number } | null>(null); + + // Derive stats based on filter + const totalListedVenues = MOCK_LISTED_VENUES.length; + const pendingApprovalsCount = MOCK_PENDING_APPROVAL_VENUES.length; + + const totalMoney = timeFilter === 'month' + ? MOCK_LISTED_VENUES.reduce((acc, curr) => acc + curr.revenueThisMonth, 0) + : MOCK_LISTED_VENUES.reduce((acc, curr) => acc + curr.revenueThisYear, 0); + + const totalBookings = timeFilter === 'month' + ? MOCK_LISTED_VENUES.reduce((acc, curr) => acc + curr.bookingsThisMonth, 0) + : MOCK_LISTED_VENUES.reduce((acc, curr) => acc + (curr.bookingsThisMonth * 10), 0); // Mock year bookings + + const graphData = timeFilter === 'month' ? MONTH_DATA : YEAR_DATA; + + // SVG Chart Config + const chartHeight = 220; + const chartWidth = 720; + const paddingX = 40; + const paddingY = 30; + + const maxVal = Math.max(...graphData.map(d => d.amount)) * 1.1; // 10% headroom + const minVal = 0; + + // Calculate coordinates + const points = graphData.map((d, index) => { + const x = paddingX + (index / (graphData.length - 1)) * (chartWidth - paddingX * 2); + const y = chartHeight - paddingY - ((d.amount - minVal) / (maxVal - minVal)) * (chartHeight - paddingY * 2); + return { x, y, label: d.label, amount: d.amount }; + }); + + // SVG path definitions + const linePath = points.length > 0 + ? points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ') + : ''; + + const areaPath = points.length > 0 + ? `${linePath} L ${points[points.length - 1].x} ${chartHeight - paddingY} L ${points[0].x} ${chartHeight - paddingY} Z` + : ''; + + return ( +
+ + {/* Header and Filter bar */} +
+
+

+ Admin Panel +

+

+ Manage your venues, monitor bookings, trace the financial trajectory and feel the beat of BMV +

+
+ +
+ + {/* Stats Cards Section */} +
+ {/* Total Money Card */} + +
+ + No Of Venues Listed +
+ +
+
+ +
+ ${totalMoney.toLocaleString('en-US', { minimumFractionDigits: 0 })} +
+
+ + +12.4% + + + vs last {timeFilter === 'month' ? 'month' : 'year'} + +
+
+
+ + {/* Listed Venues Card */} + +
+ + Users Registered Through BMV +
+ +
+
+ +
+ {totalListedVenues} +
+

+ 34 owners and 290 guests. +

+
+
+ + {/* Bookings Card */} + +
+ + Transactions Through BMV +
+ +
+
+ +
+ {totalBookings} +
+
+ + 5% + + + commision fees + +
+
+
+ + {/* Pending Approvals Card */} + +
+ + {/* Trajectory Graph & Approval Queue section */} +
+ + {/* Trajectory Graph Container (2 Cols) */} +
+ + +
+
+ Transaction Trajectory + Visualizing revenue flows and scaling trends +
+ + Active Flow + +
+
+ + + {/* Custom SVG Line Chart */} +
+ + + {/* Shadow / Area gradient */} + + + + + + + {/* Horizontal grid lines */} + {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => { + const y = paddingY + ratio * (chartHeight - paddingY * 2); + const val = maxVal - ratio * (maxVal - minVal); + return ( + + + + ${Math.round(val).toLocaleString()} + + + ); + })} + + {/* Area fill */} + {points.length > 0 && ( + + )} + + {/* Line stroke */} + {points.length > 0 && ( + + )} + + {/* Axis labels & interactive markers */} + {points.map((p, index) => { + const isHovered = hoveredPoint?.index === index; + return ( + + {/* Vertical highlight line on hover */} + {isHovered && ( + + )} + + {/* X Axis labels */} + + {p.label} + + + {/* Data point circle */} + + + ); + })} + + {/* Invisible broad hover columns for smooth interactivity */} + {points.map((p, index) => { + const widthPerCol = (chartWidth - paddingX * 2) / (points.length - 1 || 1); + const rectX = p.x - widthPerCol / 2; + return ( + setHoveredPoint({ index, x: p.x, y: p.y })} + onMouseLeave={() => setHoveredPoint(null)} + /> + ); + })} + +
+ + {/* Floating Tooltip details */} +
+ {hoveredPoint ? ( +
+ {points[hoveredPoint.index].label}: + ${points[hoveredPoint.index].amount.toLocaleString()} +
+ ) : ( + + Hover over data points to inspect detailed value logs + + )} +
+ +
+
+
+ + {/* Approval Queue Section (1 Col) */} + +
+ + Revenue Made By BMV +
+ +
+
+ +
+ {totalBookings} +
+
+ + 5% + + + commision fees + +
+
+
+ +
+ + {/* Existing Listed Venues Section */} +
+
+
+

+ Venues Waiting Approval +

+

Go through the venues , approve them and earn through it.

+
+ + {totalListedVenues} Total + +
+ +
+
+
+
+
+
+ +
+
+ +
+

Al Saj Arena, nemom, tvm, kerala

+

ziya123@gmail.com

+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+

Al Saj Arena, nemom, tvm, kerala

+

ziya123@gmail.com

+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+

Al Saj Arena, nemom, tvm, kerala

+

ziya123@gmail.com

+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+

Al Saj Arena, nemom, tvm, kerala

+

ziya123@gmail.com

+ +
+
+
+
+ + +
+
+
+
+
+ +
+
+ +
+

Al Saj Arena, nemom, tvm, kerala

+

ziya123@gmail.com

+ +
+
+
+
+ + +
+
+
+ + +
+
+
+
+ ); +} + +export default Page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(admin)/layout.tsx b/Frontend/bookmyvenue/app/(admin)/layout.tsx new file mode 100644 index 000000000..db9882857 --- /dev/null +++ b/Frontend/bookmyvenue/app/(admin)/layout.tsx @@ -0,0 +1,37 @@ +import { Geist, Geist_Mono, Figtree, Roboto } from "next/font/google" + +import "../globals.css" +import { ThemeProvider } from "@/components/theme-provider" +import { cn } from "@/lib/utils"; +import { Footer } from "@/components/Footer"; +import { Navbar } from "@/components/ui/navbar"; + +const robotoHeading = Roboto({subsets:['latin'],variable:'--font-heading'}); + +const figtree = Figtree({subsets:['latin'],variable:'--font-sans'}) + +const fontMono = Geist_Mono({ + subsets: ["latin"], + variable: "--font-mono", +}) + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + + {children} + + + + ) +} diff --git a/Frontend/bookmyvenue/app/(auth)/complete-onboarding/page.tsx b/Frontend/bookmyvenue/app/(auth)/complete-onboarding/page.tsx new file mode 100644 index 000000000..f208e0a06 --- /dev/null +++ b/Frontend/bookmyvenue/app/(auth)/complete-onboarding/page.tsx @@ -0,0 +1,91 @@ +"use client" +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Navbar } from '@/components/ui/navbar' +import { IconCompassFilled, IconDashboard, IconPhone } from '@tabler/icons-react' +import { useAuth } from '@clerk/nextjs' +import React, { useState } from 'react' +import axios from 'axios' +import { toast } from 'sonner' +import { Spinner } from '@/components/ui/spinner' +import { useRouter } from 'next/navigation' + +function page() { + + const router = useRouter() + const [phone, setPhone] = useState('') + const { getToken } = useAuth() + const [loading ,setLoading] =useState(false) + const HandleUserOnboarding = async () => { + setLoading(true) + console.log("uploading.........") + const jwtToken = await getToken() + axios + .post(`${process.env.NEXT_PUBLIC_DOMAIN}/api/v1/user/onboarding`, { + phone: phone, + }, { + headers: { + // Headers are nested under the 'headers' key + authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json' + }, + }) + .then((res) => { + console.log(res) + if (res.data.status_code == 200) { + toast.success("completed the owner onboarding...") + router.push('/') + } + if (res.data.status_code == 201) { + toast.success("you have onboarded already!") + router.push('/') + } + }) + .catch((err) => { + console.log(err.response.data) //contains the details of the error + if (err.response && err.response.status === 404) { + toast.info(err.response.message) + } + if (err.response && err.response.status === 403) { + // toast.info(err.response.data) + } + }) + .finally(() => { + setLoading(false) + }) + + } + + + + return ( +
+ +

Complete Your Onboadring

+

Experience the smoothness we BMV offers!

+
+
+

Phone Number

+ setPhone(e.target.value)} /> +

We collect your numbers just for the onboarding process and will not use it for any personal data leakage problems and will only share it with the venued where you have booked

+
+
+ +
+ +
+
+ ) +} + +export default page \ No newline at end of file diff --git a/Frontend/bookmyvenue/app/(auth)/owner-onboarding/page.tsx b/Frontend/bookmyvenue/app/(auth)/owner-onboarding/page.tsx new file mode 100644 index 000000000..e322f52d7 --- /dev/null +++ b/Frontend/bookmyvenue/app/(auth)/owner-onboarding/page.tsx @@ -0,0 +1,166 @@ +"use client" +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { IconBriefcase, IconBuilding, IconCompassFilled, IconHeart, IconSpeakerphone } from '@tabler/icons-react' +import React, { useEffect, useState } from 'react' +import { useAuth } from '@clerk/nextjs' +import axios from 'axios' +import { toast } from 'sonner' +import { Spinner } from "@/components/ui/spinner" +import { useRouter } from 'next/navigation' +function page() { + const router = useRouter() + const [org, setOrg] = useState("") + const [profession, setProfession] = useState("") + const [promise, setPromise] = useState("") + const [info, setInfo] = useState("") + const { getToken } = useAuth() + const [loading, setLoading] = useState(false) + const [checking, setChecking] = useState(false) + + + const checkOwnerStaus = async () => { + setChecking(true) + const jwtToken = await getToken() + axios + .get(`${process.env.NEXT_PUBLIC_DOMAIN}/api/v1/owner/check`, { + headers: { + // Headers are nested under the 'headers' key + authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json' + }, + }) + .then((res) => { + console.log(res) + if (res.data.status_code == 200) { + toast.success("Owner has onboarded!...") + router.push('/dashboard') + } + if (res.data.status_code == 201) { + toast.success("you have onboarded already!") + router.push('/dashboard') + } + }) + .catch((err) => { + console.log(err) + if (err.response && err.response.status_code === 404) { + toast.info(err.response.message) + } + }) + .finally(() => { + setChecking(false) + }) + } + + + const handleOnBoarding = async () => { + setLoading(true) + console.log("uploading.........") + const jwtToken = await getToken() + axios + .post(`${process.env.NEXT_PUBLIC_DOMAIN}/api/v1/owner/onboarding`, { + organization: org, + profession: profession, + promise: promise, + self_info: info + }, { + headers: { + // Headers are nested under the 'headers' key + authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json' + }, + }) + .then((res) => { + console.log(res) + if (res.data.status_code == 200) { + toast.success("completed the owner onboarding...") + router.push('/dashboard') + } + if (res.data.status_code == 201) { + toast.success("you have onboarded already!") + router.push('/dashboard') + } + }) + .catch((err) => { + console.log(err) + if (err.response && err.response.status_code === 404) { + toast.info(err.response.message) + router.push('/') + } + }) + .finally(() => { + setLoading(false) + }) + + } + + + useEffect(() => { + checkOwnerStaus() + }, []) + + + if (checking) { + return ( +
+
+ +

Checking Owner Profile

+

Please wait while we verify your owner status...

+
+
+ ) + } + + + return ( + <> +
+
+

Become An Owner

+

Get Your Venues Listed , Start Earning As a Owner

+
+
+

Name Of The Organization

+ setOrg(e.target.value)} /> +
+
+

Your Profession

+ setProfession(e.target.value)} /> +
+
+

Your Guarenty For Guests

+ setPromise(e.target.value)} /> +

Lets guests find it secure , express what your organization can do for them and will ensure them

+
+
+

Share Who You Are

+ +
+
+
+ + + + + + + + + ) +} + +export { Offline } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/payment.tsx b/Frontend/bookmyvenue/components/payment.tsx new file mode 100644 index 000000000..2564f6594 --- /dev/null +++ b/Frontend/bookmyvenue/components/payment.tsx @@ -0,0 +1,51 @@ +import { IconCash } from '@tabler/icons-react' +import React from 'react' +import { Button } from './ui/button' +import Image from 'next/image' + +function Payment() { + return ( +
+
+
+

Payment Details

+

Detailed calculation of what we charge you and what you pay for

+
+
+
Rent For One Hour :
+

$10.00

+
+
+
Platform Charge :
+

$3.00

+
+
+
GST :
+

$4.00

+
+
+
Total Rent (4 hours):
+

$40.00

+
+
+
Total Price:
+

$57.00

+
+
+
+
+
+ Payment +

Pay It With 100% Security!

+

BMV have a secured payment gateway, pay it, experience the moment

+
+
+
+
+ +
+
+ ) +} + +export { Payment } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/priceManager.tsx b/Frontend/bookmyvenue/components/priceManager.tsx new file mode 100644 index 000000000..ef22b2796 --- /dev/null +++ b/Frontend/bookmyvenue/components/priceManager.tsx @@ -0,0 +1,130 @@ +import React from 'react' +import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group' +import { IconBriefcase, IconCash, IconPlus, IconX } from '@tabler/icons-react' +import { Button } from './ui/button' +import { Badge } from './ui/badge' +import { Seasonal } from './seasonal' +import { Weekly } from './weekly' + +function PriceManager() { + return ( +
+
+

Standerd rent

+ + + + + + +
+
+
+

Seasonal Pay

+ +
+
+
+

Onam

+ 22-05-2026 to 05-06-2026 +
+
+ + + + + + + +
+
+
+
+

Onam

+ 22-05-2026 to 05-06-2026 +
+
+ + + + + + + +
+
+
+
+

Onam

+ 22-05-2026 to 05-06-2026 +
+
+ + + + + + + +
+
+
+
+
+

Weekly Pay

+ +
+
+
+

Saturday

+ +
+
+ + + + + + + +
+
+
+
+

Sunday

+ +
+
+ + + + + + + +
+
+
+ +
+ ) +} + +export { PriceManager } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/reject.tsx b/Frontend/bookmyvenue/components/reject.tsx new file mode 100644 index 000000000..76685cecf --- /dev/null +++ b/Frontend/bookmyvenue/components/reject.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +import { IconCash, IconCross, IconLocationCheck, IconWriting, IconX } from '@tabler/icons-react' +import { Input } from './ui/input' +import { PaymentCancellation } from './PaymentCancellation' +function Reject() { + return ( + +
+ + + + + + This Venue Has Rejected + + + Share Why This venue failed to showcase. + +
+
+
What made as to take this call :
+ +
+
+ + + + + + + +
+ +
+
+ ) +} + +export {Reject} \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/revenueGraph.tsx b/Frontend/bookmyvenue/components/revenueGraph.tsx new file mode 100644 index 000000000..41066d182 --- /dev/null +++ b/Frontend/bookmyvenue/components/revenueGraph.tsx @@ -0,0 +1,227 @@ +"use client" + +import * as React from "react" +import { CartesianGrid, Line, LineChart, XAxis } from "recharts" + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart" + +export const description = "An interactive line chart" + +const chartData = [ + { date: "2024-04-01", desktop: 222, mobile: 150 }, + { date: "2024-04-02", desktop: 97, mobile: 180 }, + { date: "2024-04-03", desktop: 167, mobile: 120 }, + { date: "2024-04-04", desktop: 242, mobile: 260 }, + { date: "2024-04-05", desktop: 373, mobile: 290 }, + { date: "2024-04-06", desktop: 301, mobile: 340 }, + { date: "2024-04-07", desktop: 245, mobile: 180 }, + { date: "2024-04-08", desktop: 409, mobile: 320 }, + { date: "2024-04-09", desktop: 59, mobile: 110 }, + { date: "2024-04-10", desktop: 261, mobile: 190 }, + { date: "2024-04-11", desktop: 327, mobile: 350 }, + { date: "2024-04-12", desktop: 292, mobile: 210 }, + { date: "2024-04-13", desktop: 342, mobile: 380 }, + { date: "2024-04-14", desktop: 137, mobile: 220 }, + { date: "2024-04-15", desktop: 120, mobile: 170 }, + { date: "2024-04-16", desktop: 138, mobile: 190 }, + { date: "2024-04-17", desktop: 446, mobile: 360 }, + { date: "2024-04-18", desktop: 364, mobile: 410 }, + { date: "2024-04-19", desktop: 243, mobile: 180 }, + { date: "2024-04-20", desktop: 89, mobile: 150 }, + { date: "2024-04-21", desktop: 137, mobile: 200 }, + { date: "2024-04-22", desktop: 224, mobile: 170 }, + { date: "2024-04-23", desktop: 138, mobile: 230 }, + { date: "2024-04-24", desktop: 387, mobile: 290 }, + { date: "2024-04-25", desktop: 215, mobile: 250 }, + { date: "2024-04-26", desktop: 75, mobile: 130 }, + { date: "2024-04-27", desktop: 383, mobile: 420 }, + { date: "2024-04-28", desktop: 122, mobile: 180 }, + { date: "2024-04-29", desktop: 315, mobile: 240 }, + { date: "2024-04-30", desktop: 454, mobile: 380 }, + { date: "2024-05-01", desktop: 165, mobile: 220 }, + { date: "2024-05-02", desktop: 293, mobile: 310 }, + { date: "2024-05-03", desktop: 247, mobile: 190 }, + { date: "2024-05-04", desktop: 385, mobile: 420 }, + { date: "2024-05-05", desktop: 481, mobile: 390 }, + { date: "2024-05-06", desktop: 498, mobile: 520 }, + { date: "2024-05-07", desktop: 388, mobile: 300 }, + { date: "2024-05-08", desktop: 149, mobile: 210 }, + { date: "2024-05-09", desktop: 227, mobile: 180 }, + { date: "2024-05-10", desktop: 293, mobile: 330 }, + { date: "2024-05-11", desktop: 335, mobile: 270 }, + { date: "2024-05-12", desktop: 197, mobile: 240 }, + { date: "2024-05-13", desktop: 197, mobile: 160 }, + { date: "2024-05-14", desktop: 448, mobile: 490 }, + { date: "2024-05-15", desktop: 473, mobile: 380 }, + { date: "2024-05-16", desktop: 338, mobile: 400 }, + { date: "2024-05-17", desktop: 499, mobile: 420 }, + { date: "2024-05-18", desktop: 315, mobile: 350 }, + { date: "2024-05-19", desktop: 235, mobile: 180 }, + { date: "2024-05-20", desktop: 177, mobile: 230 }, + { date: "2024-05-21", desktop: 82, mobile: 140 }, + { date: "2024-05-22", desktop: 81, mobile: 120 }, + { date: "2024-05-23", desktop: 252, mobile: 290 }, + { date: "2024-05-24", desktop: 294, mobile: 220 }, + { date: "2024-05-25", desktop: 201, mobile: 250 }, + { date: "2024-05-26", desktop: 213, mobile: 170 }, + { date: "2024-05-27", desktop: 420, mobile: 460 }, + { date: "2024-05-28", desktop: 233, mobile: 190 }, + { date: "2024-05-29", desktop: 78, mobile: 130 }, + { date: "2024-05-30", desktop: 340, mobile: 280 }, + { date: "2024-05-31", desktop: 178, mobile: 230 }, + { date: "2024-06-01", desktop: 178, mobile: 200 }, + { date: "2024-06-02", desktop: 470, mobile: 410 }, + { date: "2024-06-03", desktop: 103, mobile: 160 }, + { date: "2024-06-04", desktop: 439, mobile: 380 }, + { date: "2024-06-05", desktop: 88, mobile: 140 }, + { date: "2024-06-06", desktop: 294, mobile: 250 }, + { date: "2024-06-07", desktop: 323, mobile: 370 }, + { date: "2024-06-08", desktop: 385, mobile: 320 }, + { date: "2024-06-09", desktop: 438, mobile: 480 }, + { date: "2024-06-10", desktop: 155, mobile: 200 }, + { date: "2024-06-11", desktop: 92, mobile: 150 }, + { date: "2024-06-12", desktop: 492, mobile: 420 }, + { date: "2024-06-13", desktop: 81, mobile: 130 }, + { date: "2024-06-14", desktop: 426, mobile: 380 }, + { date: "2024-06-15", desktop: 307, mobile: 350 }, + { date: "2024-06-16", desktop: 371, mobile: 310 }, + { date: "2024-06-17", desktop: 475, mobile: 520 }, + { date: "2024-06-18", desktop: 107, mobile: 170 }, + { date: "2024-06-19", desktop: 341, mobile: 290 }, + { date: "2024-06-20", desktop: 408, mobile: 450 }, + { date: "2024-06-21", desktop: 169, mobile: 210 }, + { date: "2024-06-22", desktop: 317, mobile: 270 }, + { date: "2024-06-23", desktop: 480, mobile: 530 }, + { date: "2024-06-24", desktop: 132, mobile: 180 }, + { date: "2024-06-25", desktop: 141, mobile: 190 }, + { date: "2024-06-26", desktop: 434, mobile: 380 }, + { date: "2024-06-27", desktop: 448, mobile: 490 }, + { date: "2024-06-28", desktop: 149, mobile: 200 }, + { date: "2024-06-29", desktop: 103, mobile: 160 }, + { date: "2024-06-30", desktop: 446, mobile: 400 }, +] + +const chartConfig = { + views: { + label: "Page Views", + }, + desktop: { + label: "Desktop", + color: "var(--chart-1)", + }, + mobile: { + label: "Mobile", + color: "var(--chart-2)", + }, +} satisfies ChartConfig + +export function RevenueGraph() { + const [activeChart, setActiveChart] = + React.useState("desktop") + + const total = React.useMemo( + () => ({ + desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0), + mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0), + }), + [] + ) + + return ( + + +
+ Line Chart - Interactive + + Showing total visitors for the last 3 months + +
+
+ {["desktop", "mobile"].map((key) => { + const chart = key as keyof typeof chartConfig + return ( + + ) + })} +
+
+ + + + + { + const date = new Date(value) + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + }} + /> + { + return new Date(value).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) + }} + /> + } + /> + + + + +
+ ) +} diff --git a/Frontend/bookmyvenue/components/revenueStats.tsx b/Frontend/bookmyvenue/components/revenueStats.tsx new file mode 100644 index 000000000..d44c51c02 --- /dev/null +++ b/Frontend/bookmyvenue/components/revenueStats.tsx @@ -0,0 +1,70 @@ +import { IconAnalyze } from '@tabler/icons-react' +import React from 'react' +import { RevenueGraph } from './revenueGraph' + +function RevenueStats() { + const activityData = [ + { month: 'Jan', value: 40 }, + { month: 'Mar', value: 72 }, + { month: 'May', value: 60 }, + { month: 'Jul', value: 84 }, + { month: 'Sep', value: 68 }, + { month: 'Nov', value: 92 }, + ] + const maxActivityValue = Math.max(...activityData.map((item) => item.value)) + const graphPoints = activityData + .map((item, index) => { + const x = 50 + index * 90 + const y = 120 - (item.value / maxActivityValue) * 100 + return `${x},${y}` + }) + .join(' ') + + return ( +
+
+

+ Transaction History +

+

Monitor your venue's revenue and transactions

+
+
+
+

This Month

+

$28.4K

+

Revenue this month

+
+
+

This Year

+

$196.8K

+

Revenue this year

+
+
+

Total Bookings

+

412

+

Bookings completed

+
+
+

Repeat Guests

+

78%

+

Returning customers

+
+
+ +
+
+
+

Booking activity

+

Transactions by month

+
+
Latest update: 2 hours ago
+
+ + +
+
+ + ) +} + +export { RevenueStats } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/review.tsx b/Frontend/bookmyvenue/components/review.tsx new file mode 100644 index 000000000..3227a407d --- /dev/null +++ b/Frontend/bookmyvenue/components/review.tsx @@ -0,0 +1,69 @@ +'use client' +import React, { useState } from 'react' +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { IconLocationCheck, IconStarFilled, IconWriting, IconX } from '@tabler/icons-react' +import { Input } from './ui/input' + +function Review() { + const [rating , setRating] = useState(0) + return ( + +
+ + + + + + Drop your review here + + + Please share your experience with us. + +
+
Rate your experience:
+
+ {Array.from({ length: 5 }, (_, index) => { + const starValue = index + 1 + return ( + + ) + })} +
+ +
+ +
+
+ + + + + + + +
+ +
+
+ ) +} + +export {Review} \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/seasonal.tsx b/Frontend/bookmyvenue/components/seasonal.tsx new file mode 100644 index 000000000..320171ef0 --- /dev/null +++ b/Frontend/bookmyvenue/components/seasonal.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { IconCalendarCheck, IconClock, IconPlus, IconUpload, IconX } from '@tabler/icons-react' +import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group' +import { GroupDatePicker } from './groupDatePicker' + +function Seasonal() { + return ( + +
+ + + + + + + Add Seasonal Pay + + + Set special pricing and boost your revenue. + +
+
+
+

Seasonal Package Name

+ {/* 22-05-2026 to 05-06-2026 */} +
+ + + + + + +
+ +
+
+

Select the Dates

+ {/* 22-05-2026 to 05-06-2026 */} +
+ + +
+ + + + + + + +
+ + + + + + + +
+
+
+ ) +} + +export { Seasonal } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/theme-provider.tsx b/Frontend/bookmyvenue/components/theme-provider.tsx new file mode 100644 index 000000000..3d44f4815 --- /dev/null +++ b/Frontend/bookmyvenue/components/theme-provider.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes" + +function ThemeProvider({ + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + ) +} + +function isTypingTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false + } + + return ( + target.isContentEditable || + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.tagName === "SELECT" + ) +} + +function ThemeHotkey() { + const { resolvedTheme, setTheme } = useTheme() + + React.useEffect(() => { + function onKeyDown(event: KeyboardEvent) { + if (event.defaultPrevented || event.repeat) { + return + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return + } + + if (event.key.toLowerCase() !== "d") { + return + } + + if (isTypingTarget(event.target)) { + return + } + + setTheme(resolvedTheme === "dark" ? "light" : "dark") + } + + window.addEventListener("keydown", onKeyDown) + + return () => { + window.removeEventListener("keydown", onKeyDown) + } + }, [resolvedTheme, setTheme]) + + return null +} + +export { ThemeProvider } diff --git a/Frontend/bookmyvenue/components/topRatedVenues.tsx b/Frontend/bookmyvenue/components/topRatedVenues.tsx new file mode 100644 index 000000000..b59ead33c --- /dev/null +++ b/Frontend/bookmyvenue/components/topRatedVenues.tsx @@ -0,0 +1,57 @@ +import React from 'react' +import { Card, CardContent } from "@/components/ui/card" +import { + Carousel, + CarouselContent, + CarouselItem, +} from "@/components/ui/carousel" +import Image from 'next/image' + +const CarousalData = [ + { + id: 1, + imgURl: "/1.png", + }, + { + id: 2, + imgURl: "/2.png", + }, + { + id: 3, + imgURl: "/3.png", + }, +] + +interface TopRatedVenuesProps { + CarousalData: string[] +} + +function TopRatedVenues({ CarousalData }: TopRatedVenuesProps) { + return ( + + + {CarousalData.map((venue, index) => ( + +
+ + + Venue + + +
+
+ ))} +
+
+ { + CarousalData.map((venue ,index) => ( +
+
+ )) + } +
+
+ ) +} + +export { TopRatedVenues } \ No newline at end of file diff --git a/Frontend/bookmyvenue/components/ui/alert.tsx b/Frontend/bookmyvenue/components/ui/alert.tsx new file mode 100644 index 000000000..1fe3176d2 --- /dev/null +++ b/Frontend/bookmyvenue/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/Frontend/bookmyvenue/components/ui/badge.tsx b/Frontend/bookmyvenue/components/ui/badge.tsx new file mode 100644 index 000000000..cacff11dc --- /dev/null +++ b/Frontend/bookmyvenue/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/Frontend/bookmyvenue/components/ui/button.tsx b/Frontend/bookmyvenue/components/ui/button.tsx new file mode 100644 index 000000000..75b8c3df3 --- /dev/null +++ b/Frontend/bookmyvenue/components/ui/button.tsx @@ -0,0 +1,67 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/Frontend/bookmyvenue/components/ui/calendar.tsx b/Frontend/bookmyvenue/components/ui/calendar.tsx new file mode 100644 index 000000000..2e820e2b6 --- /dev/null +++ b/Frontend/bookmyvenue/components/ui/calendar.tsx @@ -0,0 +1,222 @@ +"use client" + +import * as React from "react" +import { + DayPicker, + getDefaultClassNames, + type DayButton, + type Locale, +} from "react-day-picker" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" +import { IconChevronLeft, IconChevronRight, IconChevronDown } from "@tabler/icons-react" + +function Calendar({ + className, + classNames, + showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + locale, + formatters, + components, + ...props +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"] +}) { + const defaultClassNames = getDefaultClassNames() + + return ( + svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className + )} + captionLayout={captionLayout} + locale={locale} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString(locale?.code, { month: "short" }), + ...formatters, + }} + classNames={{ + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_previous + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", + defaultClassNames.button_next + ), + month_caption: cn( + "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", + defaultClassNames.month_caption + ), + dropdowns: cn( + "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns + ), + dropdown_root: cn( + "relative rounded-(--cell-radius)", + defaultClassNames.dropdown_root + ), + dropdown: cn( + "absolute inset-0 bg-popover opacity-0", + defaultClassNames.dropdown + ), + caption_label: cn( + "font-medium select-none", + captionLayout === "label" + ? "text-sm" + : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", + defaultClassNames.caption_label + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none", + defaultClassNames.weekday + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-(--cell-size) select-none", + defaultClassNames.week_number_header + ), + week_number: cn( + "text-[0.8rem] text-muted-foreground select-none", + defaultClassNames.week_number + ), + day: cn( + "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", + props.showWeekNumber + ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" + : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", + defaultClassNames.day + ), + range_start: cn( + "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted", + defaultClassNames.range_start + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn( + "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted", + defaultClassNames.range_end + ), + today: cn( + "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none", + defaultClassNames.today + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled + ), + hidden: cn("invisible", defaultClassNames.hidden), + ...classNames, + }} + components={{ + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ) + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ) + } + + if (orientation === "right") { + return ( + + ) + } + + return ( + + ) + }, + DayButton: ({ ...props }) => ( + + ), + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ) + }, + ...components, + }} + {...props} + /> + ) +} + +function CalendarDayButton({ + className, + day, + modifiers, + locale, + ...props +}: React.ComponentProps & { locale?: Partial }) { + const defaultClassNames = getDefaultClassNames() + + const ref = React.useRef(null) + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus() + }, [modifiers.focused]) + + return ( + + ) +} + +function CarouselNext({ + className, + variant = "outline", + size = "icon-sm", + ...props +}: React.ComponentProps) { + const { orientation, scrollNext, canScrollNext } = useCarousel() + + return ( + + ) +} + +export { + type CarouselApi, + Carousel, + CarouselContent, + CarouselItem, + useCarousel, +} diff --git a/Frontend/bookmyvenue/components/ui/chart.tsx b/Frontend/bookmyvenue/components/ui/chart.tsx new file mode 100644 index 000000000..7c2dc8472 --- /dev/null +++ b/Frontend/bookmyvenue/components/ui/chart.tsx @@ -0,0 +1,373 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import type { TooltipValueType } from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +> + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + initialDimension?: { + width: number + height: number + } +}) { + const uniqueId = React.useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +