diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..1c2099dc4 Binary files /dev/null and b/.DS_Store differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..385f27a27 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} diff --git a/backend-bmv/.DS_Store b/backend-bmv/.DS_Store new file mode 100644 index 000000000..479dfbdb8 Binary files /dev/null and b/backend-bmv/.DS_Store differ diff --git a/backend-bmv/bookmyvenue/.gitattributes b/backend-bmv/bookmyvenue/.gitattributes new file mode 100644 index 000000000..3b41682ac --- /dev/null +++ b/backend-bmv/bookmyvenue/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/backend-bmv/bookmyvenue/.gitignore b/backend-bmv/bookmyvenue/.gitignore new file mode 100644 index 000000000..667aaef0c --- /dev/null +++ b/backend-bmv/bookmyvenue/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/backend-bmv/bookmyvenue/.mvn/wrapper/maven-wrapper.properties b/backend-bmv/bookmyvenue/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..52913720f --- /dev/null +++ b/backend-bmv/bookmyvenue/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip diff --git a/backend-bmv/bookmyvenue/mvnw b/backend-bmv/bookmyvenue/mvnw new file mode 100755 index 000000000..bd8896bf2 --- /dev/null +++ b/backend-bmv/bookmyvenue/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/backend-bmv/bookmyvenue/mvnw.cmd b/backend-bmv/bookmyvenue/mvnw.cmd new file mode 100644 index 000000000..92450f932 --- /dev/null +++ b/backend-bmv/bookmyvenue/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/backend-bmv/bookmyvenue/pom.xml b/backend-bmv/bookmyvenue/pom.xml new file mode 100644 index 000000000..8020bf814 --- /dev/null +++ b/backend-bmv/bookmyvenue/pom.xml @@ -0,0 +1,171 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.6 + + + com.bookmyvenue + bookmyvenue + 0.0.1-SNAPSHOT + bookmyvenue + + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webmvc + + + com.meilisearch.sdk + meilisearch-java + 0.20.1 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.stripe + stripe-java + 25.1.0 + + + com.bucket4j + bucket4j-core + 8.7.0 + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-data-jpa-test + test + + + org.springframework.boot + spring-boot-starter-security-test + test + + + org.springframework.boot + spring-boot-starter-validation-test + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + + compile + + + + + org.projectlombok + lombok + + + + + + default-testCompile + test-compile + + testCompile + + + + + org.projectlombok + lombok + + + + + + + + + + diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/.DS_Store b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/.DS_Store new file mode 100644 index 000000000..6132d8612 Binary files /dev/null and b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/.DS_Store differ diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/BookmyvenueApplication.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/BookmyvenueApplication.java new file mode 100644 index 000000000..145e9b8f9 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/BookmyvenueApplication.java @@ -0,0 +1,17 @@ +package com.bookmyvenue; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +@EnableSpringDataWebSupport +public class BookmyvenueApplication { + + public static void main(String[] args) { + SpringApplication.run(BookmyvenueApplication.class, args); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchConfig.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchConfig.java new file mode 100644 index 000000000..b50f56d93 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchConfig.java @@ -0,0 +1,24 @@ +package com.bookmyvenue.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.meilisearch.sdk.Client; +import com.meilisearch.sdk.Config; + +@Configuration +public class MeilisearchConfig { + + @Value("${meilisearch.host}") + private String host; + + @Value("${meilisearch.api-key}") + private String apiKey; + + @Bean + public Client meiliSearchClient(){ + return new Client(new Config(host, apiKey)); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchIndexInitializer.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchIndexInitializer.java new file mode 100644 index 000000000..60cf88b1d --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/config/MeilisearchIndexInitializer.java @@ -0,0 +1,21 @@ +package com.bookmyvenue.config; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; + +import com.bookmyvenue.service.MeilisearchService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MeilisearchIndexInitializer { + private final MeilisearchService meilisearchService; + + public void run(ApplicationArguments args){ + log.info("Configuring Meilisearch index"); + meilisearchService.configureIndex(); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AdminController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AdminController.java new file mode 100644 index 000000000..f863120dc --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AdminController.java @@ -0,0 +1,81 @@ +package com.bookmyvenue.controller; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.UserResponse; +import com.bookmyvenue.dto.UserStatusRequest; +import com.bookmyvenue.dto.VenueResponse; +import com.bookmyvenue.dto.VenueReviewRequest; +import com.bookmyvenue.repository.VenueRepository; +import com.bookmyvenue.service.AdminService; + +import lombok.RequiredArgsConstructor; + + +@RestController +@RequestMapping("api/admin") +@RequiredArgsConstructor +public class AdminController { + private final AdminService adminService; + // private final MeilisearchService meilisearchService; + public final VenueRepository venueRepository; + + @GetMapping("/users") + public ResponseEntity> getAllUsers(@PageableDefault(size = 10, sort ="createdAt", direction= Sort.Direction.DESC)Pageable pageable){ + return ResponseEntity.ok(adminService.getAllUsers(pageable)); + } + + @GetMapping("/venue/review") + public ResponseEntity> getAllPendingReviews(){ + return ResponseEntity.ok(adminService.getAllPendingReviews()); + } + + @PatchMapping("/users/{id}/status") + public ResponseEntity updateUserStatus(@PathVariable Integer id, @RequestBody UserStatusRequest request){ + return ResponseEntity.ok(adminService.updateUserStatus(id, request)); + } + + @PatchMapping("/venue/{id}/review") + public ResponseEntity reviewVenue(@PathVariable Integer id, @RequestBody VenueReviewRequest request){ + return ResponseEntity.ok(adminService.reviewVenue(id, request)); + } + + @GetMapping("/bookings") + public ResponseEntity> getAllBookings(@PageableDefault(size = 10, sort ="bookedOn", direction= Sort.Direction.DESC)Pageable pageable){ + return ResponseEntity.ok(adminService.getAllBookings(pageable)); + } + + @PatchMapping("/booking/{id}/status") + public ResponseEntity updateBookingStatus(@PathVariable Integer id, @RequestParam String bookingStatus){ + return ResponseEntity.ok(adminService.updateBookingStatus(id, bookingStatus)); + } + + @PatchMapping("/booking/{id}/payment/status") + public ResponseEntity updatePaymentStatus(@PathVariable Integer id, @RequestParam String paymentStatus){ + return ResponseEntity.ok(adminService.updatePaymentStatus(id, paymentStatus)); + } + + @GetMapping("/venues") + public ResponseEntity> getAllVenues(@PageableDefault(size = 10, sort ="createdAt", direction= Sort.Direction.DESC)Pageable pageable){ + return ResponseEntity.ok(adminService.getAllVenues(pageable)); + } + + @PatchMapping("/venue/{id}/status") + public ResponseEntity updateVenueStatus(@PathVariable Integer id, @RequestParam String status){ + return ResponseEntity.ok(adminService.updateVenueStatus(id, status)); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AuthController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AuthController.java new file mode 100644 index 000000000..51cacf2ae --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/AuthController.java @@ -0,0 +1,33 @@ +package com.bookmyvenue.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.AuthResponse; +import com.bookmyvenue.dto.LoginRequest; +import com.bookmyvenue.dto.RegisterRequest; +import com.bookmyvenue.service.AuthService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("api/auth") +@RequiredArgsConstructor +public class AuthController { + private final AuthService authService; + + @PostMapping("/register") + public ResponseEntity register (@Valid @RequestBody RegisterRequest request){ + return ResponseEntity.ok(authService.register(request)); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + return ResponseEntity.ok(authService.login(request)); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/CancellationController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/CancellationController.java new file mode 100644 index 000000000..d1345541b --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/CancellationController.java @@ -0,0 +1,62 @@ +package com.bookmyvenue.controller; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.CancelRequest; +import com.bookmyvenue.dto.CancelReviewRequest; +import com.bookmyvenue.dto.CancellationResponse; +import com.bookmyvenue.service.CancellationService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class CancellationController { + + private final CancellationService cancellationService; + + @PostMapping("/api/user/bookings/{bookingId}/cancel") + public ResponseEntity requestCancellation( + @PathVariable Integer bookingId, + @RequestBody CancelRequest request, + @AuthenticationPrincipal UserDetails userDetails) { + + return ResponseEntity.ok( + cancellationService.requestCancellation(bookingId, request, userDetails.getUsername()) + ); + } + + // OWNER: get all cancel requests for their venues + @GetMapping("/api/owner/cancellations") + public ResponseEntity> getCancelRequests( + @AuthenticationPrincipal UserDetails userDetails, @PageableDefault(size = 10, sort ="createdOn", direction= Sort.Direction.DESC)Pageable pageable) { + + return ResponseEntity.ok( + cancellationService.getCancelRequestsForOwner(userDetails.getUsername(),pageable) + ); + } + + // OWNER: approve or reject a cancel request + @PatchMapping("/api/owner/cancellations/{id}/review") + public ResponseEntity reviewCancellation( + @PathVariable Integer id, + @RequestBody CancelReviewRequest request, + @AuthenticationPrincipal UserDetails userDetails) { + + return ResponseEntity.ok( + cancellationService.reviewCancellation(id, request, userDetails.getUsername()) + ); + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/OwnerController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/OwnerController.java new file mode 100644 index 000000000..187d65e8c --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/OwnerController.java @@ -0,0 +1,73 @@ +package com.bookmyvenue.controller; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.BookingReviewRequest; +import com.bookmyvenue.dto.VenueRequest; +import com.bookmyvenue.dto.VenueResponse; +import com.bookmyvenue.dto.VenueUpdateRequest; +import com.bookmyvenue.service.OwnerService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("api/owner") +@RequiredArgsConstructor +public class OwnerController { + private final OwnerService ownerService; + + @PostMapping("/venue/register") + public ResponseEntity createVenue (@RequestBody VenueRequest request, @AuthenticationPrincipal UserDetails userDetails) + { + + VenueResponse response = ownerService.createVenue(request,userDetails.getUsername()); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/venue/owner") + public ResponseEntity> getMyVenues(@AuthenticationPrincipal UserDetails userDetails){ + return ResponseEntity.ok(ownerService.getMyVenues(userDetails.getUsername())); + } + + @PutMapping("/update/venue/{id}") + public ResponseEntity updateVenue(@PathVariable Integer id, @RequestBody VenueUpdateRequest request, @AuthenticationPrincipal UserDetails userDetails){ + System.out.println("update test"); + VenueResponse response = ownerService.updateVenue(id , request, userDetails.getUsername()); + return ResponseEntity.ok(response); + } + + @DeleteMapping("delete/venue/{id}") + public ResponseEntitydeleteVenue(@PathVariable Integer id, @AuthenticationPrincipal UserDetails userDetails){ + ownerService.deleteVenue(id, userDetails.getUsername()); + return ResponseEntity.ok("Venue Deleted successfully"); + } + + @GetMapping("/bookings/reviews") + public ResponseEntity> getBookingsReviews(@AuthenticationPrincipal UserDetails userDetails,@PageableDefault(size = 10, sort ="bookedOn", direction= Sort.Direction.DESC)Pageable pageable){ + return ResponseEntity.ok(ownerService.getBookingsReviews(userDetails.getUsername(),pageable)); + } + + @PatchMapping("/bookings/{id}/review") + public ResponseEntity reviewBooking(@PathVariable Integer id, @RequestBody BookingReviewRequest request){ + return ResponseEntity.ok(ownerService.reviewBooking(id, request)); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/PaymentController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/PaymentController.java new file mode 100644 index 000000000..c80678fd3 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/PaymentController.java @@ -0,0 +1,45 @@ +package com.bookmyvenue.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.PaymentIntentResponse; +import com.bookmyvenue.service.PaymentService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/payments") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentService paymentService; + + @PostMapping("/create-intent/{bookingId}") + public ResponseEntity createIntent( + @PathVariable Integer bookingId, + @AuthenticationPrincipal UserDetails userDetails) { + try { + return ResponseEntity.ok(paymentService.createPaymentIntent(bookingId)); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + @PostMapping("/confirm") + public ResponseEntity confirmPayment( + @RequestParam Integer bookingId, + @RequestParam String paymentIntentId) { + try { + return ResponseEntity.ok(paymentService.confirmPayment(bookingId, paymentIntentId)); + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/UserController.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/UserController.java new file mode 100644 index 000000000..979f2d24e --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/controller/UserController.java @@ -0,0 +1,64 @@ +package com.bookmyvenue.controller; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.bookmyvenue.dto.BookingRequest; +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.VenueResponse; +import com.bookmyvenue.dto.VenueSearchDocument; +import com.bookmyvenue.service.MeilisearchService; +import com.bookmyvenue.service.UserService; + +import lombok.RequiredArgsConstructor; + + +@RestController +@RequestMapping("api/user") +@RequiredArgsConstructor +public class UserController { + private final UserService userService; + private final MeilisearchService meilisearchService; + + @GetMapping("/venues") + public ResponseEntity> getApprovedVenues(){ + return ResponseEntity.ok(userService.getApprovedVenues()); + } + + @PostMapping("/booking") + public ResponseEntity createBooking(@RequestBody BookingRequest request, @AuthenticationPrincipal UserDetails userDetails){ + BookingResponse response = userService.createBooking(request, userDetails.getUsername()); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @GetMapping("/bookings/my") + public ResponseEntity> getMyBookings(@AuthenticationPrincipal UserDetails userDetails, @PageableDefault(size = 10, sort ="bookedOn", direction= Sort.Direction.DESC)Pageable pageable){ + return ResponseEntity.ok(userService.getMyBookings(userDetails.getUsername(),pageable)); + } + + @GetMapping("/venues/search") + public ResponseEntity> searchVenues( + @RequestParam(required = false) String q, + @RequestParam(required = false) String venueType, + @RequestParam(required = false) Integer minPrice, + @RequestParam(required = false) Integer maxPrice) { + + return ResponseEntity.ok( + meilisearchService.searchVenues(q, venueType, minPrice, maxPrice) + ); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/AuthResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/AuthResponse.java new file mode 100644 index 000000000..b2ac66226 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/AuthResponse.java @@ -0,0 +1,13 @@ +package com.bookmyvenue.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AuthResponse { + private String token; + private String name; + private String email; + private String role; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingRequest.java new file mode 100644 index 000000000..a9d909201 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingRequest.java @@ -0,0 +1,11 @@ +package com.bookmyvenue.dto; + +import java.time.LocalDate; + +import lombok.Data; + +@Data +public class BookingRequest { + private Integer venueId; + private LocalDate bookingDate; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingResponse.java new file mode 100644 index 000000000..c6c8453f5 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingResponse.java @@ -0,0 +1,45 @@ +package com.bookmyvenue.dto; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import com.bookmyvenue.model.Booking; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class BookingResponse { + private Integer id; + private Integer venueId; + private String venueName; + private Integer price; + private String venueLocation; + private LocalDate bookingDate; + private String bookingStatus; + private String paymentStatus; + private String ownerComments; + private LocalDateTime reviewedOn; + private LocalDateTime bookedOn; + private String customerName; + private String customerEmail; + + public static BookingResponse from(Booking booking) { + return BookingResponse.builder() + .id(booking.getId()) + .venueId(booking.getVenue().getId()) + .venueName(booking.getVenue().getVenueName()) + .venueLocation(booking.getVenue().getLocation()) + .bookingDate(booking.getBookingDate()) + .bookingStatus(booking.getBookingStatus().name()) + .paymentStatus(booking.getPaymentStatus().name()) + .ownerComments(booking.getOwnerComments()) + .reviewedOn(booking.getReviewedOn()) + .bookedOn(booking.getBookedOn()) + .customerName(booking.getUser().getName()) + .customerEmail(booking.getUser().getEmail()) + .price(booking.getVenue().getPrice()) + .build(); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingReviewRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingReviewRequest.java new file mode 100644 index 000000000..0982d27c0 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/BookingReviewRequest.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class BookingReviewRequest { +private String bookingStatus; + private String ownerComments; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelRequest.java new file mode 100644 index 000000000..b93c2a8eb --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelRequest.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class CancelRequest { + private String reason; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelReviewRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelReviewRequest.java new file mode 100644 index 000000000..12683b874 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancelReviewRequest.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class CancelReviewRequest { + private String status; + private String ownerResponse; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancellationResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancellationResponse.java new file mode 100644 index 000000000..ce137c2f3 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/CancellationResponse.java @@ -0,0 +1,48 @@ +package com.bookmyvenue.dto; + +import java.time.LocalDateTime; + +import com.bookmyvenue.model.BookingCancellation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CancellationResponse { + private Integer id; + private Integer bookingId; + private String venueName; + private String venueLocation; + private String bookingDate; + private String reason; + private String status; + private String ownerResponse; + private String cancelledBy; + private LocalDateTime createdOn; + private LocalDateTime reviewedOn; + private String bookingStatus; + private String paymentStatus; + + public static CancellationResponse from(BookingCancellation bc) { + return CancellationResponse.builder() + .id(bc.getId()) + .bookingId(bc.getBooking().getId()) + .venueName(bc.getBooking().getVenue().getVenueName()) + .venueLocation(bc.getBooking().getVenue().getLocation()) + .bookingDate(bc.getBooking().getBookingDate().toString()) + .reason(bc.getReason()) + .status(bc.getStatus().name()) + .ownerResponse(bc.getOwnerResponse()) + .cancelledBy(bc.getCancelledBy().name()) + .createdOn(bc.getCreatedOn()) + .reviewedOn(bc.getReviewedOn()) + .bookingStatus(bc.getBooking().getBookingStatus().name()) + .paymentStatus(bc.getBooking().getPaymentStatus().name()) + .build(); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/LoginRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/LoginRequest.java new file mode 100644 index 000000000..e0e23a861 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/LoginRequest.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String email; + private String password; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentRequest.java new file mode 100644 index 000000000..4907bf85b --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentRequest.java @@ -0,0 +1,10 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class PaymentIntentRequest { + private Integer bookingId; + private Long amount; + private String currency; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentResponse.java new file mode 100644 index 000000000..09c1c8424 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/PaymentIntentResponse.java @@ -0,0 +1,16 @@ +package com.bookmyvenue.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class PaymentIntentResponse { + private String clientSecret; + private String publishableKey; + private Long amount; + private String currency; + private Integer bookingId; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/RegisterRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/RegisterRequest.java new file mode 100644 index 000000000..858e19c91 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/RegisterRequest.java @@ -0,0 +1,27 @@ +package com.bookmyvenue.dto; + +import com.bookmyvenue.model.User; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class RegisterRequest { + + @NotBlank(message = "Name is required") + private String name; + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 8, message = "Password must contain at least 8 characters") + private String password; + + private User.Role role; + + private String location; +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserResponse.java new file mode 100644 index 000000000..a990b6ee4 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserResponse.java @@ -0,0 +1,33 @@ +package com.bookmyvenue.dto; + +import java.time.LocalDateTime; + +import com.bookmyvenue.model.User; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserResponse { + + private Integer id; + private String name; + private String email; + private String role; + private String location; + private LocalDateTime createdAt; + private Boolean active; + + public static UserResponse from(User user) { + return UserResponse.builder() + .id(user.getId()) + .name(user.getName()) + .email(user.getEmail()) + .role(user.getRole().name()) + .location(user.getLocation()) + .createdAt(user.getCreatedAt()) + .active(user.getActive()) + .build(); + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserStatusRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserStatusRequest.java new file mode 100644 index 000000000..8e0eea9de --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/UserStatusRequest.java @@ -0,0 +1,8 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class UserStatusRequest { + private Boolean active; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueRequest.java new file mode 100644 index 000000000..736b9e38c --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueRequest.java @@ -0,0 +1,29 @@ +package com.bookmyvenue.dto; +import com.bookmyvenue.model.Venues; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class VenueRequest { + + @NotBlank(message = "Venue Name is required") + private String venueName; + + private Venues.VenueType venueType; + + @NotBlank(message = "Location is required") + private String location; + private String venueDescription; + + @NotBlank(message = "Capacity is required") + private Integer capacity; + + @NotBlank(message = "Price is required") + private Integer price; + + @NotBlank(message = "Parking availablity is required") + private Venues.ParkingAvailability parkingAvailable; + private String imageUrl; + private Boolean termsAccepted; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueResponse.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueResponse.java new file mode 100644 index 000000000..acb7fe906 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueResponse.java @@ -0,0 +1,54 @@ +package com.bookmyvenue.dto; + +import java.time.LocalDateTime; + +import com.bookmyvenue.model.Venues; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class VenueResponse { + private Integer id; + private String venueName; + private String venueType; + private String location; + private String venueDescription; + private Integer capacity; + private Integer price; + private String parkingAvailable; + private String imageUrl; + private Boolean termsAccepted; + private String status; + private String ownerName; + private String ownerEmail; + private LocalDateTime createdAt; + private LocalDateTime approvedOn; + private String approverMessage; + + public static VenueResponse from(Venues venue) { + return VenueResponse.builder() + .id(venue.getId()) + .venueName(venue.getVenueName()) + .venueType(venue.getVenueType().name()) + .location(venue.getLocation()) + .venueDescription(venue.getVenueDescription()) + .capacity(venue.getCapacity()) + .price(venue.getPrice()) + .parkingAvailable(venue.getParkingAvailable().name()) + .imageUrl(venue.getImageUrl()) + .termsAccepted(venue.getTermsAccepted()) + .status(venue.getStatus().name()) + .ownerName(venue.getUser().getName()) + .ownerEmail(venue.getUser().getEmail()) + .createdAt(venue.getCreatedAt()) + .approvedOn(venue.getApprovedOn()) + .approverMessage(venue.getApproverMessage()) + .build(); + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueReviewRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueReviewRequest.java new file mode 100644 index 000000000..d5b2daf3d --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueReviewRequest.java @@ -0,0 +1,9 @@ +package com.bookmyvenue.dto; + +import lombok.Data; + +@Data +public class VenueReviewRequest { + private String status; + private String approverMessage; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueSearchDocument.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueSearchDocument.java new file mode 100644 index 000000000..dac90cdfa --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueSearchDocument.java @@ -0,0 +1,38 @@ +package com.bookmyvenue.dto; + +import com.bookmyvenue.model.Venues; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VenueSearchDocument { + private Integer id; + private String venueName; + private String venueType; + private String location; + private String venueDescription; + private Integer capacity; + private Integer price; + private String parkingAvailable; + private String imageUrl; + + public static VenueSearchDocument from(Venues venue) { + return VenueSearchDocument.builder() + .id(venue.getId()) + .venueName(venue.getVenueName()) + .venueType(venue.getVenueType().toString()) + .location(venue.getLocation()) + .venueDescription(venue.getVenueDescription()) + .capacity(venue.getCapacity()) + .price(venue.getPrice()) + .parkingAvailable(venue.getParkingAvailable().name()) + .imageUrl(venue.getImageUrl()) + .build(); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueUpdateRequest.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueUpdateRequest.java new file mode 100644 index 000000000..0b0123ce6 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/dto/VenueUpdateRequest.java @@ -0,0 +1,17 @@ +package com.bookmyvenue.dto; + +import com.bookmyvenue.model.Venues; + +import lombok.Data; + +@Data +public class VenueUpdateRequest { + private String venueName; + private Venues.VenueType venueType; + private String location; + private String venueDescription; + private Integer capacity; + private Integer price; + private Venues.ParkingAvailability parkingAvailable; + private String imageUrl; +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/GlobalExceptionHandler.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..b9d9cd8c3 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/GlobalExceptionHandler.java @@ -0,0 +1,63 @@ +package com.bookmyvenue.exception; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + + +@RestControllerAdvice +public class GlobalExceptionHandler { + // Validation errors + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation( + MethodArgumentNotValidException ex) { + + Map errors = new HashMap<>(); + + ex.getBindingResult().getFieldErrors() + .forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + } + + // Duplicate email + @ExceptionHandler(UserAlreadyExistsException.class) + public ResponseEntity> handleUserExists( + UserAlreadyExistsException ex) { + + Map error = new HashMap<>(); + error.put("message", ex.getMessage()); + + return new ResponseEntity<>(error, HttpStatus.CONFLICT); + } + + // inactive user + @ExceptionHandler(UserDeactivatedException.class) + public ResponseEntity handleInactiveUser(UserDeactivatedException ex) { + + return new ResponseEntity<>("Your account has been deactivated. please contact the administrator", HttpStatus.FORBIDDEN); + } + @ExceptionHandler(VenueAlredyBookedException.class) + public ResponseEntity handleInactiveUser(VenueAlredyBookedException ex) { + + return new ResponseEntity<>("This venue is alredy booked on this date .please select an another date.", HttpStatus.CONFLICT); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentials(BadCredentialsException ex) { + return new ResponseEntity<>("Invalid email or password", HttpStatus.UNAUTHORIZED); + } + + // Other exceptions + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneral(Exception ex) { + return new ResponseEntity<>("Something went wrong"+ex, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserAlreadyExistsException.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserAlreadyExistsException.java new file mode 100644 index 000000000..d21f42919 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.bookmyvenue.exception; + +public class UserAlreadyExistsException extends RuntimeException{ + public UserAlreadyExistsException(String message) { + super(message); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserDeactivatedException.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserDeactivatedException.java new file mode 100644 index 000000000..70fdc3a49 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/UserDeactivatedException.java @@ -0,0 +1,7 @@ +package com.bookmyvenue.exception; + +public class UserDeactivatedException extends RuntimeException{ + public UserDeactivatedException() { + super(); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/VenueAlredyBookedException.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/VenueAlredyBookedException.java new file mode 100644 index 000000000..71431ea9d --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/exception/VenueAlredyBookedException.java @@ -0,0 +1,7 @@ +package com.bookmyvenue.exception; + +public class VenueAlredyBookedException extends RuntimeException { + public VenueAlredyBookedException(String message) { + super(message); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Booking.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Booking.java new file mode 100644 index 000000000..1170344ca --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Booking.java @@ -0,0 +1,75 @@ +package com.bookmyvenue.model; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Entity +@Table(name = "bookings") +@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +public class Booking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + // Many bookings can belong to one user + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + // Many bookings can belong to one venue + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "venue_id", nullable = false) + private Venues venue; + + @Column(name = "booking_date", nullable = false) + private LocalDate bookingDate; + + @Enumerated(EnumType.STRING) + @Column(name = "booking_status", nullable = false) + private BookingStatus bookingStatus = BookingStatus.PENDING; + + @Enumerated(EnumType.STRING) + @Column(name = "payment_status", nullable = false) + private PaymentStatus paymentStatus = PaymentStatus.UNPAID; + + @Column(name = "owner_comments", length = 500) + private String ownerComments; + + @Column(name = "reviewed_on") + private LocalDateTime reviewedOn; + + @Column(name = "booked_on", updatable = false) + private LocalDateTime bookedOn; + + @Column(name = "payment_on") + private LocalDateTime paymentOn; + + // Version column — JPA uses this automatically for optimistic locking + // If two requests try to update the same booking at the same time, + // the second one will fail with OptimisticLockException instead of + // silently overwriting the first + @Version + @Column(nullable = false) + private Integer version = 0; + + @Column(name= "payment_intent_id", length=100) + private String paymentIntentId; + + @PrePersist + protected void onCreate() { + this.bookedOn = LocalDateTime.now(); + this.bookingStatus = BookingStatus.PENDING; + this.paymentStatus = PaymentStatus.UNPAID; + } + + public enum BookingStatus { + PENDING, APPROVED, REJECTED, CANCELLED + } + + public enum PaymentStatus { + UNPAID, PAID, REFUNDED + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/BookingCancellation.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/BookingCancellation.java new file mode 100644 index 000000000..e114b0d49 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/BookingCancellation.java @@ -0,0 +1,67 @@ +package com.bookmyvenue.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "booking_cancellations") +@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder +public class BookingCancellation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "booking_id", nullable = false, unique = true) + private Booking booking; + + @Enumerated(EnumType.STRING) + @Column(name = "cancelled_by", nullable = false) + private CancelledBy cancelledBy = CancelledBy.USER; + + @Column(nullable = false, length = 500) + private String reason; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private CancellationStatus status = CancellationStatus.PENDING; + + @Column(name = "owner_response", length = 500) + private String ownerResponse; + + @Column(name = "created_on", updatable = false) + private LocalDateTime createdOn; + + @Column(name = "reviewed_on") + private LocalDateTime reviewedOn; + + @PrePersist + protected void onCreate() { + this.createdOn = LocalDateTime.now(); + } + + public enum CancelledBy { + USER, OWNER, ADMIN + } + + public enum CancellationStatus { + PENDING, APPROVED, REJECTED + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/User.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/User.java new file mode 100644 index 000000000..258ee6321 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/User.java @@ -0,0 +1,64 @@ +package com.bookmyvenue.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(nullable = false, length = 100) + private String name; + + @Column(nullable = false, unique = true, length = 150) + private String email; + + @Column(nullable = false, length = 255) + private String password; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "ENUM('user','owner','admin') DEFAULT 'user'") + private Role role; + + @Column(length = 150) + private String location; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "active") + private Boolean active; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + if (this.role == null) this.role = Role.user; + if (this.active == null) this.active = true; + } + + public enum Role { + user, owner, admin + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Venues.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Venues.java new file mode 100644 index 000000000..d06bad1ef --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/model/Venues.java @@ -0,0 +1,112 @@ +package com.bookmyvenue.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "venues") +public class Venues { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @Column(name = "venue_name", nullable = false, length = 255) + private String venueName; + + @Enumerated(EnumType.STRING) + @Column(name = "venue_type", nullable = false) + private VenueType venueType; + + @Column(nullable = false, length = 255) + private String location; + + @Column(name = "venue_description", length = 1000) + private String venueDescription; + + @Column(nullable = false) + private Integer capacity; + + @Column(nullable = false) + private Integer price; + + @Enumerated(EnumType.STRING) + @Column(name = "parking_available", nullable = false) + private ParkingAvailability parkingAvailable; + + @Column(name = "image_url", length = 500) + private String imageUrl; + + @Column(name = "terms_accepted", nullable = false) + private Boolean termsAccepted; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private VenueStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "approved_on") + private LocalDateTime approvedOn; + + @Column(name = "approver_message", length=500) + private String approverMessage; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + + if (this.status == null) { + this.status = VenueStatus.PENDING; + } + + if (this.termsAccepted == null) { + this.termsAccepted = false; + } + } + + public enum VenueType { + BANQUET_HALL, + OUTDOOR_GARDEN, + CONFERENCE_ROOM, + WEDDING_RECEPTION_HALL + } + + public enum ParkingAvailability { + YES, + NO + } + + public enum VenueStatus { + PENDING, + APPROVED, + REJECTED + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingCancellationRepository.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingCancellationRepository.java new file mode 100644 index 000000000..aec328800 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingCancellationRepository.java @@ -0,0 +1,26 @@ +package com.bookmyvenue.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import com.bookmyvenue.model.BookingCancellation; + +public interface BookingCancellationRepository extends JpaRepository { + Optional findByBookingId(Integer bookingId); + + // All cancel requests for venues owned by a specific owner + @Query("SELECT bc FROM BookingCancellation bc " + + "WHERE bc.booking.venue.user.id = :ownerId") + Page findByVenueOwnerId(Integer ownerId, Pageable pageable); + + // All pending cancel requests for owner + @Query("SELECT bc FROM BookingCancellation bc " + + "WHERE bc.booking.venue.user.id = :ownerId " + + "AND bc.status = 'PENDING'") + List findPendingByVenueOwnerId(Integer ownerId); +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingRepository.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingRepository.java new file mode 100644 index 000000000..fa4032e1a --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/BookingRepository.java @@ -0,0 +1,25 @@ +package com.bookmyvenue.repository; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.model.Booking.BookingStatus; + +public interface BookingRepository extends JpaRepository { + Page findByUserId(Integer userId, Pageable pageable); + List findByVenueId(Integer venueId); + List findByBookingStatus(BookingStatus status); + boolean existsByVenueIdAndBookingDateAndBookingStatusIn(Integer venueId, LocalDate bookingDate, List statuses); + Page findByVenueUserId(Integer userId, Pageable pageable); + + @Query("SELECT b FROM Booking b WHERE b.bookingStatus = 'PENDING' AND b.bookedOn < :cutoffTime") + List findPendingBookingsOlderThan(@Param("cutoffTime")LocalDateTime cutoffTime); +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/UserRepository.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/UserRepository.java new file mode 100644 index 000000000..da1d24cc1 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.bookmyvenue.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bookmyvenue.model.User; + +public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/VenueRepository.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/VenueRepository.java new file mode 100644 index 000000000..33c36df6b --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/repository/VenueRepository.java @@ -0,0 +1,14 @@ +package com.bookmyvenue.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.bookmyvenue.model.Venues; +import com.bookmyvenue.model.Venues.VenueStatus; + +public interface VenueRepository extends JpaRepository{ + List findByUserId(Integer ownerId); + List findByStatus(VenueStatus status); + List findByUserIdAndStatus(Integer ownerId, VenueStatus status); +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/scheduler/BookingScheduler.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/scheduler/BookingScheduler.java new file mode 100644 index 000000000..d06387ed5 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/scheduler/BookingScheduler.java @@ -0,0 +1,50 @@ +package com.bookmyvenue.scheduler; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.repository.BookingRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class BookingScheduler { + private final BookingRepository bookingRepository; + + @Scheduled(cron= "0 0 0 * * *") + @Transactional + public void autoRejectExpiredBooking(){ + //testing 2 min + // LocalDateTime cutoffTime = LocalDateTime.now().minusMinutes(2); + LocalDateTime cutoffTime = LocalDateTime.now().minusDays(2); + log.info("Running auto-reject job. cutoff time ; {}",cutoffTime); + + List expiredBookings = bookingRepository.findPendingBookingsOlderThan(cutoffTime); + + if(expiredBookings.isEmpty()){ + log.info("No exiped bookings found."); + return; + } + + log.info("Found {} expired pending bookings. Auto-rejecting....",expiredBookings.size()); + + for(Booking booking : expiredBookings){ + booking.setBookingStatus(Booking.BookingStatus.REJECTED); + booking.setOwnerComments("Auto Rejected: the venue owner did not respond with in 2 days."+"Please try booking again or choose a differnt venue."); + booking.setReviewedOn(LocalDateTime.now()); + bookingRepository.save(booking); + + log.info("Auto-rejected booking is={} for venue={} on date={}",booking.getId(),booking.getVenue().getVenueName(),booking.getBookingDate()); + + } + log.info("Auto-reject job completed.{} bookings rejected",expiredBookings.size()); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/CustomUserDetailsService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/CustomUserDetailsService.java new file mode 100644 index 000000000..765948834 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/CustomUserDetailsService.java @@ -0,0 +1,37 @@ +package com.bookmyvenue.security; + +import java.util.List; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import com.bookmyvenue.model.User; +import com.bookmyvenue.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + return new org.springframework.security.core.userdetails.User( + user.getEmail(), + user.getPassword(), + List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name().toUpperCase())) + ); + } + + public UserRepository getUserRepository() { + return userRepository; + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtAuthFilter.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtAuthFilter.java new file mode 100644 index 000000000..0baab402e --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtAuthFilter.java @@ -0,0 +1,49 @@ +package com.bookmyvenue.security; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String authHeader = request.getHeader("Authorization"); + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + + if (jwtUtil.isTokenValid(token)) { + String email = jwtUtil.extractEmail(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + + filterChain.doFilter(request, response); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtUtil.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtUtil.java new file mode 100644 index 000000000..7dfd4bcf5 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/JwtUtil.java @@ -0,0 +1,61 @@ +package com.bookmyvenue.security; + +import java.util.Date; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtUtil { + + @Value("${app.jwt.secret}") + private String secret; + + @Value("${app.jwt.expiration}") + private long expirationMs; + + private SecretKey getSigningKey(){ + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String generateToken(String email, String role) { + return Jwts.builder() + .subject(email) + .claim("role", role) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + expirationMs)) + .signWith(getSigningKey()) + .compact(); + } + public boolean isTokenValid(String token) { + try { + parseClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + private Claims parseClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String extractEmail(String token) { + return parseClaims(token).getSubject(); + } + + public String extractRole(String token) { + return parseClaims(token).get("role", String.class); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/RateLimitFilter.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/RateLimitFilter.java new file mode 100644 index 000000000..ce5616505 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/RateLimitFilter.java @@ -0,0 +1,76 @@ +package com.bookmyvenue.security; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class RateLimitFilter extends OncePerRequestFilter { + + //cuncurrent hashmap is thread safe for concurrent requests + private final Map authBuckets = new ConcurrentHashMap<>(); + private final Map generalBuckets = new ConcurrentHashMap<>(); + + private Bucket createAuthBucket() { + Bandwidth limit = Bandwidth.builder() + .capacity(5) + .refillIntervally(5, Duration.ofMinutes(1)) + .build(); + return Bucket.builder().addLimit(limit).build(); + } + + private Bucket createGeneralBucket(){ + Bandwidth limit = Bandwidth.builder() + .capacity(60) + .refillIntervally(60, Duration.ofMinutes(1)) + .build(); + return Bucket.builder().addLimit(limit).build(); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String ip = getClientIp(request); + String path = request.getRequestURI(); + + Bucket bucket; + if(path.startsWith("/api/auth/")){ + bucket = authBuckets.computeIfAbsent(ip, k->createAuthBucket()); + }else{ + bucket = generalBuckets.computeIfAbsent(ip, k -> createGeneralBucket()); + } + if(bucket.tryConsume(1)){ + filterChain.doFilter(request, response); + }else{ + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write("{\"error\":\"Too many requests. Please wait before trying again.\","+"\"status\":429}"); + } + // throw new UnsupportedOperationException("Not supported yet."); + } + + private String getClientIp(HttpServletRequest request){ + String xForwardedFor = request.getHeader("X-Forwarded-For"); + if(xForwardedFor != null && !xForwardedFor.isEmpty()){ + return xForwardedFor.split(",")[0].trim(); + } + String xRealIp = request.getHeader("X-Real_IP"); + if(xRealIp != null && !xRealIp.isEmpty()){ + return xRealIp; + } + return request.getRemoteAddr(); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/SecurityConfig.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/SecurityConfig.java new file mode 100644 index 000000000..49a1c6733 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/security/SecurityConfig.java @@ -0,0 +1,70 @@ +package com.bookmyvenue.security; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + private final RateLimitFilter rateLimitFilter; + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**","/api/owner/**","/api/admin/**","/api/user/**","/api/payments/**").permitAll() + //.requestMatchers(HttpMethod.DELETE, "/api/owner/**").hasRole("OWNER") + //.requestMatchers("/api/owner/**").authenticated() + .anyRequest().authenticated() + ) + .addFilterBefore(rateLimitFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) + throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:5173", "http://localhost:3000")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "Cache-Control")); + configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AdminService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AdminService.java new file mode 100644 index 000000000..711f05db7 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AdminService.java @@ -0,0 +1,113 @@ +package com.bookmyvenue.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.UserResponse; +import com.bookmyvenue.dto.UserStatusRequest; +import com.bookmyvenue.dto.VenueResponse; +import com.bookmyvenue.dto.VenueReviewRequest; +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.model.User; +import com.bookmyvenue.model.Venues; +import com.bookmyvenue.repository.BookingRepository; +import com.bookmyvenue.repository.UserRepository; +import com.bookmyvenue.repository.VenueRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminService { + private final MeilisearchService meilisearchService; + private final UserRepository userRepository; + private final VenueRepository venueRepository; + private final BookingRepository bookingRepository; + + + public Page getAllUsers(Pageable pageable){ + return userRepository.findAll(pageable).map(UserResponse::from); + } + + public List getAllPendingReviews(){ + return venueRepository.findByStatus(Venues.VenueStatus.PENDING).stream().map(VenueResponse::from).collect(Collectors.toList()); + } + + public UserResponse updateUserStatus(Integer userId, UserStatusRequest request){ + User user = userRepository.findById(userId).orElseThrow(()-> new RuntimeException("User not found")); + + user.setActive(request.getActive()); + userRepository.save(user); + + return UserResponse.from(user); + } + + public VenueResponse reviewVenue(Integer venueId, VenueReviewRequest request){ + + Venues venue = venueRepository.findById(venueId).orElseThrow(()-> new RuntimeException("Venue Not Found:" + venueId)); + + if(venue.getStatus() != Venues.VenueStatus.PENDING){ + throw new RuntimeException("Venue is alredy "+venue.getStatus()); + } + + venue.setStatus(Venues.VenueStatus.valueOf(request.getStatus())); + + venue.setApproverMessage(request.getApproverMessage()); + + venue.setApprovedOn(LocalDateTime.now()); + + venueRepository.save(venue); + + if("APPROVED".equals((request.getStatus()))){ + meilisearchService.indexVenue(venue); + } + if("REMOVED".equals((request.getStatus()))){ + meilisearchService.removeVenue(venue.getId()); + } + return VenueResponse.from(venue); + } + + public Page getAllBookings(Pageable pageable){ + return bookingRepository.findAll(pageable).map(BookingResponse::from); + } + + public BookingResponse updateBookingStatus(Integer bookingId, String bookingStatus){ + Booking booking = bookingRepository.findById(bookingId).orElseThrow(()-> new RuntimeException("Booking Not Found:" + bookingId)); + + booking.setBookingStatus(Booking.BookingStatus.valueOf(bookingStatus)); + bookingRepository.save(booking); + return BookingResponse.from(booking); + } + + public BookingResponse updatePaymentStatus(Integer bookingId, String paymentStatus){ + Booking booking = bookingRepository.findById(bookingId).orElseThrow(()-> new RuntimeException("Booking Not Found:" + bookingId)); + + booking.setPaymentStatus(Booking.PaymentStatus.valueOf(paymentStatus)); + bookingRepository.save(booking); + return BookingResponse.from(booking); + } + + public Page getAllVenues(Pageable pageable){ + return venueRepository.findAll(pageable).map(VenueResponse::from); + } + + public VenueResponse updateVenueStatus(Integer venueId, String status){ + Venues venue = venueRepository.findById(venueId).orElseThrow(()-> new RuntimeException("Venue Not Found:" + venueId)); + + venue.setStatus(Venues.VenueStatus.valueOf(status)); + venueRepository.save(venue); + if("APPROVED".equals(status)){ + meilisearchService.indexVenue(venue); + } + if("REMOVED".equals(status)){ + meilisearchService.removeVenue(venue.getId()); + } + return VenueResponse.from(venue); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AuthService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AuthService.java new file mode 100644 index 000000000..22fc6281f --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/AuthService.java @@ -0,0 +1,58 @@ +package com.bookmyvenue.service; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.AuthResponse; +import com.bookmyvenue.dto.LoginRequest; +import com.bookmyvenue.dto.RegisterRequest; +import com.bookmyvenue.exception.UserAlreadyExistsException; +import com.bookmyvenue.exception.UserDeactivatedException; +import com.bookmyvenue.model.User; +import com.bookmyvenue.repository.UserRepository; +import com.bookmyvenue.security.JwtUtil; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final AuthenticationManager authenticationManager; + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + + public AuthResponse register(RegisterRequest request){ + if(userRepository.existsByEmail(request.getEmail())){ + throw new UserAlreadyExistsException("Email alredy exists"); + } + + User user = User.builder() + .name(request.getName())//user.setname(request.getName()); + .email(request.getEmail()) + .password(passwordEncoder.encode(request.getPassword())) + .role(request.getRole() != null ? request.getRole() : User.Role.user) + .location(request.getLocation()) + .build(); + + userRepository.save(user); + + String token = jwtUtil.generateToken(user.getEmail(), user.getRole().name()); + return new AuthResponse(token, user.getName(), user.getEmail(), user.getRole().name()); + + } + + public AuthResponse login(LoginRequest request){ + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(),request.getPassword())); + User user = userRepository.findByEmail(request.getEmail()).orElseThrow(() -> new RuntimeException("User not found")); + if(!user.getActive()){ + throw new UserDeactivatedException(); + } + String token = jwtUtil.generateToken(user.getEmail(), user.getRole().name()); + return new AuthResponse(token, user.getName(), user.getEmail(), user.getRole().name()); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/CancellationService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/CancellationService.java new file mode 100644 index 000000000..86432d536 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/CancellationService.java @@ -0,0 +1,171 @@ +package com.bookmyvenue.service; + +import java.time.LocalDateTime; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.CancelRequest; +import com.bookmyvenue.dto.CancelReviewRequest; +import com.bookmyvenue.dto.CancellationResponse; +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.model.BookingCancellation; +import com.bookmyvenue.model.BookingCancellation.CancellationStatus; +import com.bookmyvenue.model.User; +import com.bookmyvenue.repository.BookingCancellationRepository; +import com.bookmyvenue.repository.BookingRepository; +import com.bookmyvenue.repository.UserRepository; +import com.stripe.Stripe; +import com.stripe.model.Refund; +import com.stripe.param.RefundCreateParams; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CancellationService { + private final BookingRepository bookingRepository; + private final BookingCancellationRepository cancellationRepository; + private final UserRepository userRepository; + + @Value("${stripe.secret.key}") + private String stripeSecretKey; + + // USER: submit cancel request + @Transactional + public CancellationResponse requestCancellation(Integer bookingId, CancelRequest request, String userEmail) { + User user = userRepository.findByEmail(userEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new RuntimeException("Booking not found: " + bookingId)); + + // Only the booking owner can cancel + if (!booking.getUser().getId().equals(user.getId())) { + throw new AccessDeniedException("You can only cancel your own bookings"); + } + + // Only PENDING or APPROVED bookings can be cancelled + if (booking.getBookingStatus() == Booking.BookingStatus.CANCELLED || + booking.getBookingStatus() == Booking.BookingStatus.REJECTED) { + throw new RuntimeException("This booking cannot be cancelled"); + } + + // Check if cancel request already exists + cancellationRepository.findByBookingId(bookingId).ifPresent(existing -> { + if(existing.getStatus() == CancellationStatus.PENDING){ + throw new RuntimeException("A cancellation request already exists for this booking"); + } + }); + + BookingCancellation cancellation = cancellationRepository + .findByBookingId(bookingId) + .map(existing -> { existing.setReason(request.getReason()); + existing.setStatus(CancellationStatus.PENDING); + existing.setOwnerResponse(null); + existing.setReviewedOn(null); + existing.setCreatedOn(LocalDateTime.now()); + return existing; }) + .orElseGet(() -> BookingCancellation.builder() + .booking(booking) .reason(request.getReason()) + .cancelledBy(BookingCancellation.CancelledBy.USER) + .status(CancellationStatus.PENDING) + .build() ); + + BookingCancellation saved = cancellationRepository.save(cancellation); + return CancellationResponse.from(saved); + } + + //OWNER: get all cancel requests for their venues + public Page getCancelRequestsForOwner(String ownerEmail, Pageable pageable) { + User owner = userRepository.findByEmail(ownerEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + return cancellationRepository.findByVenueOwnerId(owner.getId(),pageable) + .map(CancellationResponse::from); + } + +//OWNER: approve or reject cancel request +@Transactional +public CancellationResponse reviewCancellation( + Integer cancellationId, + CancelReviewRequest request, + String ownerEmail) { + + User owner = userRepository.findByEmail(ownerEmail) + .orElseThrow(() -> new RuntimeException("User not found")); + + BookingCancellation cancellation = cancellationRepository.findById(cancellationId) + .orElseThrow(() -> new RuntimeException("Cancellation request not found")); + + // Make sure this cancel request belongs to this owners venue + if (!cancellation.getBooking().getVenue().getUser().getId().equals(owner.getId())) { + throw new AccessDeniedException("You cannot review this cancellation request"); + } + + // Only PENDING requests can be reviewed + if (cancellation.getStatus() != CancellationStatus.PENDING) { + throw new RuntimeException("This request has already been reviewed"); + } + + cancellation.setStatus(CancellationStatus.valueOf(request.getStatus())); + cancellation.setOwnerResponse(request.getOwnerResponse()); + cancellation.setReviewedOn(LocalDateTime.now()); + + Booking booking = cancellation.getBooking(); + + if ("APPROVED".equalsIgnoreCase(request.getStatus())) { + + // Cancel the booking + booking.setBookingStatus(Booking.BookingStatus.CANCELLED); + + // If booking was PAID — issue Stripe refund + if (booking.getPaymentStatus() == Booking.PaymentStatus.PAID) { + try { + issueStripeRefund(booking); + booking.setPaymentStatus(Booking.PaymentStatus.REFUNDED); + + log.info("Refund issued for booking id={}", booking.getId()); + + } catch (Exception e) { + + log.error( + "Stripe refund failed for booking id={}: {}", + booking.getId(), + e.getMessage()); + } + } + + bookingRepository.save(booking); + + } else { + + // REJECTED — booking stays APPROVED, no changes to booking + log.info("Cancellation rejected for booking id={}", booking.getId()); + } + + BookingCancellation reviewed = cancellationRepository.save(cancellation); + + return CancellationResponse.from(reviewed); +} + // Stripe refund + private void issueStripeRefund(Booking booking) throws Exception { + Stripe.apiKey = stripeSecretKey; + if (booking.getPaymentIntentId() != null) { + RefundCreateParams params = RefundCreateParams.builder() + .setPaymentIntent(booking.getPaymentIntentId()) + .build(); + Refund refund = Refund.create(params); + log.info("Stripe refund created: {}", refund.getId()); + } else { + log.warn("No paymentIntentId stored for booking id={}, skipping Stripe refund", + booking.getId()); + } + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/MeilisearchService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/MeilisearchService.java new file mode 100644 index 000000000..bfda4121b --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/MeilisearchService.java @@ -0,0 +1,111 @@ +package com.bookmyvenue.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.VenueSearchDocument; +import com.bookmyvenue.model.Venues; +import com.meilisearch.sdk.Client; +import com.meilisearch.sdk.Index; +import com.meilisearch.sdk.SearchRequest; +import com.meilisearch.sdk.model.SearchResult; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import tools.jackson.databind.ObjectMapper; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MeilisearchService { + private final Client meilisearchClient; + private final ObjectMapper objectMapper; + + private static final String INDEX_NAME = "venues"; + + private Index getIndex() { + try { + return meilisearchClient.index(INDEX_NAME); + } catch (Exception e) { + log.error("Failed to get Meilisearch index", e); + throw new RuntimeException("Meilisearch unavailable"); + } + } + + public void indexVenue(Venues venue) { + try { + VenueSearchDocument doc = VenueSearchDocument.from(venue); + String json = objectMapper.writeValueAsString(List.of(doc)); + getIndex().addDocuments(json, "id"); + log.info("Indexed venue id={} name={}", venue.getId(), venue.getVenueName()); + } catch (Exception e) { + log.error("Failed to index venue id={}: {}", venue.getId(), e.getMessage()); + } + } + + public void removeVenue(Integer venueId) { + try { + getIndex().deleteDocument(String.valueOf(venueId)); + log.info("Removed venue id={} from search index", venueId); + } catch (Exception e) { + log.error("Failed to remove venue id={} from index: {}", venueId, e.getMessage()); + } + } + + public List searchVenues(String query, String venueType,Integer minPrice, Integer maxPrice) { + try { + Index index = getIndex(); + + SearchRequest searchRequest = new SearchRequest(query != null ? query : ""); + searchRequest.setLimit(20); + + // Build filter + StringBuilder filter = new StringBuilder(); + if (venueType != null && !venueType.isEmpty()) { + filter.append("venueType = '").append(venueType).append("'"); + } + if (minPrice != null) { + if (filter.length() > 0) filter.append(" AND "); + filter.append("price >= ").append(minPrice); + } + if (maxPrice != null) { + if (filter.length() > 0) filter.append(" AND "); + filter.append("price <= ").append(maxPrice); + } + if (filter.length() > 0) { + searchRequest.setFilter(new String[]{filter.toString()}); + } + + SearchResult results = (SearchResult) index.search(searchRequest); + + return results.getHits() + .stream() + .map(hit -> objectMapper.convertValue(hit, VenueSearchDocument.class)) + .toList(); + + } catch (Exception e) { + log.error("Search failed: {}", e.getMessage()); + return List.of(); + } + } + + // Meilisearch needs to know which fields can be filtered BEFORE filtering works + public void configureIndex() { + try { + Index index = getIndex(); + + index.updateFilterableAttributesSettings( + new String[]{"venueType", "price", "capacity", "parkingAvailable", "location"} + ); + + index.updateSearchableAttributesSettings( + new String[]{"venueName", "location", "venueDescription", "venueType"} + ); + + log.info("Meilisearch index configured successfully"); + } catch (Exception e) { + log.error("Failed to configure Meilisearch index: {}", e.getMessage()); + } + } +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/OwnerService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/OwnerService.java new file mode 100644 index 000000000..630c5db69 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/OwnerService.java @@ -0,0 +1,125 @@ +package com.bookmyvenue.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.BookingReviewRequest; +import com.bookmyvenue.dto.VenueRequest; +import com.bookmyvenue.dto.VenueResponse; +import com.bookmyvenue.dto.VenueUpdateRequest; +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.model.User; +import com.bookmyvenue.model.Venues; +import com.bookmyvenue.repository.BookingRepository; +import com.bookmyvenue.repository.UserRepository; +import com.bookmyvenue.repository.VenueRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class OwnerService { + private final MeilisearchService meilisearchService; + public final VenueRepository venueRepository; + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + + + public VenueResponse createVenue(VenueRequest request, String userEmail ){ + User user = userRepository.findByEmail(userEmail).orElseThrow(() -> new RuntimeException("User not found")); + + if(user.getRole() != User.Role.owner){ + throw new org.springframework.security.access.AccessDeniedException("Only owner can register venues"); + } + + Venues venues = Venues.builder() + .venueName(request.getVenueName()) + .venueType(request.getVenueType()) + .location(request.getLocation()) + .venueDescription(request.getVenueDescription()) + .capacity(request.getCapacity()) + .price(request.getPrice()) + .parkingAvailable(request.getParkingAvailable()) + .imageUrl(request.getImageUrl()) + .termsAccepted(request.getTermsAccepted()) + .user(user) + .build(); + + Venues saved = venueRepository.save(venues); + // return new VenueResponse(venues.getVenueName(),venues.getVenueType().name(), venues.getLocation(), venues.getVenueDescription(), venues.getCapacity(), venues.getPrice(), venues.getParkingAvailable().name(), venues.getImageUrl(), venues.getTermsAccepted()); + return VenueResponse.from(saved); + + } + + public List getMyVenues(String ownerEmail){ + User owner = userRepository.findByEmail(ownerEmail).orElseThrow(()-> new RuntimeException("User not found")); + + return venueRepository.findByUserId(owner.getId()).stream().map(VenueResponse::from).collect(Collectors.toList()); + } + + public VenueResponse updateVenue(Integer venueId, VenueUpdateRequest request, String ownerEmail){ + Venues venue = venueRepository.findById(venueId).orElseThrow(()->new RuntimeException("venue not found with ID" + venueId)); + + if (!venue.getUser().getEmail().equals(ownerEmail)){ + throw new org.springframework.security.access.AccessDeniedException("you are not allowed to edit this venue"); + } + + if(request.getVenueName() != null) venue.setVenueName(request.getVenueName()); + if(request.getVenueType() != null) venue.setVenueType(request.getVenueType()); + if(request.getLocation() != null) venue.setLocation(request.getLocation()); + if(request.getVenueDescription() != null) venue.setVenueDescription(request.getVenueDescription()); + if(request.getCapacity() != null) venue.setCapacity(request.getCapacity()); + if(request.getPrice() != null) venue.setPrice(request.getPrice()); + if(request.getParkingAvailable() != null) venue.setParkingAvailable(request.getParkingAvailable()); + if(request.getImageUrl() != null) venue.setImageUrl(request.getImageUrl()); + + venue.setApproverMessage(null); + venue.setStatus(Venues.VenueStatus.PENDING); + + Venues updated = venueRepository.save(venue); + + return VenueResponse.from(updated); + } + + public void deleteVenue(Integer venueId, String ownerEmail){ + Venues venue = venueRepository.findById(venueId).orElseThrow(()-> new RuntimeException("Venue not found with Id:"+ venueId)); + + if (!venue.getUser().getEmail().equals(ownerEmail)){ + throw new org.springframework.security.access.AccessDeniedException("you are not allowed to delete this venue"); + } + + venueRepository.deleteById(venueId); + meilisearchService.removeVenue(venueId); + } + + public Page getBookingsReviews(String userEmail, Pageable pageable){ + User owner = userRepository.findByEmail(userEmail).orElseThrow(()->new RuntimeException("User not found")); + return bookingRepository.findByVenueUserId(owner.getId(),pageable).map(BookingResponse::from); + + } + + public BookingResponse reviewBooking(Integer bookingId, BookingReviewRequest request){ + + Booking booking = bookingRepository.findById(bookingId).orElseThrow(()-> new RuntimeException("Booking Not Found:" + bookingId)); + + if(booking.getBookingStatus() != Booking.BookingStatus.PENDING){ + throw new RuntimeException("booking is alredy "+booking.getBookingStatus()); + } + + booking.setBookingStatus(Booking.BookingStatus.valueOf(request.getBookingStatus())); + + booking.setOwnerComments(request.getOwnerComments()); + + booking.setReviewedOn(LocalDateTime.now()); + + bookingRepository.save(booking); + return BookingResponse.from(booking); + } + +} diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/PaymentService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/PaymentService.java new file mode 100644 index 000000000..77f9de16e --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/PaymentService.java @@ -0,0 +1,107 @@ +package com.bookmyvenue.service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.dto.PaymentIntentResponse; +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.repository.BookingRepository; +import com.stripe.Stripe; +import com.stripe.model.PaymentIntent; +import com.stripe.param.PaymentIntentCreateParams; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentService { + + private final BookingRepository bookingRepository; + + @Value("${stripe.secret.key}") + private String stripeSecretKey; + + @Value("${stripe.publishable.key}") + private String stripePublishableKey; + + //Create Payment Intent + // Called when user clicks "Proceed to Pay" + // Stripe needs amount in smallest currency unit — paise for INR + public PaymentIntentResponse createPaymentIntent(Integer bookingId) throws Exception { + + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new RuntimeException("Booking not found: " + bookingId)); + + if (booking.getBookingStatus() != Booking.BookingStatus.APPROVED) { + throw new RuntimeException("Booking must be APPROVED before payment"); + } + + if (booking.getPaymentStatus() == Booking.PaymentStatus.PAID) { + throw new RuntimeException("This booking is already paid"); + } + + Stripe.apiKey = stripeSecretKey; + + Long amountInPaise = (long) booking.getVenue().getPrice() * 100; + + // Create metadata so we can identify this booking on webhook + Map metadata = new HashMap<>(); + metadata.put("bookingId", String.valueOf(bookingId)); + metadata.put("venueName", booking.getVenue().getVenueName()); + + // Create Stripe PaymentIntent + PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() + .setAmount(amountInPaise) + .setCurrency("inr") + .putAllMetadata(metadata) + .setDescription("BookMyVenue — " + booking.getVenue().getVenueName() + + " on " + booking.getBookingDate()) + .build(); + + PaymentIntent intent = PaymentIntent.create(params); + + log.info("PaymentIntent created: {} for booking id={}", intent.getId(), bookingId); + + return new PaymentIntentResponse( + intent.getClientSecret(), + stripePublishableKey, + amountInPaise, + "inr", + bookingId + ); + } + + // Confirm payment success + // Called after Stripe confirms payment on frontend + @Transactional + public BookingResponse confirmPayment(Integer bookingId, String paymentIntentId) throws Exception { + + Stripe.apiKey = stripeSecretKey; + PaymentIntent intent = PaymentIntent.retrieve(paymentIntentId); + + if (!"succeeded".equals(intent.getStatus())) { + throw new RuntimeException("Payment not completed. Status: " + intent.getStatus()); + } + + // Update in DB + Booking booking = bookingRepository.findById(bookingId) + .orElseThrow(() -> new RuntimeException("Booking not found")); + + booking.setPaymentStatus(Booking.PaymentStatus.PAID); + booking.setPaymentOn(LocalDateTime.now()); + booking.setPaymentIntentId(paymentIntentId); + bookingRepository.save(booking); + + log.info("Payment confirmed for booking id={} paymentIntent={}", bookingId, paymentIntentId); + + return BookingResponse.from(booking); + } +} \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/UserService.java b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/UserService.java new file mode 100644 index 000000000..f5ac90c97 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/java/com/bookmyvenue/service/UserService.java @@ -0,0 +1,67 @@ +package com.bookmyvenue.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import com.bookmyvenue.dto.BookingRequest; +import com.bookmyvenue.dto.BookingResponse; +import com.bookmyvenue.exception.VenueAlredyBookedException; +import com.bookmyvenue.model.Booking; +import com.bookmyvenue.model.Booking.BookingStatus; +import com.bookmyvenue.model.User; +import com.bookmyvenue.model.Venues; +import com.bookmyvenue.repository.BookingRepository; +import com.bookmyvenue.repository.UserRepository; +import com.bookmyvenue.repository.VenueRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + private final VenueRepository venueRepository; + + public List getApprovedVenues(){ + return venueRepository.findByStatus(Venues.VenueStatus.APPROVED).stream().map(com.bookmyvenue.dto.VenueResponse::from).collect(Collectors.toList()); + + } + + public BookingResponse createBooking(BookingRequest request, String userEmail){ + User user = userRepository.findByEmail(userEmail).orElseThrow(() -> new RuntimeException("User not found")); + + Venues venue = venueRepository.findById(request.getVenueId()).orElseThrow(()-> new RuntimeException("Venue not found")); + + if(venue.getStatus() != Venues.VenueStatus.APPROVED){ + throw new RuntimeException("This venue is not availble for boooking currently !!!"); + } + + boolean alredyBooked = bookingRepository.existsByVenueIdAndBookingDateAndBookingStatusIn(venue.getId(), request.getBookingDate(),List.of(BookingStatus.PENDING, BookingStatus.APPROVED)); + + if(alredyBooked){ + throw new VenueAlredyBookedException(""); + } + + Booking booking = Booking.builder() + .user(user) + .venue(venue) + .bookingDate(request.getBookingDate()) + .build(); + + Booking saved = bookingRepository.save(booking); + return BookingResponse.from(saved); + + } + + public Page getMyBookings(String userEmail, Pageable pageable){ + User user = userRepository.findByEmail(userEmail).orElseThrow(()->new RuntimeException("User not found")); + + return bookingRepository.findByUserId(user.getId(),pageable).map(BookingResponse::from); + } +} diff --git a/backend-bmv/bookmyvenue/src/main/resources/application.properties b/backend-bmv/bookmyvenue/src/main/resources/application.properties new file mode 100644 index 000000000..d067555ad --- /dev/null +++ b/backend-bmv/bookmyvenue/src/main/resources/application.properties @@ -0,0 +1,18 @@ +spring.application.name=bookmyvenue + +# database +spring.datasource.url=jdbc:mysql://localhost:3306/bookmyvenue +spring.datasource.username=root +spring.datasource.password=root +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA config +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +# JWT security +app.jwt.secret=5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437 +app.jwt.expiration=86400000 + +meilisearch.host = http://localhost:7700/ +meilisearch.api-key = \ No newline at end of file diff --git a/backend-bmv/bookmyvenue/src/test/java/com/bookmyvenue/BookmyvenueApplicationTests.java b/backend-bmv/bookmyvenue/src/test/java/com/bookmyvenue/BookmyvenueApplicationTests.java new file mode 100644 index 000000000..16ab63b52 --- /dev/null +++ b/backend-bmv/bookmyvenue/src/test/java/com/bookmyvenue/BookmyvenueApplicationTests.java @@ -0,0 +1,13 @@ +package com.bookmyvenue; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BookmyvenueApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/frontend-bmv/.gitignore b/frontend-bmv/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/frontend-bmv/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend-bmv/README.md b/frontend-bmv/README.md new file mode 100644 index 000000000..a36934d87 --- /dev/null +++ b/frontend-bmv/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend-bmv/eslint.config.js b/frontend-bmv/eslint.config.js new file mode 100644 index 000000000..ea36dd3dc --- /dev/null +++ b/frontend-bmv/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + parserOptions: { ecmaFeatures: { jsx: true } }, + }, + }, +]) diff --git a/frontend-bmv/index.html b/frontend-bmv/index.html new file mode 100644 index 000000000..62ab8b50b --- /dev/null +++ b/frontend-bmv/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend-bmv + + +
+ + + diff --git a/frontend-bmv/package-lock.json b/frontend-bmv/package-lock.json new file mode 100644 index 000000000..9b86fced3 --- /dev/null +++ b/frontend-bmv/package-lock.json @@ -0,0 +1,2509 @@ +{ + "name": "frontend-bmv", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend-bmv", + "version": "0.0.0", + "dependencies": { + "@stripe/react-stripe-js": "^6.6.0", + "@stripe/stripe-js": "^9.8.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-toastify": "^11.1.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stripe/react-stripe-js": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.6.0.tgz", + "integrity": "sha512-utODPiu2/JGjCnh5BX1M1F2uyjCwDKum4Bo8CeWdTCNOlzM0980YadzBMe7YoIwjfu3uadX4PMe3L2SderejqA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=9.5.0 <10.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-9.8.0.tgz", + "integrity": "sha512-DHJpol/98VKyojNSYmpkB5vOMnlf87hPe0wPxyaYTNiTMk5QjKMXDfSZLwGctYIXAgAWDFeRABc8lFAj0BELyw==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", + "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", + "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", + "integrity": "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", + "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-toastify": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.1.0.tgz", + "integrity": "sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend-bmv/package.json b/frontend-bmv/package.json new file mode 100644 index 000000000..bb4475994 --- /dev/null +++ b/frontend-bmv/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend-bmv", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@stripe/react-stripe-js": "^6.6.0", + "@stripe/stripe-js": "^9.8.0", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-toastify": "^11.1.0" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "vite": "^8.0.12" + } +} diff --git a/frontend-bmv/public/favicon.svg b/frontend-bmv/public/favicon.svg new file mode 100644 index 000000000..6893eb132 --- /dev/null +++ b/frontend-bmv/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-bmv/public/icons.svg b/frontend-bmv/public/icons.svg new file mode 100644 index 000000000..e9522193d --- /dev/null +++ b/frontend-bmv/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend-bmv/src/App.css b/frontend-bmv/src/App.css new file mode 100644 index 000000000..f6af5b42a --- /dev/null +++ b/frontend-bmv/src/App.css @@ -0,0 +1,1055 @@ +/* ========================================================= + RESET & ROOT + ========================================================= */ +.app-root { + min-height: 100vh; + width: 100%; +} + +/* ========================================================= + AUTH SHELL + ========================================================= */ +.app-container { + display: flex; + flex-direction: column; + min-height: 100vh; + width: 100%; + box-sizing: border-box; + background: var(--bg); +} + +.app-header { + padding: 18px 32px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.logo-link { + background: none; + border: none; + padding: 0; + cursor: pointer; +} + +.logo-text { + font-family: var(--heading); + font-size: 20px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--accent); +} + +.app-content { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; +} + +.app-footer-info { + padding: 18px; + border-top: 1px solid var(--border); + text-align: center; + font-size: 14px; + color: var(--text); +} + +/* ========================================================= + AUTH CARD + ========================================================= */ +.auth-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 14px; + padding: 36px; + width: 100%; + max-width: 440px; + box-shadow: var(--shadow); + text-align: left; + box-sizing: border-box; +} + +.auth-card h2 { + margin: 0 0 6px 0; + font-size: 24px; + font-weight: 700; + color: var(--text-h); + letter-spacing: -0.5px; +} + +.auth-subtitle { + color: var(--text); + font-size: 14px; + margin: 0 0 24px 0; +} + +/* ========================================================= + FORMS + ========================================================= */ +.auth-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 12px; + font-weight: 600; + color: var(--text-h); + text-transform: uppercase; + letter-spacing: 0.6px; +} + +.form-group input, +.form-group select { + font-family: var(--sans); + font-size: 15px; + padding: 10px 13px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text-h); + outline: none; + box-sizing: border-box; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.form-group input:focus, +.form-group select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-bg); +} + +.form-group input::placeholder { + color: var(--text); + opacity: 0.5; +} + +.form-row-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +/* ========================================================= + BUTTONS + ========================================================= */ +.auth-button { + font-family: var(--sans); + font-size: 15px; + font-weight: 600; + padding: 12px; + border-radius: 8px; + cursor: pointer; + transition: opacity 0.2s ease, transform 0.1s ease; + width: 100%; + box-sizing: border-box; + text-align: center; + background: var(--accent); + color: #fff; + border: none; + margin-top: 4px; +} + +.auth-button:hover:not(:disabled) { + opacity: 0.88; + transform: translateY(-1px); +} + +.auth-button:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +/* ========================================================= + FEEDBACK MESSAGES + ========================================================= */ +.auth-error { + background-color: rgba(220, 38, 38, 0.07); + border: 1px solid rgba(220, 38, 38, 0.25); + color: #dc2626; + padding: 11px 14px; + border-radius: 8px; + font-size: 14px; + margin-bottom: 16px; +} + +.auth-success { + background-color: rgba(22, 163, 74, 0.07); + border: 1px solid rgba(22, 163, 74, 0.25); + color: #16a34a; + padding: 11px 14px; + border-radius: 8px; + font-size: 14px; + margin-bottom: 16px; +} + +/* ========================================================= + AUTH FOOTER LINKS + ========================================================= */ +.auth-footer { + text-align: center; + font-size: 14px; + color: var(--text); + margin: 20px 0 0 0; +} + +.link-button { + background: none; + border: none; + color: var(--accent); + font-weight: 600; + padding: 0; + font-size: inherit; + font-family: inherit; + cursor: pointer; + text-decoration: underline; +} + +.link-button:hover { opacity: 0.75; } +.link-button:disabled { cursor: not-allowed; opacity: 0.5; } + +/* ========================================================= + LANDING PAGE + ========================================================= */ +.landing-container { + min-height: 100vh; + background: var(--bg); + display: flex; + flex-direction: column; +} + +/* Landing Header */ +.landing-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 48px; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + background: var(--bg); + z-index: 100; +} + +.header-nav-right { + display: flex; + align-items: center; +} + +.header-auth-group { + display: flex; + align-items: center; + gap: 10px; +} + +.user-greeting { + font-size: 14px; + color: var(--text); +} + +.header-btn { + font-family: var(--sans); + font-size: 14px; + font-weight: 600; + padding: 8px 18px; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; +} + +.login-btn { + background: transparent; + border-color: var(--border); + color: var(--text-h); +} + +.login-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +.register-btn { + background: var(--accent); + color: #fff; +} + +.register-btn:hover { opacity: 0.88; } + +.dashboard-btn { + background: var(--accent-bg); + color: var(--accent); + border-color: var(--accent-border); +} + +.dashboard-btn:hover { background: var(--accent); color: #fff; } + +.logout-btn-header { + background: transparent; + border-color: var(--border); + color: var(--text); +} + +.logout-btn-header:hover { border-color: var(--accent); color: var(--accent); } + +/* Hero Section */ +.hero-section { + background: var(--code-bg); + padding: 72px 48px; + text-align: center; + border-bottom: 1px solid var(--border); +} + +.hero-content { + max-width: 720px; + margin: 0 auto; +} + +.hero-content h1 { + font-size: 48px; + font-weight: 800; + letter-spacing: -2px; + line-height: 1.1; + color: var(--text-h); + margin: 0 0 14px 0; +} + +.hero-subtitle { + font-size: 17px; + color: var(--text); + margin: 0 0 36px 0; + line-height: 1.6; +} + +.hero-search-bar { + display: flex; + align-items: flex-end; + gap: 0; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 6px; + box-shadow: var(--shadow); + max-width: 660px; + margin: 0 auto; +} + +.search-field { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + padding: 7px 12px; +} + +.search-field label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--text); +} + +.search-field input, +.search-field select { + font-family: var(--sans); + font-size: 15px; + border: none; + outline: none; + background: transparent; + color: var(--text-h); + padding: 2px 0; +} + +.search-field select { cursor: pointer; } + +.search-submit-btn { + font-family: var(--sans); + font-size: 15px; + font-weight: 700; + padding: 12px 24px; + background: var(--accent); + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + transition: opacity 0.2s ease; + white-space: nowrap; +} + +.search-submit-btn:hover { opacity: 0.88; } + +/* Categories Section */ +.categories-section { + padding: 64px 48px; +} + +.section-header { + text-align: center; + margin-bottom: 36px; +} + +.section-header h2 { + font-size: 30px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text-h); + margin: 0 0 8px 0; +} + +.section-header p { + color: var(--text); + font-size: 15px; +} + +.categories-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + max-width: 1100px; + margin: 0 auto; +} + +.category-card { + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + background: var(--bg); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.category-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow); +} + +.category-image { + height: 170px; + background-size: cover; + background-position: center; + position: relative; +} + +.category-count { + position: absolute; + bottom: 10px; + left: 10px; + background: rgba(0,0,0,0.5); + color: #fff; + font-size: 12px; + font-weight: 600; + padding: 3px 10px; + border-radius: 20px; +} + +.category-body { + padding: 16px; + text-align: left; +} + +.category-body h3 { + font-size: 15px; + font-weight: 700; + color: var(--text-h); + margin: 0 0 6px 0; +} + +.category-body p { + font-size: 13px; + color: var(--text); + line-height: 1.5; + margin: 0 0 12px 0; +} + +.category-explore-btn { + background: none; + border: none; + color: var(--accent); + font-size: 13px; + font-weight: 600; + font-family: var(--sans); + cursor: pointer; + padding: 0; + transition: opacity 0.2s ease; +} + +.category-explore-btn:hover { opacity: 0.7; } + +/* Hosting CTA */ +.hosting-cta { + background: var(--accent); + padding: 64px 48px; + text-align: center; +} + +.hosting-content h2 { + font-size: 32px; + font-weight: 800; + color: #fff; + margin: 0 0 14px 0; + letter-spacing: -0.5px; +} + +.hosting-content p { + font-size: 16px; + color: rgba(255,255,255,0.85); + max-width: 500px; + margin: 0 auto 28px auto; + line-height: 1.6; +} + +.hosting-btn { + font-family: var(--sans); + font-size: 15px; + font-weight: 700; + padding: 14px 32px; + background: #fff; + color: var(--accent); + border: none; + border-radius: 8px; + cursor: pointer; + transition: opacity 0.2s ease; +} + +.hosting-btn:hover { opacity: 0.92; } + +/* ========================================================= + DASHBOARDS (shared) + ========================================================= */ +.dashboard-container { + min-height: 100vh; + display: flex; + flex-direction: column; + background: var(--bg); +} + +.dashboard-nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 28px; + border-bottom: 1px solid var(--border); + gap: 14px; + flex-wrap: wrap; + background: var(--bg); + position: sticky; + top: 0; + z-index: 50; +} + +.dashboard-user-info { + display: flex; + align-items: center; + gap: 10px; +} + +.avatar-small { + width: 38px; + height: 38px; + border-radius: 50%; + background: var(--accent-bg); + border: 1.5px solid var(--accent-border); + color: var(--accent); + font-size: 15px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.user-text-info h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text-h); +} + +.dashboard-tabs { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.tab-button { + font-family: var(--sans); + font-size: 14px; + font-weight: 500; + padding: 7px 14px; + background: transparent; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + cursor: pointer; + transition: all 0.2s ease; +} + +.tab-button:hover { border-color: var(--accent); color: var(--accent); } + +.tab-button.active { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.logout-link-btn { + font-family: var(--sans); + font-size: 14px; + font-weight: 500; + padding: 7px 14px; + background: transparent; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + cursor: pointer; + transition: all 0.2s ease; + margin-left: 4px; +} + +.logout-link-btn:hover { border-color: var(--accent); color: var(--accent); } + +.dashboard-content-area { + flex: 1; + padding: 28px 32px; + max-width: 1200px; + width: 100%; + margin: 0 auto; + box-sizing: border-box; +} + +/* ========================================================= + METRICS ROW + ========================================================= */ +.metrics-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 32px; +} + +.metric-card { + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px 24px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.metric-title { + font-size: 12px; + font-weight: 600; + color: var(--text); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.metric-val { + font-size: 32px; + font-weight: 800; + color: var(--accent); + letter-spacing: -1px; +} + +/* ========================================================= + ROLE BADGES + ========================================================= */ +.role-badge { + display: inline-block; + background: var(--accent-bg); + color: var(--accent); + padding: 3px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 700; + text-transform: capitalize; +} + +.owner-badge { background: rgba(180, 83, 9, 0.1); color: #b45309; } +.admin-badge { background: rgba(220, 38, 38, 0.1); color: #dc2626; } + +/* ========================================================= + USER DASHBOARD - Venue Browse + ========================================================= */ +.filter-bar { + display: flex; + gap: 12px; + margin-bottom: 24px; + flex-wrap: wrap; +} + +.search-input { + font-family: var(--sans); + flex: 1; + min-width: 200px; + font-size: 15px; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text-h); + outline: none; + transition: border-color 0.2s ease; +} + +.search-input:focus { border-color: var(--accent); } + +.filter-select { + font-family: var(--sans); + font-size: 15px; + padding: 10px 14px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + color: var(--text-h); + outline: none; + cursor: pointer; + min-width: 150px; +} + +.venues-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; +} + +.venue-card { + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + background: var(--bg); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.venue-card:hover { transform: translateY(-4px); box-shadow: var(--shadow); } + +.venue-image { + height: 185px; + background-size: cover; + background-position: center; + position: relative; +} + +.venue-type-tag { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0,0,0,0.5); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 3px 9px; + border-radius: 20px; + text-transform: capitalize; +} + +.venue-card-body { padding: 16px; } + +.venue-card-body h4 { + margin: 0 0 5px 0; + font-size: 15px; + font-weight: 700; + color: var(--text-h); +} + +.venue-loc { font-size: 13px; color: var(--text); margin: 0 0 12px 0; } + +.venue-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + color: var(--text); + margin-bottom: 14px; +} + +.venue-price { font-size: 15px; font-weight: 700; color: var(--accent); } + +.book-btn { + font-family: var(--sans); + font-size: 14px; + font-weight: 600; + width: 100%; + padding: 10px; + background: var(--accent); + color: #fff; + border: none; + border-radius: 8px; + cursor: pointer; + transition: opacity 0.2s ease; +} + +.book-btn:hover { opacity: 0.88; } + +/* ========================================================= + USER DASHBOARD - Bookings Table + ========================================================= */ +.bookings-section h3, +.listings-section h3, +.admin-users-section h3, +.admin-logs-section h3 { + font-size: 19px; + font-weight: 700; + color: var(--text-h); + margin: 0 0 18px 0; +} + +.bookings-table-wrapper { + overflow-x: auto; + border-radius: 10px; + border: 1px solid var(--border); +} + +.bookings-table, +.admin-users-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.bookings-table th, +.admin-users-table th { + background: var(--code-bg); + color: var(--text); + text-align: left; + padding: 12px 16px; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 1px solid var(--border); +} + +.bookings-table td, +.admin-users-table td { + padding: 13px 16px; + color: var(--text-h); + border-bottom: 1px solid var(--border); + vertical-align: middle; +} + +.bookings-table tr:last-child td, +.admin-users-table tr:last-child td { + border-bottom: none; +} + +.bookings-table tr:hover td, +.admin-users-table tr:hover td { + background: var(--accent-bg); +} + +.bold { font-weight: 600; } + +.status-badge { + display: inline-block; + padding: 3px 10px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; +} + +.status-badge.confirmed { background: rgba(22,163,74,0.1); color: #16a34a; } +.status-badge.pending-host-approval { background: rgba(180,83,9,0.1); color: #b45309; } +.status-badge.active { background: rgba(22,163,74,0.1); color: #16a34a; } +.status-badge.suspended { background: rgba(220,38,38,0.1); color: #dc2626; } + +/* ========================================================= + OWNER DASHBOARD - Listings + ========================================================= */ +.listings-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 20px; +} + +.owner-venue-card { + display: flex; + gap: 14px; + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + background: var(--bg); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.owner-venue-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); } + +.owner-venue-img { + width: 120px; + min-height: 120px; + flex-shrink: 0; + background-size: cover; + background-position: center; +} + +.owner-venue-info { + padding: 14px 14px 14px 0; + flex: 1; +} + +.owner-venue-info h4 { + margin: 0 0 4px 0; + font-size: 14px; + font-weight: 700; + color: var(--text-h); +} + +.owner-venue-stats { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: var(--text); +} +.review-message { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 4px; + font-size: 13px; + color: var(--text); +} + +.accent-text { color: var(--accent); font-weight: 600; } + +.add-venue-section { + display: flex; + justify-content: center; +} + +.form-card { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 14px; + padding: 32px; + width: 100%; + max-width: 540px; + box-shadow: var(--shadow); +} + +.form-card h3 { + margin: 0 0 22px 0; + font-size: 20px; + font-weight: 700; + color: var(--text-h); +} + +/* ========================================================= + ADMIN DASHBOARD + ========================================================= */ +.user-table-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.subtext { font-size: 12px; color: var(--text); } + +.table-select-role { + font-family: var(--sans); + font-size: 13px; + padding: 5px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text-h); + outline: none; + cursor: pointer; +} + +.table-select-role:focus { border-color: var(--accent); } + +.action-btn-sm { + font-family: var(--sans); + font-size: 12px; + font-weight: 600; + padding: 5px 12px; + border-radius: 6px; + cursor: pointer; + transition: opacity 0.2s ease; + border: none; +} + +.suspend-btn { background: rgba(220,38,38,0.1); color: #dc2626; } +.suspend-btn:hover { background: rgba(220,38,38,0.18); } +.activate-btn { background: rgba(22,163,74,0.1); color: #16a34a; } +.activate-btn:hover { background: rgba(22,163,74,0.18); } + +/* Admin Logs */ +.logs-list { display: flex; flex-direction: column; gap: 12px; } + +.log-item { + border: 1px solid var(--border); + border-radius: 10px; + padding: 14px 18px; + background: var(--bg); +} + +.log-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.log-action-badge { + font-size: 12px; + font-weight: 700; + background: var(--accent-bg); + color: var(--accent); + padding: 3px 10px; + border-radius: 20px; +} + +.log-time { font-size: 12px; color: var(--text); } +.log-details { font-size: 14px; color: var(--text-h); margin: 0; } + +.no-results { + grid-column: 1 / -1; + text-align: center; + color: var(--text); + padding: 48px; + font-size: 15px; +} + +/* ========================================================= + RESPONSIVE + ========================================================= */ +@media (max-width: 900px) { + .categories-grid { grid-template-columns: repeat(2, 1fr); } + .hero-content h1 { font-size: 34px; } + .hero-search-bar { flex-direction: column; } + .search-submit-btn { width: 100%; } + .metrics-row { grid-template-columns: 1fr 1fr; } + .dashboard-nav { flex-direction: column; align-items: flex-start; } +} + +@media (max-width: 600px) { + .landing-header { padding: 14px 18px; } + .hero-section { padding: 44px 18px; } + .categories-section { padding: 44px 18px; } + .hosting-cta { padding: 44px 18px; } + .categories-grid { grid-template-columns: 1fr; } + .metrics-row { grid-template-columns: 1fr; } + .dashboard-content-area { padding: 18px 14px; } + .auth-card { padding: 24px 18px; border-radius: 10px; } + .form-row-grid { grid-template-columns: 1fr; } + .venues-grid { grid-template-columns: 1fr; } + .listings-grid { grid-template-columns: 1fr; } + .owner-venue-img { width: 95px; } + .header-auth-group { gap: 8px; } + .header-btn { padding: 7px 12px; font-size: 13px; } +} \ No newline at end of file diff --git a/frontend-bmv/src/App.jsx b/frontend-bmv/src/App.jsx new file mode 100644 index 000000000..c760f98b7 --- /dev/null +++ b/frontend-bmv/src/App.jsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import LandingPage from './components/LandingPage'; +import Login from './components/Login'; +import Register from './components/Register'; +import Dashboard from './components/Dashboard'; +import './App.css'; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +function App() { + const [user, setUser] = useState(() => { + const savedUser = localStorage.getItem('bmv_user'); + if (savedUser) { + try { + return JSON.parse(savedUser); + } catch { + localStorage.removeItem('bmv_user'); + } + } + return null; + }); + + const [currentPage, setCurrentPage] = useState(() => { + // If already logged in, go to dashboard; otherwise show landing page + const savedUser = localStorage.getItem('bmv_user'); + return savedUser ? 'dashboard' : 'landing'; + }); + + const handleLoginSuccess = (userData) => { + setUser(userData); + localStorage.setItem('bmv_user', JSON.stringify(userData)); + setCurrentPage('dashboard'); + }; + + const handleLogout = () => { + setUser(null); + localStorage.removeItem('bmv_user'); + setCurrentPage('landing'); + }; + + const handleNavigate = (page) => { + setCurrentPage(page); + }; + + return ( +
+ {currentPage === 'landing' && ( + + )} + + {(currentPage === 'login' || currentPage === 'register') && ( +
+
+
+ +
+
+ +
+ {currentPage === 'login' && ( + + )} + {currentPage === 'register' && ( + + )} +
+ +
+

© {new Date().getFullYear()} BookMyVenue. All rights reserved.

+
+
+ )} + {currentPage === 'dashboard' && ( + + )} + +
+ ); +} + +export default App; diff --git a/frontend-bmv/src/assets/hero.png b/frontend-bmv/src/assets/hero.png new file mode 100644 index 000000000..02251f4b9 Binary files /dev/null and b/frontend-bmv/src/assets/hero.png differ diff --git a/frontend-bmv/src/assets/react.svg b/frontend-bmv/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/frontend-bmv/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-bmv/src/assets/vite.svg b/frontend-bmv/src/assets/vite.svg new file mode 100644 index 000000000..5101b674d --- /dev/null +++ b/frontend-bmv/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend-bmv/src/components/AdminDashboard.jsx b/frontend-bmv/src/components/AdminDashboard.jsx new file mode 100644 index 000000000..ff727b716 --- /dev/null +++ b/frontend-bmv/src/components/AdminDashboard.jsx @@ -0,0 +1,839 @@ +import { useState, useEffect } from 'react';import { toast } from "react-toastify"; +function AdminDashboard({ user, onLogout }) { + const [users, setUsers] = useState([]); const [pendingVenues, setPendingVenues] = useState([]); const [allBookings, setAllBookings] = useState([]); const [allVenues, setAllVenues] = useState([]); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('users'); + + // ✅ NEW: pagination state for all-bookings tab + const [bookingsPage, setBookingsPage] = useState(0); + const [bookingsTotalPages, setBookingsTotalPages] = useState(0); + const [bookingsTotalElements, setBookingsTotalElements] = useState(0); + const bookingsSize = 10; + + // ✅ NEW: pagination state for all-venues tab + const [venuesPage, setVenuesPage] = useState(0); + const [venuesTotalPages, setVenuesTotalPages] = useState(0); + const [venuesTotalElements, setVenuesTotalElements] = useState(0); + const venuesSize = 10; + + // ✅ NEW: pagination state for users tab + const [usersPage, setUsersPage] = useState(0); + const [usersTotalPages, setUsersTotalPages] = useState(0); + const [usersTotalElements, setUsersTotalElements] = useState(0); + const usersSize = 10; + + // Detail modal + const [detailVenue, setDetailVenue] = useState(null); + + // Review modal + const [modal, setModal] = useState(null); const [comment, setComment] = useState(''); + + // ── Helper ───────────────────────────────────────────────────────────────── + const getToken = () => { const userData = JSON.parse(localStorage.getItem('bmv_user')); return userData.token; }; + + // ── Fetch users — with pagination ────────────────────────────────────────── + // GET /api/admin/users?page=0&size=10&sort=createdAt,desc + const fetchUsers = async (page = 0) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/users?page=${page}&size=${usersSize}&sort=createdAt,desc`, + { headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to fetch users'); + const data = await res.json(); + // ✅ Spring Page response: data.content has the items + setUsers(data.content); + setUsersTotalPages(data.totalPages); + setUsersTotalElements(data.totalElements); + } catch (err) { console.error(err); } + }; + + // ── Fetch pending venues ─────────────────────────────────────────────────── + const fetchPendingVenues = async () => { + try { + const res = await fetch('http://localhost:8080/api/admin/venue/review', { + headers: { Authorization: `Bearer ${getToken()}` }, + }); + if (!res.ok) throw new Error('Failed to fetch venues'); + setPendingVenues(await res.json()); + } catch (err) { console.error(err); } + }; + + // ── Fetch all bookings — with pagination ─────────────────────────────────── + // GET /api/admin/bookings?page=0&size=10&sort=bookedOn,desc + const fetchAllBookings = async (page = 0) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/bookings?page=${page}&size=${bookingsSize}&sort=bookedOn,desc`, + { headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to fetch bookings'); + const data = await res.json(); + // ✅ Spring Page response: data.content has the items + setAllBookings(data.content); + setBookingsTotalPages(data.totalPages); + setBookingsTotalElements(data.totalElements); + } catch (err) { console.error(err); } + }; + + // ── Fetch all venues — with pagination ──────────────────────────────────── + // GET /api/admin/venues?page=0&size=10&sort=createdAt,desc + const fetchAllVenues = async (page = 0) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/venues?page=${page}&size=${venuesSize}&sort=createdAt,desc`, + { headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to fetch all venues'); + const data = await res.json(); + // ✅ Spring Page response: data.content has the items + setAllVenues(data.content); + setVenuesTotalPages(data.totalPages); + setVenuesTotalElements(data.totalElements); + } catch (err) { console.error(err); } + }; + + useEffect(() => { + const loadAll = async () => { + await fetchUsers(0); + await fetchPendingVenues(); + await fetchAllBookings(0); + await fetchAllVenues(0); + setLoading(false); + }; + loadAll(); + }, []); + + // ✅ NEW: re-fetch when page changes + useEffect(() => { if (!loading) fetchAllBookings(bookingsPage); }, [bookingsPage]); + useEffect(() => { if (!loading) fetchAllVenues(venuesPage); }, [venuesPage]); + useEffect(() => { if (!loading) fetchUsers(usersPage); }, [usersPage]); + + // ── Toggle user active / inactive ────────────────────────────────────────── + const handleToggleUser = async (userId, currentActive) => { + try { + const res = await fetch(`http://localhost:8080/api/admin/users/${userId}/status`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }, + body: JSON.stringify({ active: !currentActive }), + }); + if (!res.ok) throw new Error('Failed to update user status'); + setUsers(prev => prev.map(u => u.id === userId ? { ...u, active: !currentActive } : u)); + toast.success(`User marked as ${!currentActive ? 'active' : 'inactive'}`); + } catch (err) { toast.error(err.message); } + }; + + // ── Detail modal ─────────────────────────────────────────────────────────── + const openDetail = (venue) => setDetailVenue(venue); + const closeDetail = () => setDetailVenue(null); + + // ── Review modal ─────────────────────────────────────────────────────────── + const openModal = (venue, action) => { + setDetailVenue(null); + setModal({ venueId: venue.id, venueName: venue.venueName, action }); + setComment(''); + }; + const closeModal = () => { setModal(null); setComment(''); }; + + const handleConfirmReview = async () => { + if (!modal) return; + if (modal.action === 'REJECTED' && !comment.trim()) { + toast.error('Please provide a reason for rejection'); return; + } + try { + const res = await fetch( + `http://localhost:8080/api/admin/venue/${modal.venueId}/review`, + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` }, + body: JSON.stringify({ status: modal.action, approverMessage: comment }), + } + ); + if (!res.ok) throw new Error('Failed to review venue'); + toast.success(`"${modal.venueName}" ${modal.action === 'APPROVED' ? 'approved' : 'rejected'}!`); + closeModal(); + await fetchPendingVenues(); + await fetchAllVenues(venuesPage); + } catch (err) { toast.error(err.message); } + }; + + // NEW: Update booking status + const handleUpdateBookingStatus = async (bookingId, newStatus) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/booking/${bookingId}/status?bookingStatus=${newStatus}`, + { method: 'PATCH', headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to update booking status'); + toast.success(`Booking status updated to ${newStatus}`); + await fetchAllBookings(bookingsPage); + } catch (err) { toast.error(err.message); } + }; + + // NEW: Update payment status + const handleUpdatePaymentStatus = async (bookingId, newStatus) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/booking/${bookingId}/payment/status?paymentStatus=${newStatus}`, + { method: 'PATCH', headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to update payment status'); + toast.success(`Payment status updated to ${newStatus}`); + await fetchAllBookings(bookingsPage); + } catch (err) { toast.error(err.message); } + }; + + // NEW: Update venue status + const handleUpdateVenueStatus = async (venueId, newStatus) => { + try { + const res = await fetch( + `http://localhost:8080/api/admin/venue/${venueId}/status?status=${newStatus}`, + { method: 'PATCH', headers: { Authorization: `Bearer ${getToken()}` } } + ); + if (!res.ok) throw new Error('Failed to update venue status'); + toast.success(`Venue status updated to ${newStatus}`); + await fetchAllVenues(venuesPage); + await fetchPendingVenues(); + } catch (err) { toast.error(err.message); } + }; + + // ── Metrics ──────────────────────────────────────────────────────────────── + const totalUsers = usersTotalElements || users.length; + const activeUsers = users.filter(u => u.active).length; + const totalPending = pendingVenues.length; + + // ── Shared select style ──────────────────────────────────────────────────── + const selectStyle = { + fontSize: 12, padding: '4px 6px', borderRadius: 6, + border: '1px solid var(--border)', + background: 'var(--bg)', color: 'var(--text-h)', + cursor: 'pointer', fontFamily: 'var(--sans)', + }; + + // ✅ NEW: reusable pagination controls component + const Pagination = ({ page, totalPages, totalElements, size, onPageChange, label }) => { + if (totalPages <= 1) return null; + const from = page * size + 1; + const to = Math.min((page + 1) * size, totalElements); + return ( +
+ {/* Showing X–Y of Z */} + + Showing {from}–{to} of {totalElements} {label} + + + {/* Page buttons */} +
+ {/* First */} + + {/* Prev */} + + + {/* Page number pills */} + {Array.from({ length: totalPages }, (_, i) => i) + .filter(i => i === 0 || i === totalPages - 1 || + (i >= page - 1 && i <= page + 1)) + .reduce((acc, i, idx, arr) => { + if (idx > 0 && i - arr[idx - 1] > 1) { + acc.push('...'); + } + acc.push(i); + return acc; + }, []) + .map((item, idx) => + item === '...' ? ( + + … + + ) : ( + + ) + ) + } + + {/* Next */} + + {/* Last */} + +
+
+ ); + }; + + // ✅ page button style helper + const pageBtn = (disabled, active = false) => ({ + padding: '4px 10px', borderRadius: 6, fontSize: 13, + border: '0.5px solid var(--border)', + background: active ? 'var(--accent)' : 'var(--bg)', + color: active ? '#fff' : disabled ? 'var(--border)' : 'var(--text-h)', + cursor: disabled ? 'not-allowed' : 'pointer', + fontFamily: 'var(--sans)', + opacity: disabled ? 0.4 : 1, + }); + + // ── Render ───────────────────────────────────────────────────────────────── + return ( +
+ + {/* ── Header ── */} +
+
+
+ {user?.name ? user.name.charAt(0).toUpperCase() : 'A'} +
+
+

{user?.name || 'Guest'}

+ Administrator +
+
+
+ + + + + +
+
+ +
+ + {/* ── Metrics ── */} +
+
+ Total users + {usersTotalElements || users.length} +
+
+ Active users + {activeUsers} +
+
+ Pending approvals + {totalPending} +
+
+ Total bookings + {bookingsTotalElements} +
+
+ + {loading &&

Loading...

} + + {/* ════════════════════════════════════════════════ + TAB 1 — USER MANAGEMENT + ════════════════════════════════════════════════ */} + {activeTab === 'users' && !loading && ( +
+

User accounts registry

+ {users.length === 0 &&

No users found.

} + {users.length > 0 && ( +
+ + + + + + + + + + + + + {users.map(u => ( + + + + + + + + + ))} + +
User detailsLocationRoleJoinedStatusAction
+
+ {u.name} + {u.email} +
+
{u.location || '—'} + + {u.role} + + + {new Date(u.createdAt).toLocaleDateString('en-IN', { + day: 'numeric', month: 'short', year: 'numeric', + })} + + + {u.active ? 'active' : 'inactive'} + + + +
+ {/* ✅ Pagination for users */} + setUsersPage(p)} + label="users" + /> +
+ )} +
+ )} + + {/* ════════════════════════════════════════════════ + TAB 2 — PENDING VENUES (unchanged) + ════════════════════════════════════════════════ */} + {activeTab === 'venues' && !loading && ( +
+

Pending venue approvals

+ {pendingVenues.length === 0 &&

No pending venues to review.

} + {pendingVenues.length > 0 && ( +
+ + + + + + + + + + + + + {pendingVenues.map(venue => ( + + + + + + + + + ))} + +
Venue detailsOwnerTypePriceStatusAction
+
+ openDetail(venue)} + title="Click to view full details" + > + {venue.venueName} + + {venue.location} +
+
+
+ {venue.ownerName} + {venue.ownerEmail} +
+
{venue.venueType} + + ₹{venue.price.toLocaleString('en-IN')} + + + + {venue.status} + + + {venue.status === 'PENDING' ? ( +
+ + +
+ ) : ( + {venue.status.toLowerCase()} + )} +
+
+ )} +
+ )} + + {/* ════════════════════════════════════════════════ + TAB 3 — ALL BOOKINGS with pagination + ════════════════════════════════════════════════ */} + {activeTab === 'all-bookings' && !loading && ( +
+

All Bookings

+ {allBookings.length === 0 &&

No bookings found.

} + {allBookings.length > 0 && ( +
+ + + + + + + + + + + + + {allBookings.map(booking => ( + + + + + + + + + ))} + +
VenueUserBooking dateBooked onBooking statusPayment status
+
+ {booking.venueName} + {booking.venueLocation} +
+
+
+ {booking.customerName || '—'} + {booking.customerEmail || '—'} +
+
+ {booking.bookingDate + ? new Date(booking.bookingDate + 'T00:00:00').toLocaleDateString('en-IN', { + day: 'numeric', month: 'short', year: 'numeric', + }) + : '—'} + + {booking.bookedOn + ? new Date(booking.bookedOn).toLocaleDateString('en-IN', { + day: 'numeric', month: 'short', year: 'numeric', + }) + : '—'} + + + + +
+ {/* ✅ Pagination for bookings */} + setBookingsPage(p)} + label="bookings" + /> +
+ )} +
+ )} + + {/* ════════════════════════════════════════════════ + TAB 4 — ALL VENUES with pagination + ════════════════════════════════════════════════ */} + {activeTab === 'all-venues' && !loading && ( +
+

All Venues

+ {allVenues.length === 0 &&

No venues found.

} + {allVenues.length > 0 && ( +
+ + + + + + + + + + + + + {allVenues.map(venue => ( + + + + + + + + + ))} + +
Venue detailsOwnerTypePriceCapacityStatus
+
+ {venue.venueName} + {venue.location} +
+
+
+ {venue.ownerName} + {venue.ownerEmail} +
+
{venue.venueType} + + ₹{venue.price.toLocaleString('en-IN')} + + {venue.capacity} guests + +
+ {/* ✅ Pagination for venues */} + setVenuesPage(p)} + label="venues" + /> +
+ )} +
+ )} + + {/* ════════════════════════════════════════════════ + VENUE DETAIL MODAL (unchanged) + ════════════════════════════════════════════════ */} + {detailVenue && ( +
+
e.stopPropagation()}> +
+
+

{detailVenue.venueName}

+

{detailVenue.location}

+
+ + {detailVenue.status} + +
+ {detailVenue.imageUrl && ( +
+ )} + {detailVenue.venueDescription && ( +

+ {detailVenue.venueDescription} +

+ )} +
+ {[ + { label: 'Venue type', value: detailVenue.venueType }, + { label: 'Capacity', value: `${detailVenue.capacity} guests` }, + { label: 'Price per day', value: `₹${detailVenue.price.toLocaleString('en-IN')}` }, + { label: 'Parking', value: detailVenue.parkingAvailable }, + { label: 'Owner', value: detailVenue.ownerName }, + { label: 'Owner email', value: detailVenue.ownerEmail }, + { label: 'Submitted on', value: new Date(detailVenue.createdAt).toLocaleDateString('en-IN', { day: 'numeric', month: 'short', year: 'numeric' }) }, + { label: 'Terms accepted',value: detailVenue.termsAccepted ? 'Yes' : 'No' }, + ].map(item => ( +
+
+ {item.label} +
+
+ {item.value} +
+
+ ))} +
+
+ + {detailVenue.status === 'PENDING' && ( + <> + + + + )} +
+
+
+ )} + + {/* ════════════════════════════════════════════════ + APPROVE / REJECT MODAL (unchanged) + ════════════════════════════════════════════════ */} + {modal && ( +
+
e.stopPropagation()}> +

+ {modal.action === 'APPROVED' ? 'Approve venue' : 'Reject venue'} +

+
+ Venue: + {modal.venueName} +
+
+ +