diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba88e3d..e87337c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,7 +29,7 @@ jobs: run: | docker buildx build --platform linux/amd64 \ -t ${{ secrets.DOCKER_USERNAME }}/hscodes:${{ github.run_number }} \ - --file Autumn.UI/Dockerfile \ + --file Dockerfile \ --push . - name: Set up SSH agent @@ -44,44 +44,46 @@ jobs: run: | if [ "${{ github.event_name }}" == "push" ]; then ENVIRONMENT="production" - BRANCH="main" # Deploy from the main branch for production + BRANCH="main" + DEPLOY_DIR="/root/hscodes" + COMPOSE_FILE="docker-compose.prod.yml" + CONNECTION_STRING="${{ secrets.PROD_CONNECTION_STRING }}" else ENVIRONMENT="staging" - BRANCH="${{ github.head_ref }}" # Deploy from the PR branch for staging + BRANCH="${{ github.head_ref }}" + DEPLOY_DIR="/root/hscodes-staging" + COMPOSE_FILE="docker-compose.staging.yml" + CONNECTION_STRING="${{ secrets.STAGING_CONNECTION_STRING }}" fi - RUN_NUMBER=${{ github.run_number }} # Get the GitHub Run Number + RUN_NUMBER=${{ github.run_number }} echo "Deploying to $ENVIRONMENT environment..." + # Write .env file locally and upload to VPS + cat > /tmp/deploy.env << EOF + AUTH0_DOMAIN=${{ secrets.AUTH0_DOMAIN }} + AUTH0_AUDIENCE=${{ secrets.AUTH0_AUDIENCE }} + GROQAPIKEY=${{ secrets.GROQAPIKEY }} + CONNECTION_STRING=${CONNECTION_STRING} + GITHUB_RUN_NUMBER=${RUN_NUMBER} + EOF + # Remove leading whitespace from heredoc + sed -i 's/^[[:space:]]*//' /tmp/deploy.env + + ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} "mkdir -p ${DEPLOY_DIR}" + scp /tmp/deploy.env ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:${DEPLOY_DIR}/.env + + # Deploy on VPS ssh ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} " - if [ '$ENVIRONMENT' == 'production' ]; then - mkdir -p /root/hscodes && - cd /root/hscodes; - else - mkdir -p /root/hscodes-staging && - cd /root/hscodes-staging; - fi && - if [ ! -d .git ]; then - echo 'Directory is not a git repository. Cloning...'; - git clone https://github.com/samabos/hscodesdotnet.git .; - fi; + cd ${DEPLOY_DIR} && + if [ ! -d .git ]; then + echo 'Cloning repository...' && + git clone https://github.com/samabos/hscodesdotnet.git .; + fi && git reset --hard HEAD && - git fetch origin $BRANCH && - git checkout $BRANCH && - git pull origin $BRANCH && - if [ '$ENVIRONMENT' == 'production' ]; then - export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_CLIENTID='${{ secrets.AUTH0_CLIENTID }}' && - export CONNECTION_STRING='${{ secrets.PROD_CONNECTION_STRING }}' && - export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker-compose -f ./docker-compose.prod.yml down && - docker-compose -f ./docker-compose.prod.yml up -d --build; - else - export AUTH0_DOMAIN='${{ secrets.AUTH0_DOMAIN }}' && - export AUTH0_CLIENTID='${{ secrets.AUTH0_CLIENTID }}' && - export CONNECTION_STRING='${{ secrets.STAGING_CONNECTION_STRING }}' && - export GITHUB_RUN_NUMBER=$RUN_NUMBER && - docker-compose -f ./docker-compose.staging.yml up -d --build; - fi + git fetch origin ${BRANCH} && + git checkout ${BRANCH} && + git pull origin ${BRANCH} && + docker compose -f ./${COMPOSE_FILE} down && + docker compose -f ./${COMPOSE_FILE} up -d " - diff --git a/.gitignore b/.gitignore index a4fe18b..75ce13d 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,14 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# App settings with secrets +**/appsettings.json +**/appsettings.Development.json + +# Claude Code +.claude/ + +# Temp files +nul +hs-codes-redesign.jsx diff --git a/Autumn.API/Autumn.API.csproj b/Autumn.API/Autumn.API.csproj index 846e47f..43dddde 100644 --- a/Autumn.API/Autumn.API.csproj +++ b/Autumn.API/Autumn.API.csproj @@ -1,35 +1,22 @@ - netcoreapp2.2 - InProcess + net10.0 + enable + enable - + + + - - all - true - - - - - - + + + + - - - - - - - - - - - diff --git a/Autumn.API/Contract/V1/ApiRoutes.cs b/Autumn.API/Contract/V1/ApiRoutes.cs deleted file mode 100644 index e660859..0000000 --- a/Autumn.API/Contract/V1/ApiRoutes.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1 -{ - public static class ApiRoutes - { - public const string Root = "api"; - public const string Version = "v1"; - //public const string Base = Root + "/" + Version; - public const string Base = Version; - public static class Search - { - public const string Get = Base + "/search"; - } - public static class Note - { - public const string Get = Base + "/note/{hscode}"; - } - public static class Duty - { - public const string Get = Base + "/duty"; - } - public static class CodeList - { - public const string Currency = Base + "/codelist/currency"; - public const string Tags = Base + "/codelist/tags/{query?}"; - public const string Products = Base + "/codelist/products/{query?}"; - } - public static class Identity - { - public const string Login = Base + "/identity/login"; - public const string Register = Base + "/identity/register"; - } - - public static class Keyword - { - public const string Get = Base + "/keyword/get"; - } - } -} diff --git a/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs b/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs deleted file mode 100644 index eb9618a..0000000 --- a/Autumn.API/Contract/V1/Requests/ClassifyCommodityRequest.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class ClassifyCommodityRequest - { - public string ItemDescription { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/DutyRequest.cs b/Autumn.API/Contract/V1/Requests/DutyRequest.cs deleted file mode 100644 index 629235a..0000000 --- a/Autumn.API/Contract/V1/Requests/DutyRequest.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class DutyRequest - { - [BindProperty] - [Required] - [Display(Name = "Commodity Description")] - public string ProductDesc { get; set; } - [BindProperty] - [Required] - [Display(Name = "HS Code")] - public string HSCode { get; set; } - [BindProperty] - [Required] - [Display(Name = "Cost Price")] - public decimal Cost { get; set; } - [BindProperty] - [Required] - [Display(Name = "Freight Amount")] - public decimal Freight { get; set; } - [BindProperty] - [Required] - [Display(Name = "Insurance Amount")] - public decimal Insurance { get; set; } - [BindProperty] - [Required] - [Display(Name = "Currency")] - public string Currency { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/SearchRequest.cs b/Autumn.API/Contract/V1/Requests/SearchRequest.cs deleted file mode 100644 index ca64900..0000000 --- a/Autumn.API/Contract/V1/Requests/SearchRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class SearchRequest - { - public string id { get; set; } - public string pid { get; set; } - public string level { get; set; } - public string keyword { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Requests/UserRequest.cs b/Autumn.API/Contract/V1/Requests/UserRequest.cs deleted file mode 100644 index 6b95bb7..0000000 --- a/Autumn.API/Contract/V1/Requests/UserRequest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Requests -{ - public class UserRequest - { - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs b/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs deleted file mode 100644 index 18470b0..0000000 --- a/Autumn.API/Contract/V1/Responses/AuthFailedResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class AuthFailedResponse - { - public IEnumerable Error { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs b/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs deleted file mode 100644 index 9d93c88..0000000 --- a/Autumn.API/Contract/V1/Responses/AuthSuccessResponse.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class AuthSuccessResponse - { - public string Token { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs b/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs deleted file mode 100644 index 728103b..0000000 --- a/Autumn.API/Contract/V1/Responses/ClassifyCommodityResponse.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class ClassifyCommodityResponse - { - - public bool Success { get; set; } - public IEnumerable Errors { get; set; } - - public string HSCode { get; set; } - public string Accuracy { get; set; } - public HSCodeTariff Record { get; set; } - } - public class HSCodeTariff - { - public long Id { get; set; } - public string Code { get; set; } - public string Description { get; set; } - public string Duty { get; set; } - public string Levy { get; set; } - public string VAT { get; set; } - public string NAC { get; set; } - public string SUR { get; set; } - public string ETL { get; set; } - public string CIS { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/CurrencyObject.cs b/Autumn.API/Contract/V1/Responses/CurrencyObject.cs deleted file mode 100644 index de78b65..0000000 --- a/Autumn.API/Contract/V1/Responses/CurrencyObject.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class CurrencyObject - { - public string CurrencyCode { get; set; } - public string Rate { get; set; } - public string TimeStamp { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs b/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs deleted file mode 100644 index dba4451..0000000 --- a/Autumn.API/Contract/V1/Responses/CurrencyResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class CurrencyResponse - { - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs b/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs deleted file mode 100644 index bf2d292..0000000 --- a/Autumn.API/Contract/V1/Responses/CustomsTariffObject.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses { - public class CustomsTariffObject - { - public string Id { get; set; } - public string Header { get; set; } - public string HSCode { get; set; } - public string Description { get; set; } - public string DUTY { get; set; } - public string LEVY { get; set; } - public string VAT { get; set; } - public string NAC { get; set; } - public string SUR { get; set; } - public string ETLS { get; set; } - public string CISS { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/DocumentObject.cs b/Autumn.API/Contract/V1/Responses/DocumentObject.cs deleted file mode 100644 index 1414657..0000000 --- a/Autumn.API/Contract/V1/Responses/DocumentObject.cs +++ /dev/null @@ -1,29 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; - -namespace Autumn.API.Contract.V1.Responses -{ - public class DocumentObject - { - - public string Id { get; set; } - public int? Level { get; set; } - public string Parent { get; set; } - public string Code { get; set; } - public string Description { get; set; } - public string Country { get; set; } - public string Issuer { get; set; } - public string Validity { get; set; } - public string DurationForIssue { get; set; } - public string ApplicationForm { get; set; } - public string InspectionFee { get; set; } - public string PermitNew { get; set; } - public string PermitRenewal { get; set; } - public string LateRenewal { get; set; } - public string PnsupportingDocument { get; set; } - public string PrsupportingDocument { get; set; } - public string Remark { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/DutyResponse.cs b/Autumn.API/Contract/V1/Responses/DutyResponse.cs deleted file mode 100644 index 9e1e464..0000000 --- a/Autumn.API/Contract/V1/Responses/DutyResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class DutyResponse - { - public IEnumerable Error { get; set; } - public bool Success { get; set; } - - public string ProductDesc { get; set; } - public string HSCode { get; set; } - public decimal Cost { get; set; } - public decimal Freight { get; set; } - public decimal Insurance { get; set; } - public string Currency { get; set; } - public decimal ExRate { get; set; } - public decimal CF { get; set; } - public decimal CIF { get; set; } - public decimal CIFLocal { get; set; } - public string IDRate { get; set; } - public string VATRate { get; set; } - public string ETLRate { get; set; } - public string SURRate { get; set; } - public string CISSRate { get; set; } - public string NACRate { get; set; } - public string LEVYRate { get; set; } - public decimal IDPayableLocal { get; set; } - public decimal VATPayableLocal { get; set; } - public decimal ETLPayableLocal { get; set; } - public decimal SURPayableLocal { get; set; } - public decimal CISSPayable { get; set; } - public decimal CISSPayableLocal { get; set; } - public decimal NACPayable { get; set; } - public decimal NACPayableLocal { get; set; } - public decimal LEVYPayableLocal { get; set; } - public decimal TotalPayableLocal { get; set; } - public string HSCodeDescription { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/HSCodeObject.cs b/Autumn.API/Contract/V1/Responses/HSCodeObject.cs deleted file mode 100644 index 0828ff1..0000000 --- a/Autumn.API/Contract/V1/Responses/HSCodeObject.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - - public class HSCodeObject - { - public string PId { get; set; } - - public long Order { get; set; } - public long Level { get; set; } - - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Code { get; set; } - - public string ParentCode { get; set; } - public string Description { get; set; } - public string SelfExplanatory { get; set; } - - } -} diff --git a/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs b/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs deleted file mode 100644 index 817e82f..0000000 --- a/Autumn.API/Contract/V1/Responses/HscodeToDocumentObject.cs +++ /dev/null @@ -1,23 +0,0 @@ -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using System; -using System.Collections.Generic; - -namespace Autumn.API.Contract.V1.Responses -{ - public class HscodeToDocumentObject - { - public string Id { get; set; } - public string Country { get; set; } - public string Agency { get; set; } - public string Hscode { get; set; } - public string HscodeLocal { get; set; } - public string Description { get; set; } - public string ImpGeneral { get; set; } - public string ImpFinishedProductsInRetailPack { get; set; } - public string ImpBulkConsignments { get; set; } - public string ImpChemicalsOrRawMaterials { get; set; } - public string ImpSupermktOrRestaurant { get; set; } - public string ExpGeneral { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs b/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs deleted file mode 100644 index bae7d00..0000000 --- a/Autumn.API/Contract/V1/Responses/KeywordAPIResponsecs.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class KeywordAPIResponsecs - { - public string status { get; set; } - public string message { get; set; } - public string queryPrefix { get; set; } - public string fullQuery { get; set; } - public string query { get; set; } - public string type { get; set; } - public List results { get; set; } - - } - public class Terms - { - public string term { get; set; } - } -} diff --git a/Autumn.API/Contract/V1/Responses/KeywordResponse.cs b/Autumn.API/Contract/V1/Responses/KeywordResponse.cs deleted file mode 100644 index 352d252..0000000 --- a/Autumn.API/Contract/V1/Responses/KeywordResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class KeywordResponse - { - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - } - -} diff --git a/Autumn.API/Contract/V1/Responses/NoteResponse.cs b/Autumn.API/Contract/V1/Responses/NoteResponse.cs deleted file mode 100644 index dd904fd..0000000 --- a/Autumn.API/Contract/V1/Responses/NoteResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class NoteResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - public List Documents { get; set; } - public List Tariff { get; set; } - public List RecordsToDocuments { get; set; } - } - - -} diff --git a/Autumn.API/Contract/V1/Responses/SearchResponse.cs b/Autumn.API/Contract/V1/Responses/SearchResponse.cs deleted file mode 100644 index 950254e..0000000 --- a/Autumn.API/Contract/V1/Responses/SearchResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class SearchResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - } - -} diff --git a/Autumn.API/Contract/V1/Responses/TagsResult.cs b/Autumn.API/Contract/V1/Responses/TagsResult.cs deleted file mode 100644 index fd1dbe1..0000000 --- a/Autumn.API/Contract/V1/Responses/TagsResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V1.Responses -{ - public class TagsResult - { - - [JsonProperty("success")] - public bool Success { get; set; } - - [JsonProperty("results")] - public List Results { get; set; } - } - - public class Result - { - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("value")] - public string Value { get; set; } - - [JsonProperty("text")] - public string Text { get; set; } - } - -} diff --git a/Autumn.API/Contract/V2/ApiRoutes.cs b/Autumn.API/Contract/V2/ApiRoutes.cs deleted file mode 100644 index 1ab8848..0000000 --- a/Autumn.API/Contract/V2/ApiRoutes.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2 -{ - public static class ApiRoutes - { - public const string Root = "api"; - public const string Version = "v2"; - //public const string Base = Root + "/" + Version; - public const string Base = Version; - public static class Search - { - public const string Get = Base + "/search"; - } - } -} diff --git a/Autumn.API/Contract/V2/Requests/SearchRequest.cs b/Autumn.API/Contract/V2/Requests/SearchRequest.cs deleted file mode 100644 index 203fee7..0000000 --- a/Autumn.API/Contract/V2/Requests/SearchRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Requests -{ - public class SearchRequest - { - public string id { get; set; } - public string pid { get; set; } - public string level { get; set; } - public string keyword { get; set; } - public string settings { get; set; } - } -} diff --git a/Autumn.API/Contract/V2/Responses/HSCodeObject.cs b/Autumn.API/Contract/V2/Responses/HSCodeObject.cs deleted file mode 100644 index a78a870..0000000 --- a/Autumn.API/Contract/V2/Responses/HSCodeObject.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Responses -{ - - public class HSCodeObject - { - public string PId { get; set; } - - public long Order { get; set; } - public long Level { get; set; } - - public string Id { get; set; } - - public string ParentId { get; set; } - - public string Code { get; set; } - - public string ParentCode { get; set; } - public string Description { get; set; } - public string SelfExplanatory { get; set; } - - } -} diff --git a/Autumn.API/Contract/V2/Responses/ResultModel.cs b/Autumn.API/Contract/V2/Responses/ResultModel.cs deleted file mode 100644 index d4a89c9..0000000 --- a/Autumn.API/Contract/V2/Responses/ResultModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.Domain.Models; - -namespace Autumn.API.Contract.V2.Responses -{ - public class ResultModel - { - public List HSCodes { get; internal set; } - public string Prediction { get; internal set; } - public float Rating { get; internal set; } - public List Tags { get; internal set; } - public string Code { get; internal set; } - public List PHSCodes { get; internal set; } - } -} diff --git a/Autumn.API/Contract/V2/Responses/SearchResponse.cs b/Autumn.API/Contract/V2/Responses/SearchResponse.cs deleted file mode 100644 index 412352c..0000000 --- a/Autumn.API/Contract/V2/Responses/SearchResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Autumn.API.Contract.V2.Responses -{ - public class SearchResponse - { - - public IEnumerable Error { get; set; } - - public bool Success { get; set; } - - public List Records { get; set; } - public bool ai { get; set; } - } - -} diff --git a/Autumn.API/Controllers/V1/CodeListController.cs b/Autumn.API/Controllers/V1/CodeListController.cs deleted file mode 100644 index 5a3be2e..0000000 --- a/Autumn.API/Controllers/V1/CodeListController.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [ApiController] - public class CodeListController : ControllerBase - { - - private readonly CurrencyService _currencyService; - private readonly ProductService _productService; - - public CodeListController(CurrencyService currencyService, ProductService productService) - { - _currencyService = currencyService; - _productService = productService; - } - - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.CodeList.Currency)] - public async Task CurrencyAsync() - { - CurrencyResponse response = new CurrencyResponse(); - try - { - var currency = await _currencyService.GetAsync(); - response.Records = currency.Select(x => new CurrencyObject { CurrencyCode = x.CurrencyCode, Rate = x.Rate, TimeStamp = x.TimeStamp }).ToList(); - response.Success = true; - return Ok(response); - } - catch (Exception ex) - { - return BadRequest(new CurrencyResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - [HttpGet(ApiRoutes.CodeList.Tags)] - public async Task TagsAsync(string query = null) - { - - TagsResult tr = new TagsResult(); - try - { - List p = await _productService.GetByTagsAsync(query); - var tags = p.Select(x => x.Tags); - tr.Results = new List(); - foreach (var tagarr in tags) - { - foreach (var tag in tagarr) - { - - if (tr.Results.Any(x => x.Name != tag)) - { - Result r = new Result { Name = tag, Text = tag, Value = tag }; - tr.Results.Add(r); - } - else if (tr.Results.Count == 0) - { - Result r = new Result { Name = tag, Text = tag, Value = tag }; - tr.Results.Add(r); - } - } - } - tr.Success = true; - return new JsonResult(tr); - } - catch (Exception ex) - { - tr.Success = false; - return new JsonResult(tr); - } - - } - [HttpGet(ApiRoutes.CodeList.Products)] - public async Task ProductsAsync(string query = null) - { - - TagsResult tr = new TagsResult(); - try - { - List p = await _productService.GetLikeKeywordAsync(query); - // var tags = p.Select(x => x.Tags); - tr.Results = new List(); - - foreach (var tag in p) - { - if (tr.Results.Count == 0) - { - Result r = new Result { Name = tag.Keyword, Text = tag.Keyword, Value = tag.Code }; - tr.Results.Add(r); - } - else - { - if (tr.Results.Any(x => x.Name != tag.Keyword)) - { - Result r = new Result { Name = tag.Keyword, Text = tag.Keyword, Value = tag.Code }; - tr.Results.Add(r); - } - } - } - - tr.Success = true; - return new JsonResult(tr); - } - catch (Exception ex) - { - tr.Success = false; - return new JsonResult(tr); - } - - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/DutyController.cs b/Autumn.API/Controllers/V1/DutyController.cs deleted file mode 100644 index a5d5b00..0000000 --- a/Autumn.API/Controllers/V1/DutyController.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - [ApiController] - public class DutyController : ControllerBase - { - private readonly CustomsTariffService _tariffService; - private readonly CurrencyService _currencyService; - - public DutyController(CustomsTariffService tariffService, CurrencyService currencyService) - { - _tariffService = tariffService; - _currencyService = currencyService; - } - - - // [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Duty.Get)] - public async Task GetAsync([FromQuery] DutyRequest request) - { - try - { - DutyResponse response = new DutyResponse(); - if (ModelState.IsValid) - { - //Get HS Code Tariff - var tariff = _tariffService.GetByHSCode(request.HSCode); - var currency = _currencyService.GetByCurrency(request.Currency); - var cif = request.Cost + request.Insurance + request.Freight; - var cf = request.Cost + request.Freight; - response.ExRate = decimal.Parse(currency.Rate); - var duty = cif * (decimal.Parse(tariff.DUTY) / 100); - var vat = (cif + duty) * (decimal.Parse(tariff.VAT) / 100); - var sur = cif * (decimal.Parse(tariff.SUR) / 100); - var etl = cif * (decimal.Parse(tariff.ETLS) / 100); - var ciss = cif * (decimal.Parse(tariff.CISS) / 100); - var nac = cif * (decimal.Parse(tariff.NAC) / 100); - var levy = cif * (decimal.Parse(tariff.LEVY) / 100); - - - response.ProductDesc = request.ProductDesc; - response.HSCode = request.HSCode; - response.Cost = request.Cost; - response.Freight = request.Freight; - response.Insurance = request.Insurance; - response.Currency = request.Currency; - - response.CF = cf; - response.CIF = cif; - response.CIFLocal = cif * response.ExRate; - response.IDRate = tariff.DUTY; - response.IDPayableLocal = duty * response.ExRate; - response.VATRate = tariff.VAT; - response.VATPayableLocal = vat * response.ExRate; - response.ETLRate = tariff.ETLS; - response.ETLPayableLocal = etl * response.ExRate; - response.SURRate = tariff.SUR; - response.SURPayableLocal = sur * response.ExRate; - response.CISSRate = tariff.CISS; - response.CISSPayableLocal = ciss * response.ExRate; - response.NACRate = tariff.NAC; - response.NACPayableLocal = nac * response.ExRate; - response.LEVYRate = tariff.LEVY; - response.LEVYPayableLocal = levy * response.ExRate; - response.TotalPayableLocal = (duty + vat + sur + etl + ciss + nac + levy) * response.ExRate; - response.HSCodeDescription = tariff.Description; - - response.Success = true; - - return Ok(response); - } - else { - StringBuilder sb = new StringBuilder(); - foreach (var modelState in ModelState.Values) - { - foreach (var error in modelState.Errors) - { - sb.Append(error.ErrorMessage); - sb.AppendLine(); - sb.Append(error.Exception.Message); - } - } - return BadRequest(new DutyResponse { Success = false, Error = new[] { sb.ToString() } }); - } - } - catch (Exception ex) - { - return BadRequest(new DutyResponse { Success = false, Error = new[] { ex.Message } }); - } - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/IdentityController.cs b/Autumn.API/Controllers/V1/IdentityController.cs deleted file mode 100644 index 158640e..0000000 --- a/Autumn.API/Controllers/V1/IdentityController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [ApiController] - public class IdentityController : ControllerBase - { - private readonly IdentityService _identityService; - - public IdentityController(IdentityService identityService) { - _identityService = identityService; - } - - [HttpPost(ApiRoutes.Identity.Register)] - public async Task Register([FromForm] UserRequest request) - { - if (!ModelState.IsValid) - { - return BadRequest(new AuthFailedResponse { Error = ModelState.Values.SelectMany(x => x.Errors.Select(xx => xx.ErrorMessage)) }); - } - - var authResponse = _identityService.Register(request.Username, request.Password); - if (!authResponse.Success) - { - return BadRequest(new AuthFailedResponse - { - Error = authResponse.Errors - }); - } - return Ok(new AuthSuccessResponse { Token = authResponse.Token }); - } - - [HttpPost(ApiRoutes.Identity.Login)] - public IActionResult Login([FromForm] UserRequest request) - { - var authResponse = _identityService.Login(request.Username, request.Password); - if (!authResponse.Success) - { - return BadRequest(new AuthFailedResponse - { - Error = authResponse.Errors - }); - } - return Ok(new AuthSuccessResponse { Token = authResponse.Token }); - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/KeywordsController.cs b/Autumn.API/Controllers/V1/KeywordsController.cs deleted file mode 100644 index 1733011..0000000 --- a/Autumn.API/Controllers/V1/KeywordsController.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; -using RestSharp; -using Autumn.Domain.Models; -using Microsoft.AspNetCore.Authorization; - -namespace Autumn.API.V1 -{ - [Authorize] - //[Route("api/[controller]")] - [ApiController] - public class KeywordsController : ControllerBase - { - - private readonly KeywordService _keywordService; - private readonly ProductService _productService; - - public KeywordsController(KeywordService keywordService, ProductService productService) - { - _keywordService = keywordService; - _productService = productService; - } - - [HttpGet(ApiRoutes.Keyword.Get)] - public async Task GetAsync() - { - try - { - var products = await _productService.GetAsync(); - var keywords = await _keywordService.GetAsync(); - var remaining = products.Where(x=> !keywords.Select(s=>s.ParentKeyword).Contains(x.Keyword)).OrderBy(a=>a.Id); - - foreach (var r in remaining) - { - try - { - if (r.Keyword.Split().Count() < 10) - { - var client = new RestClient("https://uscensus.prod.3ceonline.com/ui/autocomplete"); - client.Timeout = -1; - var request = new RestRequest(Method.POST); - request.AddHeader("Content-Type", "application/json"); - request.AddParameter("application/json", "{\"query\":\"" + r.Keyword + "\"}", ParameterType.RequestBody); - IRestResponse response = client.Execute(request); - - var rep = JsonConvert.DeserializeObject(response.Content); - //Console.WriteLine(response.Content); - if (rep != null) - { - foreach (var term in rep.results) - { - _keywordService.Create(new Keyword { ParentKeyword = r.Keyword, ChildKeyword = term.term }); - } - } - - } - } - catch { } - } - return Ok(new KeywordResponse { Success = true, Error = new[] { "ok" } }); - } - catch (Exception ex) - { - return BadRequest(new KeywordResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/NoteController.cs b/Autumn.API/Controllers/V1/NoteController.cs deleted file mode 100644 index ececb4b..0000000 --- a/Autumn.API/Controllers/V1/NoteController.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - [ApiController] - public class NoteController : ControllerBase - { - - private readonly HSCodeService _hscodeService; - private readonly DocumentService _documentService; - private readonly HSCodeToDocumentService _hscodeToDocumentService; - private readonly CustomsTariffService _customsTariffService; - - public NoteController(HSCodeService hscodeService, DocumentService documentService, HSCodeToDocumentService hscodeToDocumentService, CustomsTariffService customsTariffService) - { - _hscodeService = hscodeService; - _documentService = documentService; - _hscodeToDocumentService = hscodeToDocumentService; - _customsTariffService = customsTariffService; - } - - [Authorize] - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Note.Get)] - public async Task GetAsync(string hscode) - { - try - { - var documentTask = _documentService.GetAsync(); - var hscodeTask = _hscodeService.GetWithHSCodeOptionsAsync(hscode, null, null); - var hsdocsTask = _hscodeToDocumentService.GetWithCodeAsync(hscode); - var tariff = _customsTariffService.GetByHeaderAsync(hscode); - - var hscodeList = await hscodeTask; - var hscodeToDocumentList = await hsdocsTask; - var documentList = await documentTask; - var tariffList = await tariff; - - var hscodeObj = hscodeList.Select(x => new HSCodeObject - { - Code = x.Code, - Description = x.Description, - Id = x.Id, - Level = x.Level, - Order = x.Order, - ParentCode = x.ParentCode, - ParentId = x.ParentId, - PId = x.PId, - SelfExplanatory = x.SelfExplanatory - }).ToList(); - - var hscodeToDocumentObj = hscodeToDocumentList.Select(x => new HscodeToDocumentObject - { - Agency = x.Agency, - Country = x.Country, - Description = x.Description, - ExpGeneral = x.ExpGeneral, - Hscode = x.Hscode, - HscodeLocal = x.HscodeLocal, - Id = x.Id, - ImpBulkConsignments = x.ImpBulkConsignments, - ImpChemicalsOrRawMaterials = x.ImpChemicalsOrRawMaterials, - ImpFinishedProductsInRetailPack = x.ImpFinishedProductsInRetailPack, - ImpGeneral = x.ImpGeneral, - ImpSupermktOrRestaurant = x.ImpSupermktOrRestaurant - }).ToList(); - - var documentObj = documentList.Select(x => new DocumentObject - { - ApplicationForm = x.ApplicationForm, - Code = x.Code, - Country = x.Country, - Description = x.Description, - DurationForIssue = x.DurationForIssue, - Id = x.Id, - InspectionFee = x.InspectionFee, - Issuer = x.Issuer, - LateRenewal = x.LateRenewal, - Level = x.Level, - Parent = x.Parent, - PermitNew = x.PermitNew, - PermitRenewal = x.PermitRenewal, - PnsupportingDocument = x.PnsupportingDocument, - PrsupportingDocument = x.PrsupportingDocument, - Remark = x.Remark, - Validity = x.Validity - }).ToList(); - - var tariffObj = tariffList.Select(x => new CustomsTariffObject - { - CISS = x.CISS, - Description = x.Description, - DUTY = x.DUTY, - ETLS = x.ETLS, - Header = x.Header, - HSCode = x.HSCode, - Id = x.Id, - LEVY = x.LEVY, - NAC = x.NAC, - SUR = x.SUR, - VAT = x.VAT - }).ToList(); - - return Ok(new NoteResponse { Success = true, Documents = documentObj, Records = hscodeObj, RecordsToDocuments = hscodeToDocumentObj, Tariff = tariffObj }); - } - catch (Exception ex) - { - return BadRequest(new NoteResponse { Success = false, Error = new[] { ex.Message } }); - } - } - } -} \ No newline at end of file diff --git a/Autumn.API/Controllers/V1/SearchController.cs b/Autumn.API/Controllers/V1/SearchController.cs deleted file mode 100644 index 4db2f62..0000000 --- a/Autumn.API/Controllers/V1/SearchController.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Autumn.API.Contract.V1; -using Autumn.API.Contract.V1.Requests; -using Autumn.API.Contract.V1.Responses; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Autumn_UIML.Model; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Autumn.API.V1 -{ - [Authorize] - // [Route("api/[controller]")] - [ApiController] - public class SearchController : ControllerBase - { - private readonly HSCodeService _hscodeService; - private readonly IPredict _predict; - - public SearchController(HSCodeService hscodeService, IPredict predict) - { - _hscodeService = hscodeService; - _predict = predict; - } - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - [HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - try - { - List hscodes = new List(); - if (string.IsNullOrEmpty(request.keyword)) - { - hscodes = await _hscodeService.GetWithOptionsAsync(request.id, request.pid, request.level); - } - else - { - //ProductDesc = productDesc; - var ai = _predict.GetHSCode(request.keyword); - var aiarr = ai.Prediction.Split('-'); - hscodes = await _hscodeService.GetWithHSCodeOptionsAsync(null, aiarr[1], null); - } - var records = hscodes.Select(x => new HSCodeObject - { - Code = x.Code, - Description = x.Description, - Id = x.Id, - Level = x.Level, - Order = x.Order, - ParentCode = x.ParentCode, - ParentId = x.ParentId, - PId = x.PId, - SelfExplanatory = x.SelfExplanatory - }).ToList(); - - return Ok(new SearchResponse { Success = true, Records = records }); - } - catch (Exception ex) - { - return BadRequest(new SearchResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - - } -} diff --git a/Autumn.API/Controllers/V2/SearchController.cs b/Autumn.API/Controllers/V2/SearchController.cs deleted file mode 100644 index b0a7aaa..0000000 --- a/Autumn.API/Controllers/V2/SearchController.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using Autumn.API.Contract.V2; -using Autumn.API.Contract.V2.Requests; -using Autumn.API.Contract.V2.Responses; -using Autumn.BL.Interface.V2; -using Autumn.BL.Models.Request.V2; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Autumn_UIML.Model; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; - -namespace Autumn.API.V2 -{ - [Authorize] - // [Route("api/[controller]")] - [ApiController] - public class SearchController : ControllerBase - { - private readonly HSCodeService _hscodeService; - private readonly IPredict _predict; - private readonly ProductService _productService; - private IConfiguration _configuration; - private readonly IClassification _classification; - private readonly IMapper _mapper; - - public SearchController(IConfiguration configuration, HSCodeService hscodeService, IPredict predict, ProductService productService, IClassification classification, IMapper mapper) - { - _hscodeService = hscodeService; - _predict = predict; - _productService = productService; - _configuration = configuration; - _classification = classification; - _mapper = mapper; - } - //[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - /*[HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - try - { - SearchResponse response = new SearchResponse { Success = true }; - ResultModel rm = new ResultModel(); - var records = new List(); - - //Do Navigation or Tag Query - rm.Prediction = string.Empty;// item.Key; - rm.Code = request.pid;// aiarr[1]; - rm.Rating = 0;// item.Value; - rm.Tags = new List(); - rm.PHSCodes = new List(); - rm.HSCodes = new List(); - - if (!string.IsNullOrEmpty(request.settings)) - { - if (request.settings == "nav") - { - rm.HSCodes = await _hscodeService.GetWithOptionsAsync(request.id, request.pid, request.level); - if (!string.IsNullOrEmpty(request.pid)) - rm.PHSCodes = await _hscodeService.GetWithOptionsAsync(request.pid, null, null); - - } - else if (request.settings == "tag") - { - rm.HSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(request.id, request.pid, request.level); - if (!string.IsNullOrEmpty(request.pid)) - rm.PHSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(request.pid, null, null); - } - - records.Add(rm); - - response.Records = records; - return Ok(response); - } - else - { - List products = await _productService.GetByKeywordAsync(request.keyword); - - var ctn = products.Count(x => x.Tags != null); - - if (products.Count > 0) - { - foreach (var product in products) - { - rm = new ResultModel(); - rm.Tags = new List(); - // var aiarr = product.Key.Split('-'); - rm.HSCodes = await _hscodeService.GetWithHSCodeOptionsAsync(product.Code, null, null); - //rm.HSCodes = Result2; - rm.Prediction = product.Keyword; - rm.Code = product.Code; - if (product.Tags != null) - rm.Tags.AddRange(product.Tags); - //rm.Rating = item.Value; - rm.PHSCodes = await _hscodeService.GetWithOptionsAsync(rm.HSCodes.FirstOrDefault().ParentId, null, null); - records.Add(rm); - if (ctn == 0) return Ok(new SearchResponse { Success = true, Records = records }); - } - - } - else if (products.Count == 0) - { - var ai = GetHSCode(request.keyword, double.Parse(_configuration["SiteSettings:Threshold"])); - if (ai.Count > 0) response.ai = true; - - rm = new ResultModel(); - rm.HSCodes = new List(); - foreach (var item in ai) - { - var aiarr = item.Key.Split('-'); - rm.HSCodes.AddRange(await _hscodeService.GetWithHSCodeOptionsAsync(aiarr[1], null, null)); - //rm.HSCodes = Result2; - //rm.Code = aiarr[1]; - //rm.Rating = item.Value; - } - - rm.Prediction = request.keyword; - rm.PHSCodes = new List(); - rm.Tags = new List(); - records.Add(rm); - } - response.Records = records; - return Ok(response); - } - } - catch (Exception ex) - { - return BadRequest(new SearchResponse { Success = false, Error = new[] { ex.Message } }); - } - } - - private Dictionary GetHSCode(string product, double threshold) - { - ModelInput data = new ModelInput - { - Keyword = product - }; - // Make a single prediction on the sample data and print results - Dictionary predictionResult = ConsumeModel.Predict(data, threshold); - - return predictionResult; - }*/ - [HttpGet(ApiRoutes.Search.Get)] - public async Task GetAsync([FromQuery] SearchRequest request) - { - - - SearchResponse response = new SearchResponse { Success = true }; - ResultModel rm = new ResultModel(); - var records = new List(); - var resquetMapped = _mapper.Map(request); - var resp = await _classification.SearchAsync(resquetMapped); - if (resp.Success) - return Ok(resp); - else - return BadRequest(resp); - - } - - } -} diff --git a/Autumn.API/Dto/ApiDtos.cs b/Autumn.API/Dto/ApiDtos.cs new file mode 100644 index 0000000..8f911d3 --- /dev/null +++ b/Autumn.API/Dto/ApiDtos.cs @@ -0,0 +1,200 @@ +namespace Autumn.API.Dto; + +// ── Search ────────────────────────────────────────────────────── + +public class SearchApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public Dictionary> Records { get; set; } = new(); +} + +public class SearchResultDto +{ + public List HSCodes { get; set; } = new(); + public List ParentHSCodes { get; set; } = new(); + public string Prediction { get; set; } = string.Empty; + public float Rating { get; set; } + public List Tags { get; set; } = new(); + public string Code { get; set; } = string.Empty; +} + +public class HSCodeDto +{ + public string Id { get; set; } = string.Empty; + public string ParentId { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string ParentCode { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? SelfExplanatory { get; set; } + public long Level { get; set; } + public long Order { get; set; } +} + +// ── Browse ────────────────────────────────────────────────────── + +public class BrowseApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); +} + +// ── Duty Calculator ───────────────────────────────────────────── + +public class DutyRequest +{ + public string ProductDesc { get; set; } = string.Empty; + public string HSCode { get; set; } = string.Empty; + public decimal Cost { get; set; } + public decimal Freight { get; set; } + public decimal Insurance { get; set; } + public string Currency { get; set; } = string.Empty; +} + +public class DutyApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + + // Input echo + public string ProductDesc { get; set; } = string.Empty; + public string HSCode { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + public decimal Cost { get; set; } + public decimal Freight { get; set; } + public decimal Insurance { get; set; } + public string Currency { get; set; } = string.Empty; + + // Calculated + public decimal CIF { get; set; } + + // Dynamic rate breakdown (works for any country) + public List Breakdown { get; set; } = new(); + public decimal TotalDuty { get; set; } + + public string HSCodeDescription { get; set; } = string.Empty; +} + +public class DutyLineItem +{ + public string Code { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; + public decimal Rate { get; set; } + public decimal Amount { get; set; } +} + +// ── Note ──────────────────────────────────────────────────────── + +public class NoteApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); + public List Documents { get; set; } = new(); + public List RecordsToDocuments { get; set; } = new(); + public List Tariff { get; set; } = new(); +} + +public class DocumentDto +{ + public string Id { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? Country { get; set; } + public string? Issuer { get; set; } + public string? Level { get; set; } + public string? Parent { get; set; } + public string? Validity { get; set; } + public string? DurationForIssue { get; set; } + public string? ApplicationForm { get; set; } + public string? InspectionFee { get; set; } + public string? PermitNew { get; set; } + public string? PermitRenewal { get; set; } + public string? LateRenewal { get; set; } + public string? PnsupportingDocument { get; set; } + public string? PrsupportingDocument { get; set; } + public string? Remark { get; set; } +} + +public class HSCodeToDocumentDto +{ + public string Id { get; set; } = string.Empty; + public string? Agency { get; set; } + public string? Country { get; set; } + public string? Hscode { get; set; } + public string? HscodeLocal { get; set; } + public string? Description { get; set; } + public string? ImpGeneral { get; set; } + public string? ImpFinishedProductsInRetailPack { get; set; } + public string? ImpBulkConsignments { get; set; } + public string? ImpChemicalsOrRawMaterials { get; set; } + public string? ImpSupermktOrRestaurant { get; set; } + public string? ExpGeneral { get; set; } +} + +public class CustomsTariffDto +{ + public string Id { get; set; } = string.Empty; + public string? Country { get; set; } + public string? Header { get; set; } + public string HSCode { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string? DUTY { get; set; } + public string? VAT { get; set; } + public string? LEVY { get; set; } + public string? NAC { get; set; } + public string? SUR { get; set; } + public string? ETLS { get; set; } + public string? CISS { get; set; } + public string? NHIL { get; set; } + public string? GETFUND { get; set; } + public string? IDF { get; set; } + public string? RDF { get; set; } +} + +// ── CodeList ──────────────────────────────────────────────────── + +public class CurrencyApiResponse +{ + public bool Success { get; set; } + public IEnumerable? Error { get; set; } + public List Records { get; set; } = new(); +} + +public class CurrencyDto +{ + public string CurrencyCode { get; set; } = string.Empty; + public string Rate { get; set; } = string.Empty; + public string? TimeStamp { get; set; } +} + +public class TagsApiResponse +{ + public bool Success { get; set; } + public List Results { get; set; } = new(); +} + +public class TagResult +{ + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; +} + +// ── Countries ────────────────────────────────────────────────── + +public class CountryApiResponse +{ + public bool Success { get; set; } + public List Records { get; set; } = new(); +} + +public class CountryDto +{ + public string Code { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Flag { get; set; } = string.Empty; + public string Currency { get; set; } = string.Empty; + public string Symbol { get; set; } = string.Empty; +} diff --git a/Autumn.API/Endpoints/AdminEndpoints.cs b/Autumn.API/Endpoints/AdminEndpoints.cs new file mode 100644 index 0000000..fe092df --- /dev/null +++ b/Autumn.API/Endpoints/AdminEndpoints.cs @@ -0,0 +1,153 @@ +using Autumn.API.Dto; +using Autumn.Domain.Models; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class AdminEndpoints +{ + public static void MapAdminEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/admin") + .WithTags("Admin") + .RequireAuthorization(); + + // Dashboard + group.MapGet("/dashboard", GetDashboard); + + // Products CRUD + group.MapGet("/products", GetProducts); + group.MapGet("/products/{id}", GetProduct); + group.MapPost("/products", CreateProduct); + group.MapPut("/products/{id}", UpdateProduct); + group.MapDelete("/products/{id}", DeleteProduct); + + // HS Codes CRUD + group.MapGet("/codes", GetCodes); + group.MapGet("/codes/{id}", GetCode); + group.MapPut("/codes/{id}", UpdateCode); + + // Tariffs CRUD + group.MapGet("/tariffs", GetTariffs); + group.MapGet("/tariffs/{id}", GetTariff); + group.MapPost("/tariffs", CreateTariff); + group.MapPut("/tariffs/{id}", UpdateTariff); + group.MapDelete("/tariffs/{id}", DeleteTariff); + + // Query Logs + group.MapGet("/querylogs", GetQueryLogs); + } + + // ── Dashboard ─────────────────────────────────────────────── + + private static async Task GetDashboard( + IProductService productService, + IHsCodeService hsCodeService, + ICustomsTariffService tariffService) + { + var products = await productService.GetAsync(); + var codes = await hsCodeService.GetAsync(); + var tariffs = await tariffService.GetAsync(); + + return Results.Ok(new + { + ProductCount = products.Count, + HSCodeCount = codes.Count, + TariffCount = tariffs.Count + }); + } + + // ── Products ──────────────────────────────────────────────── + + private static async Task GetProducts(IProductService productService) + { + var products = await productService.GetAsync(); + return Results.Ok(products); + } + + private static async Task GetProduct(IProductService productService, string id) + { + var product = await productService.GetAsync(id); + return product is null ? Results.NotFound() : Results.Ok(product); + } + + private static async Task CreateProduct(IProductService productService, Product product) + { + var created = await productService.CreateAsync(product); + return Results.Created($"/api/admin/products/{created.Id}", created); + } + + private static async Task UpdateProduct(IProductService productService, string id, Product product) + { + await productService.UpdateAsync(id, product); + return Results.NoContent(); + } + + private static async Task DeleteProduct(IProductService productService, string id) + { + await productService.RemoveAsync(id); + return Results.NoContent(); + } + + // ── HS Codes ──────────────────────────────────────────────── + + private static async Task GetCodes(IHsCodeService hsCodeService) + { + var codes = await hsCodeService.GetAsync(); + return Results.Ok(codes); + } + + private static async Task GetCode(IHsCodeService hsCodeService, string id) + { + var code = await hsCodeService.GetAsync(id); + return code is null ? Results.NotFound() : Results.Ok(code); + } + + private static async Task UpdateCode(IHsCodeService hsCodeService, string id, HSCode code) + { + await hsCodeService.UpdateAsync(id, code); + return Results.NoContent(); + } + + // ── Tariffs ───────────────────────────────────────────────── + + private static async Task GetTariffs(ICustomsTariffService tariffService, string? country = null) + { + var tariffs = await tariffService.GetAsync(); + if (!string.IsNullOrEmpty(country)) + tariffs = tariffs.Where(t => t.Country == country || (t.Country == null && country == "NG")).ToList(); + return Results.Ok(tariffs); + } + + private static async Task GetTariff(ICustomsTariffService tariffService, string id) + { + var tariff = await tariffService.GetAsync(id); + return tariff is null ? Results.NotFound() : Results.Ok(tariff); + } + + private static async Task CreateTariff(ICustomsTariffService tariffService, CustomsTariff tariff) + { + var created = await tariffService.CreateAsync(tariff); + return Results.Created($"/api/admin/tariffs/{created.Id}", created); + } + + private static async Task UpdateTariff(ICustomsTariffService tariffService, string id, CustomsTariff tariff) + { + await tariffService.UpdateAsync(id, tariff); + return Results.NoContent(); + } + + private static async Task DeleteTariff(ICustomsTariffService tariffService, string id) + { + await tariffService.RemoveAsync(id); + return Results.NoContent(); + } + + // ── Query Logs ────────────────────────────────────────────── + + private static async Task GetQueryLogs(ISearchLogService searchLogService) + { + var logs = await searchLogService.GetAsync(); + return Results.Ok(logs); + } +} diff --git a/Autumn.API/Endpoints/BrowseEndpoints.cs b/Autumn.API/Endpoints/BrowseEndpoints.cs new file mode 100644 index 0000000..446fa86 --- /dev/null +++ b/Autumn.API/Endpoints/BrowseEndpoints.cs @@ -0,0 +1,55 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class BrowseEndpoints +{ + public static void MapBrowseEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/browse") + .WithTags("Browse") + .AllowAnonymous(); + + group.MapGet("/", Browse); + } + + private static async Task Browse( + IHsCodeService hsCodeService, + string? code = null, + string? parentCode = null, + string? parentId = null, + string? level = null) + { + try + { + List hscodes; + + if (!string.IsNullOrEmpty(parentId)) + { + // ID-based navigation (matches Razor page pattern) + hscodes = await hsCodeService.GetWithOptionsAsync(null, parentId, level); + } + else + { + hscodes = await hsCodeService.GetWithHSCodeOptionsAsync(code, parentCode, level); + } + + var records = hscodes.Select(SearchEndpoints.MapHSCode).ToList(); + + return Results.Ok(new BrowseApiResponse + { + Success = true, + Records = records + }); + } + catch (Exception ex) + { + return Results.BadRequest(new BrowseApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } +} diff --git a/Autumn.API/Endpoints/CodeListEndpoints.cs b/Autumn.API/Endpoints/CodeListEndpoints.cs new file mode 100644 index 0000000..4eaa6b3 --- /dev/null +++ b/Autumn.API/Endpoints/CodeListEndpoints.cs @@ -0,0 +1,108 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class CodeListEndpoints +{ + public static void MapCodeListEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/codelist") + .WithTags("Reference Data") + .AllowAnonymous(); + + group.MapGet("/countries", GetCountries); + group.MapGet("/currency", GetCurrencies); + group.MapGet("/products/{query?}", GetProducts); + group.MapGet("/tags/{query?}", GetTags); + } + + private static async Task GetCountries(ICountryService countryService) + { + var countries = await countryService.GetAsync(); + return Results.Ok(new CountryApiResponse + { + Success = true, + Records = countries.Select(c => new CountryDto + { + Code = c.Code ?? string.Empty, + Name = c.Name ?? string.Empty, + Flag = c.Flag ?? string.Empty, + Currency = c.Currency ?? string.Empty, + Symbol = c.Symbol ?? string.Empty + }).ToList() + }); + } + + private static async Task GetCurrencies(ICurrencyService currencyService) + { + try + { + var currencies = await currencyService.GetAsync(); + return Results.Ok(new CurrencyApiResponse + { + Success = true, + Records = currencies.Select(c => new CurrencyDto + { + CurrencyCode = c.CurrencyCode ?? string.Empty, + Rate = c.Rate.ToString(), + TimeStamp = c.TimeStamp.ToString("o") + }).ToList() + }); + } + catch (Exception ex) + { + return Results.BadRequest(new CurrencyApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + private static async Task GetProducts(IProductService productService, string? query = null) + { + try + { + var products = string.IsNullOrEmpty(query) + ? await productService.GetAsync() + : await productService.GetLikeKeywordAsync(query); + + var results = products.Select(p => new TagResult + { + Name = p.Keyword ?? string.Empty, + Value = p.Code ?? string.Empty, + Text = p.Keyword ?? string.Empty + }).ToList(); + + return Results.Ok(new TagsApiResponse { Success = true, Results = results }); + } + catch (Exception ex) + { + return Results.BadRequest(new TagsApiResponse { Success = false }); + } + } + + private static async Task GetTags(IProductService productService, string? query = null) + { + try + { + var products = string.IsNullOrEmpty(query) + ? await productService.GetAsync() + : await productService.GetByTagsAsync(query); + + var results = products.Select(p => new TagResult + { + Name = p.Keyword ?? string.Empty, + Value = p.Code ?? string.Empty, + Text = p.Keyword ?? string.Empty + }).ToList(); + + return Results.Ok(new TagsApiResponse { Success = true, Results = results }); + } + catch (Exception ex) + { + return Results.BadRequest(new TagsApiResponse { Success = false }); + } + } +} diff --git a/Autumn.API/Endpoints/DutyEndpoints.cs b/Autumn.API/Endpoints/DutyEndpoints.cs new file mode 100644 index 0000000..8c9c23e --- /dev/null +++ b/Autumn.API/Endpoints/DutyEndpoints.cs @@ -0,0 +1,127 @@ +using Autumn.API.Dto; +using Autumn.Domain.Models; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class DutyEndpoints +{ + private static readonly Dictionary RateLabels = new() + { + ["DUTY"] = "Import Duty", + ["VAT"] = "VAT", + ["LEVY"] = "Levy", + ["SUR"] = "Surcharge", + ["ETLS"] = "ETL", + ["CISS"] = "CISS", + ["NAC"] = "NAC", + ["NHIL"] = "NHIL", + ["GETFUND"] = "GETFund", + ["IDF"] = "IDF", + ["RDF"] = "RDF" + }; + + public static void MapDutyEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/duty") + .WithTags("Duty Calculator") + .AllowAnonymous(); + + group.MapGet("/", CalculateDuty); + } + + private static async Task CalculateDuty( + ICustomsTariffService tariffService, + string HSCode, + string Country = "NG", + string ProductDesc = "", + decimal Cost = 0, + decimal Freight = 0, + decimal Insurance = 0, + string Currency = "USD") + { + try + { + // Try exact HSCode match first, then fall back to Header match + var tariff = await tariffService.GetByHSCodeAndCountryAsync(HSCode, Country); + if (tariff == null) + { + var headerMatches = await tariffService.GetByHeaderAndCountryAsync(HSCode, Country); + tariff = headerMatches.FirstOrDefault(); + } + if (tariff == null) + return Results.BadRequest(new DutyApiResponse + { + Success = false, + Error = new[] { $"No tariff found for HS Code: {HSCode} in country: {Country}" } + }); + + var cif = Cost + Insurance + Freight; + + // Build dynamic breakdown from all non-null/non-zero rate fields + var breakdown = new List(); + decimal dutyAmount = 0; // track import duty for VAT base calculation + + foreach (var (code, rateStr) in GetRateFields(tariff)) + { + if (!decimal.TryParse(rateStr, out var rate) || rate == 0) + continue; + + // VAT is typically calculated on CIF + Import Duty + var baseAmount = code == "VAT" ? cif + dutyAmount : cif; + var amount = baseAmount * (rate / 100); + + if (code == "DUTY") + dutyAmount = amount; + + breakdown.Add(new DutyLineItem + { + Code = code, + Label = RateLabels.GetValueOrDefault(code, code), + Rate = rate, + Amount = Math.Round(amount, 2) + }); + } + + return Results.Ok(new DutyApiResponse + { + Success = true, + ProductDesc = ProductDesc, + HSCode = HSCode, + Country = Country, + Cost = Cost, + Freight = Freight, + Insurance = Insurance, + Currency = Currency, + CIF = cif, + Breakdown = breakdown, + TotalDuty = Math.Round(breakdown.Sum(b => b.Amount), 2), + HSCodeDescription = tariff.Description ?? string.Empty + }); + } + catch (Exception ex) + { + return Results.BadRequest(new DutyApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + private static IEnumerable<(string Code, string? Rate)> GetRateFields(CustomsTariff tariff) + { + // Yield DUTY first so its amount is available for VAT base calculation + yield return ("DUTY", tariff.DUTY); + yield return ("VAT", tariff.VAT); + yield return ("LEVY", tariff.LEVY); + yield return ("SUR", tariff.SUR); + yield return ("ETLS", tariff.ETLS); + yield return ("CISS", tariff.CISS); + yield return ("NAC", tariff.NAC); + yield return ("NHIL", tariff.NHIL); + yield return ("GETFUND", tariff.GETFUND); + yield return ("IDF", tariff.IDF); + yield return ("RDF", tariff.RDF); + } +} diff --git a/Autumn.API/Endpoints/NoteEndpoints.cs b/Autumn.API/Endpoints/NoteEndpoints.cs new file mode 100644 index 0000000..d2bf5d1 --- /dev/null +++ b/Autumn.API/Endpoints/NoteEndpoints.cs @@ -0,0 +1,109 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; + +namespace Autumn.API.Endpoints; + +public static class NoteEndpoints +{ + public static void MapNoteEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/note") + .WithTags("Notes") + .AllowAnonymous(); + + group.MapGet("/{hscode}", GetNote); + } + + private static async Task GetNote( + IHsCodeService hsCodeService, + IDocumentService documentService, + IHsCodeDocumentService hsCodeDocumentService, + ICustomsTariffService tariffService, + string hscode, + string? country = null) + { + try + { + // Run queries in parallel + var hscodeTask = hsCodeService.GetWithHSCodeOptionsAsync(hscode, null, null); + var documentTask = documentService.GetAsync(); + var hsdocsTask = hsCodeDocumentService.GetWithCodeAsync(hscode); + var tariffTask = tariffService.GetByHeaderAndCountryAsync(hscode, country ?? "NG"); + + await Task.WhenAll(hscodeTask, documentTask, hsdocsTask, tariffTask); + + var hscodeList = await hscodeTask; + var documentList = await documentTask; + var hscodeToDocumentList = await hsdocsTask; + var tariffList = await tariffTask; + + return Results.Ok(new NoteApiResponse + { + Success = true, + Records = hscodeList.Select(SearchEndpoints.MapHSCode).ToList(), + Documents = documentList.Select(x => new DocumentDto + { + Id = x.Id ?? string.Empty, + Code = x.Code ?? string.Empty, + Description = x.Description ?? string.Empty, + Country = x.Country, + Issuer = x.Issuer, + Level = x.Level?.ToString(), + Parent = x.Parent, + Validity = x.Validity, + DurationForIssue = x.DurationForIssue, + ApplicationForm = x.ApplicationForm, + InspectionFee = x.InspectionFee, + PermitNew = x.PermitNew, + PermitRenewal = x.PermitRenewal, + LateRenewal = x.LateRenewal, + PnsupportingDocument = x.PnsupportingDocument, + PrsupportingDocument = x.PrsupportingDocument, + Remark = x.Remark + }).ToList(), + RecordsToDocuments = hscodeToDocumentList.Select(x => new HSCodeToDocumentDto + { + Id = x.Id ?? string.Empty, + Agency = x.Agency, + Country = x.Country, + Hscode = x.Hscode, + HscodeLocal = x.HscodeLocal, + Description = x.Description, + ImpGeneral = x.ImpGeneral, + ImpFinishedProductsInRetailPack = x.ImpFinishedProductsInRetailPack, + ImpBulkConsignments = x.ImpBulkConsignments, + ImpChemicalsOrRawMaterials = x.ImpChemicalsOrRawMaterials, + ImpSupermktOrRestaurant = x.ImpSupermktOrRestaurant, + ExpGeneral = x.ExpGeneral + }).ToList(), + Tariff = tariffList.Select(x => new CustomsTariffDto + { + Id = x.Id ?? string.Empty, + Country = x.Country, + Header = x.Header, + HSCode = x.HSCode ?? string.Empty, + Description = x.Description ?? string.Empty, + DUTY = x.DUTY, + VAT = x.VAT, + LEVY = x.LEVY, + NAC = x.NAC, + SUR = x.SUR, + ETLS = x.ETLS, + CISS = x.CISS, + NHIL = x.NHIL, + GETFUND = x.GETFUND, + IDF = x.IDF, + RDF = x.RDF + }).ToList() + }); + } + catch (Exception ex) + { + return Results.BadRequest(new NoteApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } +} diff --git a/Autumn.API/Endpoints/SearchEndpoints.cs b/Autumn.API/Endpoints/SearchEndpoints.cs new file mode 100644 index 0000000..53a8c24 --- /dev/null +++ b/Autumn.API/Endpoints/SearchEndpoints.cs @@ -0,0 +1,90 @@ +using Autumn.API.Dto; +using Autumn.Service.Interface; +using Autumn.BL.Models.Request.V3; + +namespace Autumn.API.Endpoints; + +public static class SearchEndpoints +{ + public static void MapSearchEndpoints(this IEndpointRouteBuilder endpoints) + { + var group = endpoints.MapGroup("/api/search") + .WithTags("Search") + .AllowAnonymous(); + + group.MapGet("/", Search); + } + + private static async Task Search( + IClassification classification, + string? keyword = null, + string? id = null, + string? pid = null, + string? level = null, + string? settings = null) + { + try + { + var request = new BLSearchRequest + { + id = id, + pid = pid, + level = level, + keyword = keyword, + settings = settings + }; + + var resp = await classification.SearchAsync(request); + + if (!resp.Success) + return Results.BadRequest(new SearchApiResponse + { + Success = false, + Error = resp.Error + }); + + // Map BLSearchResponse to clean API response + var response = new SearchApiResponse { Success = true }; + + if (resp.Records != null) + { + foreach (var kvp in resp.Records) + { + var results = kvp.Value.Select(r => new SearchResultDto + { + Prediction = r.Prediction ?? string.Empty, + Rating = r.Rating, + Code = r.Code ?? string.Empty, + Tags = r.Tags ?? new List(), + HSCodes = r.HSCodes?.Select(MapHSCode).ToList() ?? new List(), + ParentHSCodes = r.PHSCodes?.Select(MapHSCode).ToList() ?? new List() + }).ToList(); + + response.Records[kvp.Key] = results; + } + } + + return Results.Ok(response); + } + catch (Exception ex) + { + return Results.BadRequest(new SearchApiResponse + { + Success = false, + Error = new[] { ex.Message } + }); + } + } + + internal static HSCodeDto MapHSCode(Autumn.Domain.Models.HSCode x) => new() + { + Id = x.Id ?? string.Empty, + ParentId = x.ParentId ?? string.Empty, + Code = x.Code ?? string.Empty, + ParentCode = x.ParentCode ?? string.Empty, + Description = x.Description ?? string.Empty, + SelfExplanatory = x.SelfExplanatory, + Level = x.Level, + Order = x.Order + }; +} diff --git a/Autumn.API/Profiles/SearchRequestProfile.cs b/Autumn.API/Profiles/SearchRequestProfile.cs deleted file mode 100644 index 9a6b12b..0000000 --- a/Autumn.API/Profiles/SearchRequestProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using AutoMapper; -using Autumn.API.Contract.V2.Requests; -using Autumn.BL.Models.Request.V2; - -namespace Autumn.API.Profiles -{ - public class SearchRequestProfile : Profile - { - public SearchRequestProfile() - { - CreateMap(); - } - } -} diff --git a/Autumn.API/Program.cs b/Autumn.API/Program.cs index 67865c1..bd7ccd7 100644 --- a/Autumn.API/Program.cs +++ b/Autumn.API/Program.cs @@ -1,25 +1,138 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Autumn.API +using System.Threading.RateLimiting; +using Autumn.API.Endpoints; +using Autumn.Domain.Models; +using Autumn.Infrastructure; +using Autumn.Service; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.RateLimiting; +using MongoDB.Driver; + +var builder = WebApplication.CreateBuilder(args); +var configuration = builder.Configuration; + +// ── MongoDB & SQL database services ───────────────────────────── +builder.Services.AddDocumentDatabaseServices(configuration); +builder.Services.AddRelationalDatabaseServices(configuration); + +// ── Repository & Business services ────────────────────────────── +builder.Services.AddRepositoryServices(); +builder.Services.AddBusinessServices(); + +// ── CORS ──────────────────────────────────────────────────────── +builder.Services.AddCors(options => { - public class Program + options.AddPolicy("AllowSPA", policy => { - public static void Main(string[] args) - { - Console.Title = "Autumn API"; - CreateWebHostBuilder(args).Build().Run(); - } + var origins = configuration.GetSection("Cors:AllowedOrigins").Get() + ?? new[] { "http://localhost:5173", "http://localhost:5174" }; + + policy.WithOrigins(origins) + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +// ── Rate Limiting ──────────────────────────────────────────────── +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = 429; + options.GlobalLimiter = PartitionedRateLimiter.Create(ctx => + RateLimitPartition.GetFixedWindowLimiter( + ctx.Connection.RemoteIpAddress?.ToString() ?? "anonymous", + _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 30, + Window = TimeSpan.FromSeconds(60), + QueueLimit = 0 + })); +}); + +// ── Auth0 JWT Authentication ──────────────────────────────────── +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var domain = configuration["Auth0:Domain"] ?? ""; + if (!domain.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + domain = $"https://{domain}"; + options.Authority = domain; + options.Audience = configuration["Auth0:Audience"]; + }); + +builder.Services.AddAuthorization(); + +// ── OpenAPI / Swagger ─────────────────────────────────────────── +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "HS Codes API", + Version = "v1", + Description = "HS Commodity Classification & Duty Calculator API" + }); +}); + +var app = builder.Build(); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup(); +// ── Seed ──────────────────────────────────────────────────────── +using (var scope = app.Services.CreateScope()) +{ + var settings = scope.ServiceProvider.GetRequiredService(); + var client = new MongoClient(settings.ConnectionString); + var db = client.GetDatabase(settings.DatabaseName); + + // Set Country = "NG" on existing tariff records that have no country + var tariffs = db.GetCollection(settings.CustomsTariffStoreCollectionName); + var filter = Builders.Filter.Eq(t => t.Country, null); + var update = Builders.Update.Set(t => t.Country, "NG"); + var result = await tariffs.UpdateManyAsync(filter, update); + if (result.ModifiedCount > 0) + app.Logger.LogInformation("Seed: Updated {Count} tariff records with Country = 'NG'", result.ModifiedCount); + + // Seed countries collection if empty + var countries = db.GetCollection(settings.CountryStoreCollectionName); + var countryCount = await countries.CountDocumentsAsync(Builders.Filter.Empty); + if (countryCount == 0) + { + var seedCountries = new List + { + new() { Code = "NG", Name = "Nigeria", Flag = "\U0001F1F3\U0001F1EC", Currency = "NGN", Symbol = "\u20A6" }, + new() { Code = "GH", Name = "Ghana", Flag = "\U0001F1EC\U0001F1ED", Currency = "GHS", Symbol = "GH\u20B5" }, + new() { Code = "KE", Name = "Kenya", Flag = "\U0001F1F0\U0001F1EA", Currency = "KES", Symbol = "KSh" }, + new() { Code = "ZA", Name = "South Africa", Flag = "\U0001F1FF\U0001F1E6", Currency = "ZAR", Symbol = "R" }, + new() { Code = "GB", Name = "United Kingdom", Flag = "\U0001F1EC\U0001F1E7", Currency = "GBP", Symbol = "\u00A3" }, + }; + await countries.InsertManyAsync(seedCountries); + app.Logger.LogInformation("Seed: Inserted {Count} countries", seedCountries.Count); } } + +// ── Middleware pipeline ───────────────────────────────────────── +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "HS Codes API v1")); +} + +app.UseCors("AllowSPA"); +app.UseRateLimiter(); +app.UseAuthentication(); +app.UseAuthorization(); +app.UseStaticFiles(); + +// ── Map endpoints ─────────────────────────────────────────────── +app.MapSearchEndpoints(); +app.MapBrowseEndpoints(); +app.MapDutyEndpoints(); +app.MapNoteEndpoints(); +app.MapCodeListEndpoints(); +app.MapAdminEndpoints(); + +// ── SPA fallback (serves index.html for client-side routes) ───── +if (!app.Environment.IsDevelopment()) +{ + app.MapFallbackToFile("index.html"); +} + +app.Run(); diff --git a/Autumn.API/Properties/launchSettings.json b/Autumn.API/Properties/launchSettings.json index d70d56b..36d1032 100644 --- a/Autumn.API/Properties/launchSettings.json +++ b/Autumn.API/Properties/launchSettings.json @@ -1,30 +1,23 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:5003", - "sslPort": 0 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", + "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "http": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "api/values", + "applicationUrl": "http://localhost:5174", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Autumn.API": { + "https": { "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "api/values", + "applicationUrl": "https://localhost:7235;http://localhost:5174", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5001" + } } } -} \ No newline at end of file +} diff --git a/Autumn.API/Startup.cs b/Autumn.API/Startup.cs deleted file mode 100644 index 98e3c2a..0000000 --- a/Autumn.API/Startup.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; -using Autumn.BL.Interface.V2; -using Autumn.BL.Services.V2; -using Autumn.Domain.Data; -using Autumn.Domain.Infra; -using Autumn.Domain.Models; -using Autumn.Domain.Services; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; - -namespace Autumn.API -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - - //services.AddDbContext(options => - // options.UseSqlServer( - // Configuration.GetConnectionString("DefaultConnection"))); - services.Configure( - Configuration.GetSection(nameof(StoreDatabaseSettings))); - - services.AddSingleton(sp => - sp.GetRequiredService>().Value); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - //services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - services.AddMvcCore() - .AddAuthorization() - .AddJsonFormatters(); - - services.AddAutoMapper(typeof(Startup)); - - - services.AddAuthentication("Bearer") - .AddJwtBearer("Bearer", options => - { - options.Authority = Configuration["SiteSettings:SSOURL"]; - options.RequireHttpsMetadata = false; - - options.Audience = "autumnapi"; - }); - - services.AddCors(options => - { - // this defines a CORS policy called "default" - options.AddPolicy("default", policy => - { - policy.WithOrigins("http://localhost:5003") - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - // Register the Swagger generator, defining 1 or more Swagger documents - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "HS Codes", - Description = "HS Commodity Classification API", - TermsOfService = new Uri("https://example.com/terms"), - Contact = new OpenApiContact - { - Name = "Support", - Email = string.Empty, - Url = new Uri("https://example.com/support"), - }, - License = new OpenApiLicense - { - Name = "Use under LICX", - Url = new Uri("https://example.com/license"), - } - }); - //First we define the security scheme - c.AddSecurityDefinition("Bearer", //Name the security scheme - new OpenApiSecurityScheme - { - Description = "JWT Authorization header using the Bearer scheme.", - Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication - Scheme = "bearer" //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer". - }); - - c.AddSecurityRequirement(new OpenApiSecurityRequirement{ - { - new OpenApiSecurityScheme{ - Reference = new OpenApiReference{ - Id = "Bearer", //The name of the previously defined security scheme. - Type = ReferenceType.SecurityScheme - } - },new List() - } - }); - }); - - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - app.UseHttpsRedirection(); - app.UseCors("default"); - app.UseAuthentication(); - app.UseMvc(); - - // Enable middleware to serve generated Swagger as a JSON endpoint. - app.UseSwagger(); - - // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), - // specifying the Swagger JSON endpoint. - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Autumn API V1"); - }); - } - } -} diff --git a/Autumn.API/appsettings.Development.json b/Autumn.API/appsettings.Development.json deleted file mode 100644 index dfc6fd5..0000000 --- a/Autumn.API/appsettings.Development.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "SiteSettings": { - "Threshold": "0.02", - "SSOURL": "http://localhost:5000" - }, - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} diff --git a/Autumn.API/appsettings.Production.json b/Autumn.API/appsettings.Production.json new file mode 100644 index 0000000..415a834 --- /dev/null +++ b/Autumn.API/appsettings.Production.json @@ -0,0 +1,43 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "", + "Audience": "" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "https://hs.codes", + "https://www.hs.codes" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Autumn.API/appsettings.Staging.json b/Autumn.API/appsettings.Staging.json new file mode 100644 index 0000000..614a8de --- /dev/null +++ b/Autumn.API/appsettings.Staging.json @@ -0,0 +1,42 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "", + "Audience": "" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "http://hs.codes:8443" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Autumn.API/appsettings.example.json b/Autumn.API/appsettings.example.json new file mode 100644 index 0000000..5147f5f --- /dev/null +++ b/Autumn.API/appsettings.example.json @@ -0,0 +1,44 @@ +{ + "StoreDatabaseSettings": { + "HSCodeStoreCollectionName": "hscodes", + "ProductStoreCollectionName": "products", + "Product2StoreCollectionName": "Products2", + "KeywordStoreCollectionName": "keywords", + "SearchLogStoreCollectionName": "SearchLog", + "DocumentStoreCollectionName": "Documents", + "CurrencyStoreCollectionName": "currencies", + "IdentityStoreCollectionName": "Identities", + "CustomsTariffStoreCollectionName": "tariffs", + "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", + "RequirementStoreCollectionName": "requirements", + "CountryStoreCollectionName": "countries", + "ConnectionString": "mongodb+srv://:@.mongodb.net/", + "DatabaseName": "ClassificationDb" + }, + "ConnectionStrings": { + "DefaultConnection": "" + }, + "Auth0": { + "Domain": "https://your-auth0-domain.auth0.com/", + "Audience": "autumnapi" + }, + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "", + "GroqModel": "llama-3.1-8b-instant" + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:5173", + "http://localhost:5174", + "http://localhost:5003" + ] + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Autumn.API/appsettings.json b/Autumn.API/appsettings.json deleted file mode 100644 index cad529c..0000000 --- a/Autumn.API/appsettings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "StoreDatabaseSettings": { - "HSCodeStoreCollectionName": "HSCodes", - "ProductStoreCollectionName": "Products", - "KeywordStoreCollectionName": "Keyword", - "SearchLogStoreCollectionName": "SearchLog", - "DocumentStoreCollectionName": "Documents", - "CurrencyStoreCollectionName": "Currencies", - "IdentityStoreCollectionName": "Identities", - "CustomsTariffStoreCollectionName": "CustomsTariff", - "HSCodeToDocumentStoreCollectionName": "HSCodeToDocuments", - "ConnectionString": "mongodb://localhost:27017", - "DatabaseName": "ClassificationDb" - }, - "ConnectionStrings": { - //"DefaultConnection": "Server=(local)\\SAMABOS;Database=classification;Trusted_Connection=True;MultipleActiveResultSets=true" - - "DefaultConnection": "Server=198.38.83.33;Database=Uthman_avvs;User ID=uthman_tradehubuser;Password=Dem@ter1al!ze7;Trusted_Connection=False;" - }, - "SiteSettings": { - "Threshold": "0.02", - "SSOURL": "http://104.154.117.94:5000" - }, - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/Autumn.BL/Autumn.Service.csproj b/Autumn.BL/Autumn.Service.csproj index 6798169..dae27cc 100644 --- a/Autumn.BL/Autumn.Service.csproj +++ b/Autumn.BL/Autumn.Service.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/Autumn.BL/DependencyInjection.cs b/Autumn.BL/DependencyInjection.cs index a8a1c9b..f57dba4 100644 --- a/Autumn.BL/DependencyInjection.cs +++ b/Autumn.BL/DependencyInjection.cs @@ -15,6 +15,7 @@ public static void AddBusinessServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Autumn.BL/Interface/ICountryService.cs b/Autumn.BL/Interface/ICountryService.cs new file mode 100644 index 0000000..08e67e5 --- /dev/null +++ b/Autumn.BL/Interface/ICountryService.cs @@ -0,0 +1,10 @@ +using Autumn.Domain.Models; +using System.Threading.Tasks; + +namespace Autumn.Service.Interface +{ + public interface ICountryService : IBaseService + { + Task GetByCodeAsync(string code); + } +} diff --git a/Autumn.BL/Interface/ICustomsTariffService.cs b/Autumn.BL/Interface/ICustomsTariffService.cs index 6313197..529755c 100644 --- a/Autumn.BL/Interface/ICustomsTariffService.cs +++ b/Autumn.BL/Interface/ICustomsTariffService.cs @@ -8,5 +8,7 @@ public interface ICustomsTariffService : IBaseService { Task> GetByHeaderAsync(string header); Task GetByHSCodeAsync(string hscode); + Task GetByHSCodeAndCountryAsync(string hscode, string country); + Task> GetByHeaderAndCountryAsync(string header, string country); } } diff --git a/Autumn.BL/Interface/IHsCodeService.cs b/Autumn.BL/Interface/IHsCodeService.cs index b47593d..7ab280d 100644 --- a/Autumn.BL/Interface/IHsCodeService.cs +++ b/Autumn.BL/Interface/IHsCodeService.cs @@ -8,5 +8,6 @@ public interface IHsCodeService : IBaseService { Task> GetWithHSCodeOptionsAsync(string code, string pcode, string level); Task> GetWithOptionsAsync(string id, string pid, string level); + Task> SearchByDescriptionAsync(string keyword, int limit = 20); } } diff --git a/Autumn.BL/Interface/IProductService.cs b/Autumn.BL/Interface/IProductService.cs index bdba899..d941379 100644 --- a/Autumn.BL/Interface/IProductService.cs +++ b/Autumn.BL/Interface/IProductService.cs @@ -9,5 +9,6 @@ public interface IProductService : IBaseService Task> GetByKeywordAsync(string keyword); Task> GetByTagsAsync(string tag); Task> GetLikeKeywordAsync(string keyword); + Task> SearchByKeywordAsync(string keyword, int limit = 20); } } diff --git a/Autumn.BL/Services/Classification.cs b/Autumn.BL/Services/Classification.cs index dca14d3..1094a2c 100644 --- a/Autumn.BL/Services/Classification.cs +++ b/Autumn.BL/Services/Classification.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Autumn.BL.Models.Request.V3; using Autumn.BL.Models.Response.V3; @@ -9,6 +10,7 @@ using Autumn_UIML.Model; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using RestSharp; namespace Autumn.Service @@ -34,80 +36,144 @@ public async Task SearchAsync(BLSearchRequest request) { try { - BLSearchResponse response = new BLSearchResponse { Success = true }; - BLResultModel rm = new BLResultModel(); - var rms = new List(); + var response = new BLSearchResponse { Success = true }; var records = new Dictionary>(); - //Do Navigation or Tag Query - rm.Prediction = string.Empty;// item.Key; - rm.Code = request.pid;// aiarr[1]; - rm.Rating = 0;// item.Value; - rm.Tags = new List(); - rm.PHSCodes = new List(); - rm.HSCodes = new List(); - + // Navigation mode — handle separately + if (!string.IsNullOrEmpty(request.settings)) + { + var rm = new BLResultModel + { + Prediction = string.Empty, + Code = request.pid, + Rating = 0, + Tags = new List(), + PHSCodes = new List(), + HSCodes = new List() + }; + var rms = new List(); + return await Navigation(request, response, rm, rms, records); + } + // --- Blended search: run DB stages concurrently --- + var allResults = new List(); + // DB stages in parallel: exact match + Atlas Search + description + var exactTask = _productService.GetByKeywordAsync(request.keyword); + var searchTask = _productService.SearchByKeywordAsync(request.keyword); + var descTask = _hsCodeService.SearchByDescriptionAsync(request.keyword); + await Task.WhenAll(exactTask, searchTask, descTask); - //Naigation Logic this should be seperated + var exactProducts = exactTask.Result ?? new List(); + var searchProducts = searchTask.Result ?? new List(); + var hsResults = descTask.Result ?? new List(); - if (!string.IsNullOrEmpty(request.settings)) + // Process exact matches (highest confidence: 0.88–0.97) + var exactRms = new List(); + for (var pi = 0; pi < exactProducts.Count; pi++) { - return await Navigation(request, response, rm, rms, records); + var conf = Math.Max(0.88f, 0.97f - pi * 0.03f); + await LoadProduct(exactRms, exactProducts[pi], conf); } - else - { - // Check if there is a direct match from the database - List products = await _productService.GetByKeywordAsync(request.keyword); - - var ctn = products.Count(x => x.Tags != null); + allResults.AddRange(exactRms); + // Process Atlas Search / regex matches (medium confidence: 0.60–0.82) + var searchRms = new List(); + for (var ri = 0; ri < searchProducts.Count; ri++) + { + var conf = Math.Max(0.60f, 0.82f - ri * 0.02f); + await LoadProduct(searchRms, searchProducts[ri], conf); + } + allResults.AddRange(searchRms); - if (products.Count > 0) + // Process description matches (lower confidence: 0.40–0.73) + for (var hi = 0; hi < hsResults.Count; hi++) + { + var hs = hsResults[hi]; + var levelBonus = hs.Level == 4 ? 0.05f : 0f; + var conf = Math.Max(0.40f, 0.68f + levelBonus - hi * 0.02f); + var rm = new BLResultModel + { + Prediction = hs.Description, + Code = hs.Code, + Rating = conf, + Tags = new List(), + HSCodes = new List { hs }, + PHSCodes = new List() + }; + // Build ancestor chain + var ancestors = new List(); + var cur = hs; + while (cur != null && !string.IsNullOrEmpty(cur.ParentId)) { - foreach (var product in products) + var parents = await _hsCodeService.GetWithOptionsAsync(cur.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) { - rm = await LoadProduct(rms, product); - if (ctn == 0) - { - records.Add("match", rms); - return new BLSearchResponse { Success = true, Records = records }; - } + ancestors.Insert(0, parent); + cur = parent; } - records.Add("match", rms); - return new BLSearchResponse { Success = true, Records = records }; - + else break; } - else if (products.Count == 0) - { - // there is no direct match from the database Attempt Synonyms - var synonyms = await GetMatchSynonyms(request.keyword); - if (synonyms.Count > 0) - { - foreach (var product in synonyms) - { - rm = await LoadProduct(rms, product); - } - records.Add("synonym", rms); - response.Records = records; - return response; + rm.PHSCodes = ancestors; + allResults.Add(rm); + } - } - else - { + // Groq fallback: only call LLM if DB stages returned no high-confidence results + var bestConfidence = allResults.Count > 0 ? allResults.Max(r => r.Rating) : 0f; + if (bestConfidence < 0.7f) + { + var groqResults = await GroqClassifyAsync(request.keyword); + allResults.AddRange(groqResults); + } - //Attempt Prediction - rm = await AIMethod(request, response, rms); - records.Add("ai", rms); - response.Records = records; - return response; - } + // Deduplicate by HS code, keeping the highest-confidence entry + var merged = allResults + .GroupBy(r => r.Code ?? r.HSCodes?.FirstOrDefault()?.Code ?? "") + .Where(g => !string.IsNullOrEmpty(g.Key)) + .Select(g => g.OrderByDescending(r => r.Rating).First()) + .OrderByDescending(r => r.Rating) + .Take(20) + .ToList(); + + if (merged.Count > 0) + { + records["match"] = merged; + response.Records = records; + return response; + } + + // Fallback: synonyms (only if no results from primary stages) + var synonyms = await GetMatchSynonyms(request.keyword); + if (synonyms.Count > 0) + { + var synRms = new List(); + for (var si = 0; si < synonyms.Count; si++) + { + var conf = Math.Max(0.35f, 0.58f - si * 0.03f); + await LoadProduct(synRms, synonyms[si], conf); } + records["synonym"] = synRms; response.Records = records; return response; } + + // Last resort: ML model prediction + try + { + var aiRms = new List(); + await AIMethod(request, response, aiRms); + if (aiRms.Count > 0) + records["ai"] = aiRms; + } + catch + { + // ML model may not be available + } + + response.Records = records; + return response; } catch (Exception ex) { @@ -115,19 +181,35 @@ public async Task SearchAsync(BLSearchRequest request) } } - private async Task LoadProduct(List rms, Product product) + private async Task LoadProduct(List rms, Product product, float confidence = 0.5f) { BLResultModel rm = new BLResultModel(); + rm.Rating = confidence; rm.Tags = new List(); - // var aiarr = product.Key.Split('-'); + rm.PHSCodes = new List(); rm.HSCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(product.Code, null, null); - //rm.HSCodes = Result2; rm.Prediction = product.Keyword; rm.Code = product.Code; if (product.Tags != null) rm.Tags.AddRange(product.Tags); - //rm.Rating = item.Value; - rm.PHSCodes = await _hsCodeService.GetWithOptionsAsync(rm.HSCodes.FirstOrDefault().ParentId, null, null); + // Build full ancestor chain (Section → Chapter → Heading) + if (rm.HSCodes.Count > 0) + { + var ancestors = new List(); + var current = rm.HSCodes.FirstOrDefault(); + while (current != null && !string.IsNullOrEmpty(current.ParentId)) + { + var parents = await _hsCodeService.GetWithOptionsAsync(current.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) + { + ancestors.Insert(0, parent); + current = parent; + } + else break; + } + rm.PHSCodes = ancestors; + } rms.Add(rm); return rm; } @@ -138,7 +220,7 @@ private async Task> GetMatchSynonyms(string keyword) var synonyms = await GetSynonyms(keyword.ToLower()); foreach (var synonym in synonyms.ToList()) { - List productExist = await _productService.GetLikeKeywordAsync(synonym); + List productExist = await _productService.GetLikeKeywordAsync(synonym) ?? new List(); //if (productExist.Count == 0) synonyms.Remove(synonym); collector.AddRange(productExist); @@ -158,7 +240,7 @@ private async Task> GetSynonyms(string keyword) req.AddHeader("x-rapidapi-host", "languagetools.p.rapidapi.com"); var resp = await client.ExecuteAsync(req); var synonyms = JsonConvert.DeserializeObject(resp.Content); - return synonyms.Synonyms; + return synonyms?.Synonyms ?? new List(); } catch { @@ -180,9 +262,7 @@ private async Task AIMethod(BLSearchRequest request, BLSearchResp { var aiarr = item.Key.Split('-'); rm.HSCodes.AddRange(await _hsCodeService.GetWithHSCodeOptionsAsync(aiarr[1], null, null)); - //rm.HSCodes = Result2; - //rm.Code = aiarr[1]; - //rm.Rating = item.Value; + rm.Rating = item.Value; } rm.Prediction = request.keyword; @@ -215,6 +295,132 @@ private async Task Navigation(BLSearchRequest request, BLSearc return response; } + private async Task> GroqClassifyAsync(string keyword) + { + var results = new List(); + var apiKey = _configuration["SiteSettings:GroqApiKey"]; + if (string.IsNullOrEmpty(apiKey)) + return results; + + try + { + var model = _configuration["SiteSettings:GroqModel"] ?? "llama-3.1-8b-instant"; + var client = new RestClient("https://api.groq.com/openai/v1/chat/completions"); + var req = new RestRequest { Method = Method.Post }; + req.AddHeader("Authorization", $"Bearer {apiKey}"); + req.AddHeader("Content-Type", "application/json"); + + var systemPrompt = @"You are an HS Code classification expert. Given a product description, return the most likely Harmonized System (HS) codes at the 4-digit or 6-digit heading level. + +Rules: +- Return ONLY a JSON array of objects with ""code"" and ""description"" fields +- Each code should be a valid HS code (4 or 6 digits, e.g. ""8471"" or ""847130"") +- Return up to 5 most likely codes, ordered by confidence +- Do not include any text outside the JSON array +- Format codes without dots or spaces + +Example response: +[{""code"":""8471"",""description"":""Automatic data processing machines""},{""code"":""8473"",""description"":""Parts and accessories for office machines""}]"; + + var body = new + { + model, + messages = new[] + { + new { role = "system", content = systemPrompt }, + new { role = "user", content = $"Classify this product: {keyword}" } + }, + temperature = 0.1, + max_tokens = 512 + }; + + req.AddJsonBody(body); + var resp = await client.ExecuteAsync(req); + + if (!resp.IsSuccessful || string.IsNullOrEmpty(resp.Content)) + return results; + + var json = JObject.Parse(resp.Content); + var content = json["choices"]?[0]?["message"]?["content"]?.ToString(); + if (string.IsNullOrEmpty(content)) + return results; + + // Extract JSON array from response (LLM may wrap it in markdown code blocks) + var arrayMatch = Regex.Match(content, @"\[.*\]", RegexOptions.Singleline); + if (!arrayMatch.Success) + return results; + + var predictions = JArray.Parse(arrayMatch.Value); + + for (var i = 0; i < predictions.Count; i++) + { + var code = predictions[i]["code"]?.ToString(); + var desc = predictions[i]["description"]?.ToString() ?? ""; + if (string.IsNullOrEmpty(code)) + continue; + + // Look up the code in the database + var hsCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(code, null, null); + if (hsCodes.Count == 0) + { + // Try partial match — search children with this prefix + hsCodes = await _hsCodeService.GetWithHSCodeOptionsAsync(null, code, null); + } + + var foundInDb = hsCodes.Count > 0; + + // Log every Groq prediction for accuracy tracking (fire-and-forget) + _ = _searchlogService.CreateAsync(new SearchLog + { + Keyword = keyword, + Prediction = $"{code}-{desc}", + Rating = i + 1, + Threshold = 0, + Source = "groq", + FoundInDb = foundInDb, + Created = DateTime.Now + }); + + if (!foundInDb) + continue; + + var conf = Math.Max(0.45f, 0.75f - i * 0.06f); + var rm = new BLResultModel + { + Prediction = desc.Length > 0 ? desc : keyword, + Code = code, + Rating = conf, + Tags = new List { "ai" }, + HSCodes = hsCodes, + PHSCodes = new List() + }; + + // Build ancestor chain + var ancestors = new List(); + var cur = hsCodes.FirstOrDefault(); + while (cur != null && !string.IsNullOrEmpty(cur.ParentId)) + { + var parents = await _hsCodeService.GetWithOptionsAsync(cur.ParentId, null, null); + var parent = parents.FirstOrDefault(); + if (parent != null) + { + ancestors.Insert(0, parent); + cur = parent; + } + else break; + } + rm.PHSCodes = ancestors; + results.Add(rm); + } + } + catch + { + // Groq API not available — return empty + } + + return results; + } + public Dictionary GetHSCode(string product, double threshold) { ModelInput data = new ModelInput diff --git a/Autumn.BL/Services/CountryService.cs b/Autumn.BL/Services/CountryService.cs new file mode 100644 index 0000000..0a50fb3 --- /dev/null +++ b/Autumn.BL/Services/CountryService.cs @@ -0,0 +1,20 @@ +using Autumn.Domain.Models; +using Autumn.Infrastructure.Interface; +using Autumn.Service.Interface; +using System.Threading.Tasks; + +namespace Autumn.Service +{ + public class CountryService : BaseService, ICountryService + { + protected readonly ICountryRepository _repository; + + public CountryService(ICountryRepository repository) : base(repository) + { + _repository = repository; + } + + public async Task GetByCodeAsync(string code) => + await _repository.GetByCodeAsync(code); + } +} diff --git a/Autumn.BL/Services/CustomsTariffService.cs b/Autumn.BL/Services/CustomsTariffService.cs index 20efeca..78ac947 100644 --- a/Autumn.BL/Services/CustomsTariffService.cs +++ b/Autumn.BL/Services/CustomsTariffService.cs @@ -18,5 +18,10 @@ public async Task GetByHSCodeAsync(string hscode) => await _repository.GetByHSCodeAsync(hscode); public async Task> GetByHeaderAsync(string header) => await _repository.GetByHeaderAsync(header); + + public async Task GetByHSCodeAndCountryAsync(string hscode, string country) => + await _repository.GetByHSCodeAndCountryAsync(hscode, country); + public async Task> GetByHeaderAndCountryAsync(string header, string country) => + await _repository.GetByHeaderAndCountryAsync(header, country); } } diff --git a/Autumn.BL/Services/HsCodeService.cs b/Autumn.BL/Services/HsCodeService.cs index 3fc08aa..06075b1 100644 --- a/Autumn.BL/Services/HsCodeService.cs +++ b/Autumn.BL/Services/HsCodeService.cs @@ -19,5 +19,8 @@ public async Task> GetWithOptionsAsync(string id, string parentId, public async Task> GetWithHSCodeOptionsAsync(string code, string parentCode, string level) => await _hsCodeRepository.GetWithHSCodeOptionsAsync(code, parentCode, level); + public async Task> SearchByDescriptionAsync(string keyword, int limit = 20) => + await _hsCodeRepository.SearchByDescriptionAsync(keyword, limit); + } } diff --git a/Autumn.BL/Services/ProductService.cs b/Autumn.BL/Services/ProductService.cs index 7f4a484..881a42f 100644 --- a/Autumn.BL/Services/ProductService.cs +++ b/Autumn.BL/Services/ProductService.cs @@ -24,5 +24,8 @@ public async Task> GetByKeywordAsync(string keyword) => public async Task> GetLikeKeywordAsync(string keyword) => await _productRepository.GetLikeKeywordAsync(keyword); + public async Task> SearchByKeywordAsync(string keyword, int limit = 20) => + await _productRepository.SearchByKeywordAsync(keyword, limit); + } } diff --git a/Autumn.Domain/Autumn.Domain.csproj b/Autumn.Domain/Autumn.Domain.csproj index 44b56dd..b20615b 100644 --- a/Autumn.Domain/Autumn.Domain.csproj +++ b/Autumn.Domain/Autumn.Domain.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/Autumn.Domain/Models/Country.cs b/Autumn.Domain/Models/Country.cs new file mode 100644 index 0000000..9ab003b --- /dev/null +++ b/Autumn.Domain/Models/Country.cs @@ -0,0 +1,17 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Autumn.Domain.Models +{ + public class Country + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public string Flag { get; set; } + public string Currency { get; set; } + public string Symbol { get; set; } + } +} diff --git a/Autumn.Domain/Models/CustomsTariff.cs b/Autumn.Domain/Models/CustomsTariff.cs index 6818b91..14abac4 100644 --- a/Autumn.Domain/Models/CustomsTariff.cs +++ b/Autumn.Domain/Models/CustomsTariff.cs @@ -7,20 +7,30 @@ namespace Autumn.Domain.Models { + [BsonIgnoreExtraElements] public class CustomsTariff { [BsonId] [BsonRepresentation(BsonType.ObjectId)] public string Id { get; set; } + public string Country { get; set; } public string Header { get; set; } public string HSCode { get; set; } public string Description { get; set; } + // Common public string DUTY { get; set; } - public string LEVY { get; set; } public string VAT { get; set; } + public string LEVY { get; set; } + // Nigeria public string NAC { get; set; } public string SUR { get; set; } public string ETLS { get; set; } public string CISS { get; set; } + // Ghana + public string NHIL { get; set; } + public string GETFUND { get; set; } + // Kenya + public string IDF { get; set; } + public string RDF { get; set; } } } diff --git a/Autumn.Domain/Models/Document.cs b/Autumn.Domain/Models/Document.cs index 05fa50c..db3ebf6 100644 --- a/Autumn.Domain/Models/Document.cs +++ b/Autumn.Domain/Models/Document.cs @@ -5,6 +5,7 @@ namespace Autumn.Domain.Models { + [BsonIgnoreExtraElements] public partial class Document { diff --git a/Autumn.Domain/Models/SearchLog.cs b/Autumn.Domain/Models/SearchLog.cs index 2eb0b95..5474805 100644 --- a/Autumn.Domain/Models/SearchLog.cs +++ b/Autumn.Domain/Models/SearchLog.cs @@ -13,6 +13,8 @@ public class SearchLog public string Prediction { get; set; } public double Rating { get; set; } public double Threshold { get; set; } + public string Source { get; set; } + public bool FoundInDb { get; set; } public DateTime Created { get; set; } } diff --git a/Autumn.Domain/Models/StoreDatabaseSettings.cs b/Autumn.Domain/Models/StoreDatabaseSettings.cs index ffc32dc..ae47838 100644 --- a/Autumn.Domain/Models/StoreDatabaseSettings.cs +++ b/Autumn.Domain/Models/StoreDatabaseSettings.cs @@ -15,6 +15,7 @@ public class StoreDatabaseSettings : IStoreDatabaseSettings public string CustomsTariffStoreCollectionName { get; set; } public string HSCodeToDocumentStoreCollectionName { get; set; } public string RequirementStoreCollectionName { get; set; } + public string CountryStoreCollectionName { get; set; } public string ConnectionString { get; set; } public string DatabaseName { get; set; } } @@ -32,6 +33,7 @@ public interface IStoreDatabaseSettings string CustomsTariffStoreCollectionName { get; set; } string HSCodeToDocumentStoreCollectionName { get; set; } string RequirementStoreCollectionName { get; set; } + string CountryStoreCollectionName { get; set; } string ConnectionString { get; set; } string DatabaseName { get; set; } } diff --git a/Autumn.Repository/Autumn.Infrastructure.csproj b/Autumn.Repository/Autumn.Infrastructure.csproj index 8c16921..2b0ac37 100644 --- a/Autumn.Repository/Autumn.Infrastructure.csproj +++ b/Autumn.Repository/Autumn.Infrastructure.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable diff --git a/Autumn.Repository/DependencyInjection.cs b/Autumn.Repository/DependencyInjection.cs index fc91519..d1f0687 100644 --- a/Autumn.Repository/DependencyInjection.cs +++ b/Autumn.Repository/DependencyInjection.cs @@ -22,6 +22,7 @@ public static void AddRepositoryServices(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } diff --git a/Autumn.Repository/Interface/ICountryRepository.cs b/Autumn.Repository/Interface/ICountryRepository.cs new file mode 100644 index 0000000..3bdca32 --- /dev/null +++ b/Autumn.Repository/Interface/ICountryRepository.cs @@ -0,0 +1,9 @@ +using Autumn.Domain.Models; + +namespace Autumn.Infrastructure.Interface +{ + public interface ICountryRepository : IBaseRepository + { + Task GetByCodeAsync(string code); + } +} diff --git a/Autumn.Repository/Interface/ICustomsTariffRepository.cs b/Autumn.Repository/Interface/ICustomsTariffRepository.cs index 1709227..da9a6bf 100644 --- a/Autumn.Repository/Interface/ICustomsTariffRepository.cs +++ b/Autumn.Repository/Interface/ICustomsTariffRepository.cs @@ -6,5 +6,7 @@ public interface ICustomsTariffRepository : IBaseRepository { Task> GetByHeaderAsync(string header); Task GetByHSCodeAsync(string hscode); + Task GetByHSCodeAndCountryAsync(string hscode, string country); + Task> GetByHeaderAndCountryAsync(string header, string country); } } diff --git a/Autumn.Repository/Interface/IHsCodeRepository.cs b/Autumn.Repository/Interface/IHsCodeRepository.cs index 2ae0b65..34d24f1 100644 --- a/Autumn.Repository/Interface/IHsCodeRepository.cs +++ b/Autumn.Repository/Interface/IHsCodeRepository.cs @@ -6,5 +6,6 @@ public interface IHsCodeRepository : IBaseRepository { Task> GetWithHSCodeOptionsAsync(string code, string parentCode, string level); Task> GetWithOptionsAsync(string id, string parentId, string level); + Task> SearchByDescriptionAsync(string keyword, int limit = 20); } } diff --git a/Autumn.Repository/Interface/IProductRepository.cs b/Autumn.Repository/Interface/IProductRepository.cs index dc45ed6..b685a85 100644 --- a/Autumn.Repository/Interface/IProductRepository.cs +++ b/Autumn.Repository/Interface/IProductRepository.cs @@ -8,5 +8,6 @@ public interface IProductRepository : IBaseRepository Task> GetByKeywordAsync(string keyword); Task> GetByTagsAsync(string tag); Task> GetLikeKeywordAsync(string keyword); + Task> SearchByKeywordAsync(string keyword, int limit = 20); } } diff --git a/Autumn.Repository/Repository/CountryRepository.cs b/Autumn.Repository/Repository/CountryRepository.cs new file mode 100644 index 0000000..4a9fd1b --- /dev/null +++ b/Autumn.Repository/Repository/CountryRepository.cs @@ -0,0 +1,17 @@ +using Autumn.Domain.Models; +using Autumn.Infrastructure.Interface; +using MongoDB.Driver; + +namespace Autumn.Infrastructure.Repository +{ + public class CountryRepository : BaseRepository, ICountryRepository + { + public CountryRepository(IStoreDatabaseSettings settings) + : base(settings, settings.CountryStoreCollectionName) + { + } + + public async Task GetByCodeAsync(string code) => + await _collection.Find(x => x.Code == code).FirstOrDefaultAsync(); + } +} diff --git a/Autumn.Repository/Repository/CustomsTariffRepository.cs b/Autumn.Repository/Repository/CustomsTariffRepository.cs index 8f67e45..4a4607a 100644 --- a/Autumn.Repository/Repository/CustomsTariffRepository.cs +++ b/Autumn.Repository/Repository/CustomsTariffRepository.cs @@ -14,5 +14,15 @@ public async Task GetByHSCodeAsync(string hscode) => await _collection.Find(x => x.HSCode == hscode).FirstOrDefaultAsync(); public async Task> GetByHeaderAsync(string header) => await _collection.Find(x => x.Header == header).ToListAsync(); + + public async Task GetByHSCodeAndCountryAsync(string hscode, string country) => + await _collection.Find(x => + x.HSCode == hscode && (x.Country == country || (x.Country == null && country == "NG")) + ).FirstOrDefaultAsync(); + + public async Task> GetByHeaderAndCountryAsync(string header, string country) => + await _collection.Find(x => + x.Header == header && (x.Country == country || (x.Country == null && country == "NG")) + ).ToListAsync(); } } diff --git a/Autumn.Repository/Repository/HsCodeRepository.cs b/Autumn.Repository/Repository/HsCodeRepository.cs index d9b5c65..606340d 100644 --- a/Autumn.Repository/Repository/HsCodeRepository.cs +++ b/Autumn.Repository/Repository/HsCodeRepository.cs @@ -1,5 +1,6 @@ using Autumn.Domain.Models; using Autumn.Infrastructure.Interface; +using MongoDB.Bson; using MongoDB.Driver; namespace Autumn.Infrastructure.Repository @@ -64,6 +65,71 @@ public async Task> GetWithHSCodeOptionsAsync(string code, string pa return resp.OrderBy(x => x.Order).ToList(); } + /// + /// Atlas Search with fuzzy matching on Description field, filtered to levels 3 & 4. + /// Requires an Atlas Search index named "default" on the hscodes collection. + /// Falls back to tokenized regex if Atlas Search is unavailable. + /// + public async Task> SearchByDescriptionAsync(string keyword, int limit = 20) + { + if (string.IsNullOrEmpty(keyword?.Trim())) + return new List(); + + // Try Atlas Search first + try + { + var searchStage = new BsonDocument("$search", new BsonDocument + { + { "index", "hscodes-index" }, + { "compound", new BsonDocument + { + { "must", new BsonArray + { + new BsonDocument("text", new BsonDocument + { + { "query", keyword }, + { "path", "Description" }, + { "fuzzy", new BsonDocument { { "maxEdits", 1 }, { "prefixLength", 2 } } } + }) + } + }, + { "filter", new BsonArray + { + new BsonDocument("range", new BsonDocument + { + { "path", "Level" }, + { "gte", 3 }, + { "lte", 4 } + }) + } + } + } + } + }); + var limitStage = new BsonDocument("$limit", limit); + + var pipeline = PipelineDefinition.Create(searchStage, limitStage); + var results = await _collection.Aggregate(pipeline).ToListAsync(); + if (results.Count > 0) + return results; + } + catch + { + // Atlas Search not available — fall through to regex + } + + // Fallback: tokenized regex (split query into words, match all in any order) + var words = keyword.Trim().Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(w => System.Text.RegularExpressions.Regex.Escape(w)); + var pattern = string.Join("", words.Select(w => $"(?=.*{w})")) + ".*"; + + var filter = Builders.Filter.And( + Builders.Filter.Regex(x => x.Description, new BsonRegularExpression(pattern, "i")), + Builders.Filter.In(x => x.Level, new long[] { 3, 4 }) + ); + return await _collection.Find(filter).Limit(limit).SortBy(x => x.Level).ToListAsync(); + } + // Override methods for specific operations if necessary public override async Task InsertOneAsync(HSCode x) { diff --git a/Autumn.Repository/Repository/ProductRepository.cs b/Autumn.Repository/Repository/ProductRepository.cs index f919d43..8cfaff6 100644 --- a/Autumn.Repository/Repository/ProductRepository.cs +++ b/Autumn.Repository/Repository/ProductRepository.cs @@ -1,5 +1,7 @@ -using Autumn.Domain.Models; +using System.Text.RegularExpressions; +using Autumn.Domain.Models; using Autumn.Infrastructure.Interface; +using MongoDB.Bson; using MongoDB.Driver; namespace Autumn.Infrastructure.Repository @@ -15,12 +17,9 @@ public async Task> GetByTagsAsync(string tag) { if (string.IsNullOrEmpty(tag)) return await _collection.Find(x => x.Tags != null).ToListAsync(); - //return await base.GetAsync(); else return await _collection.Find(x => x.Tags.Contains(tag)).ToListAsync(); } - // public List GetByKeyword(string keyword) => - // _collection.Find(x => x.Keyword == keyword).ToList(); public async Task> GetByKeywordAsync(string keyword) => await _collection.Find(x => x.Keyword.ToLower() == keyword.ToLower()).ToListAsync(); @@ -29,7 +28,6 @@ public async Task> GetLikeKeywordAsync(string keyword) { if (string.IsNullOrEmpty(keyword)) { - //return await _collection.Find(x => true).ToListAsync(); return await base.GetAsync(); } else @@ -38,6 +36,53 @@ public async Task> GetLikeKeywordAsync(string keyword) } } + /// + /// Atlas Search with fuzzy matching on the Keyword field. + /// Requires an Atlas Search index named "default" on the products collection. + /// Falls back to tokenized regex if Atlas Search is unavailable. + /// + public async Task> SearchByKeywordAsync(string keyword, int limit = 20) + { + if (string.IsNullOrEmpty(keyword?.Trim())) + return new List(); + + // Try Atlas Search first (word splitting + fuzzy matching) + try + { + var searchStage = new BsonDocument("$search", new BsonDocument + { + { "index", "product-index" }, + { "text", new BsonDocument + { + { "query", keyword }, + { "path", "Keyword" }, + { "fuzzy", new BsonDocument { { "maxEdits", 1 }, { "prefixLength", 2 } } } + } + } + }); + var limitStage = new BsonDocument("$limit", limit); + + var pipeline = PipelineDefinition.Create(searchStage, limitStage); + var results = await _collection.Aggregate(pipeline).ToListAsync(); + if (results.Count > 0) + return results; + } + catch + { + // Atlas Search not available — fall through to regex + } + + // Fallback: tokenized regex (split query into words, match all in any order) + var words = keyword.Trim().Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(w => Regex.Escape(w)); + var pattern = string.Join("", words.Select(w => $"(?=.*{w})")) + ".*"; + + var filter = Builders.Filter.Regex( + x => x.Keyword, + new BsonRegularExpression(pattern, "i")); + return await _collection.Find(filter).Limit(limit).ToListAsync(); + } + public override async Task CreateAsync(Product entity) { var now = DateTime.Now; diff --git a/Autumn.SPA/.gitignore b/Autumn.SPA/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/Autumn.SPA/.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/Autumn.SPA/README.md b/Autumn.SPA/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/Autumn.SPA/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 [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## 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/Autumn.SPA/eslint.config.js b/Autumn.SPA/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/Autumn.SPA/eslint.config.js @@ -0,0 +1,29 @@ +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: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/Autumn.SPA/index.html b/Autumn.SPA/index.html new file mode 100644 index 0000000..9be6324 --- /dev/null +++ b/Autumn.SPA/index.html @@ -0,0 +1,14 @@ + + + + + + + + HS.Codes + + +
+ + + diff --git a/Autumn.SPA/package-lock.json b/Autumn.SPA/package-lock.json new file mode 100644 index 0000000..6b8a00c --- /dev/null +++ b/Autumn.SPA/package-lock.json @@ -0,0 +1,3441 @@ +{ + "name": "autumn-spa", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "autumn-spa", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.564.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "peer": true, + "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": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "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/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "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": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "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==", + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "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==", + "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": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "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.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "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/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "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/lucide-react": { + "version": "0.564.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz", + "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "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/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "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/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.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "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/Autumn.SPA/package.json b/Autumn.SPA/package.json new file mode 100644 index 0000000..1787163 --- /dev/null +++ b/Autumn.SPA/package.json @@ -0,0 +1,30 @@ +{ + "name": "autumn-spa", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.564.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } +} diff --git a/Autumn.SPA/public/favicon.svg b/Autumn.SPA/public/favicon.svg new file mode 100644 index 0000000..6febc41 --- /dev/null +++ b/Autumn.SPA/public/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Autumn.SPA/public/vite.svg b/Autumn.SPA/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/Autumn.SPA/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Autumn.SPA/src/App.jsx b/Autumn.SPA/src/App.jsx new file mode 100644 index 0000000..8be8a45 --- /dev/null +++ b/Autumn.SPA/src/App.jsx @@ -0,0 +1,110 @@ +import { useState, useEffect } from "react"; +import { api } from "./api"; +import Header from "./components/Header"; +import Home from "./components/Home"; +import SearchView from "./components/SearchView"; +import CalcView from "./components/CalcView"; +import BrowseView from "./components/BrowseView"; +import Toast from "./components/Toast"; + +export default function App() { + const [mode, setMode] = useState("light"); + const [view, setView] = useState("home"); + const [query, setQuery] = useState(""); + const [country, setCountry] = useState("NG"); + const [countries, setCountries] = useState([]); + // Cross-view state for calculator prefill + const [calcInit, setCalcInit] = useState({ hscode: "", product: "" }); + + useEffect(() => { + document.documentElement.classList.toggle("dark", mode === "dark"); + }, [mode]); + + useEffect(() => { + api.countries().then((resp) => { + if (resp.success && resp.records) { + setCountries(resp.records); + } + }).catch(() => {}); + }, []); + + const onReset = () => { + setView("home"); + setQuery(""); + }; + + // Navigate from SearchView → CalcView with HS code prefilled + const goToCalc = (hscode, product) => { + setCalcInit({ hscode: hscode || "", product: product || "" }); + setView("calculator"); + }; + + // Navigate from CalcView → SearchView to find an HS code + const goToSearch = (product) => { + setQuery(product || ""); + setView("results"); + }; + + return ( +
+
+ + {view === "home" && ( + {}} + onNavigate={setView} + /> + )} + + {view === "results" && ( + + )} + + {view === "calculator" && ( + + )} + + {view === "browse" && ( + + )} + + + +
+ © 2025 HS.Codes — Open source commodity classification. + GitHub → +
+
+ ); +} diff --git a/Autumn.SPA/src/api.js b/Autumn.SPA/src/api.js new file mode 100644 index 0000000..e1d27dc --- /dev/null +++ b/Autumn.SPA/src/api.js @@ -0,0 +1,42 @@ +const BASE = '/api'; + +const json = async (r) => { + if (r.status === 429) { + window.dispatchEvent(new CustomEvent('api:ratelimit')); + throw new Error('Too many requests. Please wait a moment and try again.'); + } + return r.json(); +}; + +export const api = { + search: (keyword) => + fetch(`${BASE}/search?keyword=${encodeURIComponent(keyword)}`).then(json), + + browse: ({ code, parentCode, parentId, level } = {}) => { + const params = new URLSearchParams(); + if (code) params.set('code', code); + if (parentCode) params.set('parentCode', parentCode); + if (parentId) params.set('parentId', parentId); + if (level != null) params.set('level', String(level)); + return fetch(`${BASE}/browse?${params}`).then(json); + }, + + duty: ({ HSCode, Country, ProductDesc, Cost, Freight, Insurance, Currency }) => + fetch(`${BASE}/duty?${new URLSearchParams({ + HSCode, Country, ProductDesc: ProductDesc || '', + Cost: String(Cost), Freight: String(Freight), + Insurance: String(Insurance), Currency: Currency || 'USD' + })}`).then(json), + + note: (hscode, country) => + fetch(`${BASE}/note/${encodeURIComponent(hscode)}?country=${encodeURIComponent(country)}`).then(json), + + countries: () => + fetch(`${BASE}/codelist/countries`).then(json), + + currencies: () => + fetch(`${BASE}/codelist/currency`).then(json), + + products: (query) => + fetch(`${BASE}/codelist/products/${encodeURIComponent(query || '')}`).then(json), +}; diff --git a/Autumn.SPA/src/app.css b/Autumn.SPA/src/app.css new file mode 100644 index 0000000..b0d927f --- /dev/null +++ b/Autumn.SPA/src/app.css @@ -0,0 +1,118 @@ +@import "tailwindcss"; + +/* ===== TAILWIND THEME EXTENSIONS ===== */ +@theme { + --font-sans: 'DM Sans', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + + --color-bg: var(--theme-bg); + --color-surface: var(--theme-surface); + --color-surface2: var(--theme-surface2); + --color-surface3: var(--theme-surface3); + --color-border: var(--theme-border); + --color-border-hover: var(--theme-border-hover); + --color-fg: var(--theme-text); + --color-fg-sec: var(--theme-text-sec); + --color-fg-dim: var(--theme-text-dim); + --color-accent: var(--theme-accent); + --color-accent-hover: var(--theme-accent-hover); + --color-accent-dim: var(--theme-accent-dim); + --color-accent-border: var(--theme-accent-border); + --color-success: var(--theme-success); + --color-success-dim: var(--theme-success-dim); + --color-info: var(--theme-info); + --color-info-dim: var(--theme-info-dim); + --color-input-bg: var(--theme-input-bg); + --color-header-bg: var(--theme-header-bg); + --color-danger: var(--theme-danger); + --color-btn-text: var(--theme-btn-text); + --color-logo-glow: var(--theme-logo-glow); + --color-warning: var(--theme-warning); + --color-warning-dim: var(--theme-warning-dim); + + --animate-fade-up: fade-up 0.4s ease; + --animate-fade-up-fast: fade-up 0.25s ease; + --animate-spin-loading: spin-loading 0.7s linear infinite; +} + +/* ===== LIGHT MODE (default) ===== */ +:root { + --theme-bg: #FFFFFF; + --theme-surface: #FFFFFF; + --theme-surface2: #F4F7F5; + --theme-surface3: #E9EFEC; + --theme-border: #DBE4DF; + --theme-border-hover: #C2D0C8; + --theme-text: #111110; + --theme-text-sec: #3D4A42; + --theme-text-dim: #728070; + --theme-accent: #059669; + --theme-accent-hover: #06B47D; + --theme-accent-dim: rgba(5,150,105,0.08); + --theme-accent-border: rgba(5,150,105,0.22); + --theme-success: #059669; + --theme-success-dim: rgba(5,150,105,0.10); + --theme-info: #2563EB; + --theme-info-dim: rgba(37,99,235,0.10); + --theme-input-bg: #F9FBF9; + --theme-header-bg: rgba(255,255,255,0.92); + --theme-grad: linear-gradient(135deg,#059669,#047857); + --theme-danger: #DC3545; + --theme-btn-text: #FFFFFF; + --theme-logo-glow: rgba(5,150,105,0.18); + --theme-warning: #D97706; + --theme-warning-dim: rgba(217,119,6,0.08); +} + +/* ===== DARK MODE ===== */ +.dark { + --theme-bg: #0A0A0A; + --theme-surface: #111111; + --theme-surface2: #181818; + --theme-surface3: #1F1F1F; + --theme-border: #262626; + --theme-border-hover: #333333; + --theme-text: #E8E6E1; + --theme-text-sec: #9CA3AF; + --theme-text-dim: #6B7280; + --theme-accent: #C9A84C; + --theme-accent-hover: #DBBD64; + --theme-accent-dim: rgba(201,168,76,0.10); + --theme-accent-border: rgba(201,168,76,0.25); + --theme-success: #4EA87A; + --theme-success-dim: rgba(78,168,122,0.12); + --theme-info: #5B8DD9; + --theme-info-dim: rgba(91,141,217,0.12); + --theme-input-bg: #131313; + --theme-header-bg: rgba(10,10,10,0.88); + --theme-grad: linear-gradient(135deg,#C9A84C,#A08535); + --theme-danger: #E05A4E; + --theme-btn-text: #0A0A0A; + --theme-logo-glow: rgba(201,168,76,0.20); + --theme-warning: #E09840; + --theme-warning-dim: rgba(224,152,64,0.12); +} + +/* ===== KEYFRAMES ===== */ +@keyframes fade-up { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes spin-loading { + to { transform: rotate(360deg); } +} + +@keyframes slideIn { + from { opacity: 0; transform: translateX(100%); } + to { opacity: 1; transform: translateX(0); } +} + +/* ===== GLOBAL ===== */ +@layer base { + * { box-sizing: border-box; margin: 0; } + html, body, #root { width: 100%; min-height: 100vh; } + ::selection { background: var(--theme-accent-dim); } +} + +.bg-grad { background: var(--theme-grad); } diff --git a/Autumn.SPA/src/assets/react.svg b/Autumn.SPA/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/Autumn.SPA/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Autumn.SPA/src/components/Badge.jsx b/Autumn.SPA/src/components/Badge.jsx new file mode 100644 index 0000000..dd72a99 --- /dev/null +++ b/Autumn.SPA/src/components/Badge.jsx @@ -0,0 +1,16 @@ +const variants = { + default: "bg-accent-dim text-accent", + ai: "bg-[rgba(139,92,246,0.12)] text-[#A78BFA]", + match: "bg-success-dim text-success", + synonym: "bg-info-dim text-info", +}; + +export default function Badge({ children, variant = "default", small }) { + return ( + + {children} + + ); +} diff --git a/Autumn.SPA/src/components/BrowseView.jsx b/Autumn.SPA/src/components/BrowseView.jsx new file mode 100644 index 0000000..1f4800f --- /dev/null +++ b/Autumn.SPA/src/components/BrowseView.jsx @@ -0,0 +1,216 @@ +import { useState, useEffect } from "react"; +import { ChevronRight, Package } from "lucide-react"; +import { api } from "../api"; +import { SECTIONS, ICON_MAP } from "../data/sections"; +import TariffPrompt from "./TariffPrompt"; + +const RATE_LABELS = { + duty: "Import Duty", vat: "VAT", levy: "Levy", sur: "Surcharge", + etls: "ETL", ciss: "CISS", nac: "NAC", nhil: "NHIL", + getfund: "GETFund", idf: "IDF", rdf: "RDF", +}; +const RATE_FIELDS = ["duty", "vat", "levy", "sur", "etls", "ciss", "nac", "nhil", "getfund", "idf", "rdf"]; + +function formatTariffEntries(tariff) { + if (!tariff) return []; + return RATE_FIELDS + .filter(f => tariff[f] && tariff[f] !== "0") + .map(f => [RATE_LABELS[f] || f, `${tariff[f]}%`]); +} + +export default function BrowseView({ country, countries, setCountry }) { + // path entries: { code, description, id, level, childLevel } + const [path, setPath] = useState([]); + const [children, setChildren] = useState(null); + const [leafDetail, setLeafDetail] = useState(null); + const [loading, setLoading] = useState(false); + + const isRoot = path.length === 0; + const selCountry = countries.find(c => c.code === country); + + const loadChildren = async (parentId, parentCode, level) => { + setLoading(true); setChildren(null); setLeafDetail(null); + try { + const params = parentId ? { parentId, level } : { parentCode, level }; + const resp = await api.browse(params); + if (resp.success && resp.records?.length > 0) { + setChildren(resp.records.sort((a, b) => a.code.localeCompare(b.code))); + } else { + setChildren(null); + // No children — it's a leaf, load tariff + const code = parentCode || path[path.length - 1]?.code; + if (code) { + const noteResp = await api.note(code, country || "NG"); + if (noteResp.success) setLeafDetail(noteResp); + } + } + } catch { + setChildren(null); + } + setLoading(false); + }; + + const navigateSection = (section) => { + const entry = { code: section.parentCode, description: `Section ${section.code} — ${section.title}`, id: null, level: 1, childLevel: 2 }; + setPath([entry]); + loadChildren(null, section.parentCode, 2); + }; + + const navigateTo = async (child) => { + const nextLevel = child.level + 1; + const entry = { code: child.code, description: child.description, id: child.id, level: child.level, childLevel: nextLevel }; + setPath(prev => [...prev, entry]); + setLoading(true); setChildren(null); setLeafDetail(null); + try { + const resp = await api.browse({ parentId: child.id, level: nextLevel }); + if (resp.success && resp.records?.length > 0) { + setChildren(resp.records.sort((a, b) => a.code.localeCompare(b.code))); + } else { + const noteResp = await api.note(child.code, country || "NG"); + if (noteResp.success) setLeafDetail(noteResp); + } + } catch { + const noteResp = await api.note(child.code, country || "NG").catch(() => null); + if (noteResp?.success) setLeafDetail(noteResp); + } + setLoading(false); + }; + + const browseToLevel = (idx) => { + if (idx < 0) { + setPath([]); setChildren(null); setLeafDetail(null); + return; + } + const newPath = path.slice(0, idx + 1); + setPath(newPath); + const target = newPath[newPath.length - 1]; + loadChildren(target.id, target.id ? null : target.code, target.childLevel); + }; + + useEffect(() => { + if (leafDetail && country && path.length > 0) { + const code = path[path.length - 1].code; + api.note(code, country).then(resp => { if (resp.success) setLeafDetail(resp); }).catch(() => {}); + } + }, [country]); + + const leafTariffEntries = leafDetail?.tariff?.[0] ? formatTariffEntries(leafDetail.tariff[0]) : []; + + return ( +
+

Browse Harmonized System

+

Navigate the WCO Harmonized System nomenclature.

+ + {/* Root grid */} + {isRoot && !loading && ( + <> +
+ {SECTIONS.map((s, i) => { + const Icon = ICON_MAP[s.code] || Package; + return ( + + ); + })} +
+

+ Try: Machinery & Electrical Equipment → Chapter 84 → 8471 → 8471.30 +

+ + )} + + {/* Hierarchy tree + children / leaf */} + {!isRoot && ( +
+
Navigation
+ + {/* "All Sections" root link */} + + + {/* Path hierarchy rows */} +
+ {path.map((entry, idx) => { + const isLast = idx === path.length - 1; + const isLeaf = isLast && leafDetail && !children; + const isClickable = !isLast; + return ( + + ); + })} +
+ + {/* Loading */} + {loading && ( +
+
+

Loading…

+
+ )} + + {/* Child items below the hierarchy */} + {children && !loading && ( +
+ {children.length === 0 ? ( +
No sub-items found.
+ ) : children.map((child, ci) => ( + + ))} +
+ )} + + {/* Leaf detail — tariff */} + {leafDetail && !loading && ( +
+ {leafDetail.records?.[0]?.description && ( +

{leafDetail.records[0].description}

+ )} +
+ Applicable Tariffs {selCountry && — {selCountry.flag} {selCountry.name}} +
+ {!country ? ( + + ) : country !== "NG" ? ( +
Tariff data for {selCountry?.name || country} coming soon.
+ ) : leafTariffEntries.length > 0 ? ( +
+ {leafTariffEntries.map(([l, v], ti) => ( +
+
{l}
+
{v}
+
+ ))} +
+ ) : ( +
No tariff data available for this code.
+ )} +
+ )} +
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/CalcView.jsx b/Autumn.SPA/src/components/CalcView.jsx new file mode 100644 index 0000000..56150a9 --- /dev/null +++ b/Autumn.SPA/src/components/CalcView.jsx @@ -0,0 +1,230 @@ +import { useState } from "react"; +import { Calculator, Search } from "lucide-react"; +import { api } from "../api"; +import TariffPrompt from "./TariffPrompt"; + +const labelCls = "block text-[11px] font-bold text-fg-dim mb-1 uppercase tracking-[0.05em]"; +const inputCls = "w-full px-3 py-2.5 rounded-lg border border-border bg-input-bg text-fg text-sm outline-none font-sans transition-colors duration-150 focus:border-accent"; + +// Strip commas to get raw number string +const rawNum = (v) => v.replace(/,/g, ""); + +// Format a number string with commas and 2 decimals +const fmtInput = (v) => { + const n = parseFloat(rawNum(v)); + if (isNaN(n)) return ""; + return n.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +}; + +export default function CalcView({ country, countries, setCountry, onSearchCode, initialHscode, initialProduct }) { + const [calc, setCalc] = useState({ + product: initialProduct || "", + hscode: initialHscode || "", + cost: "", + freight: "", + insurance: "", + }); + // Track which money field is focused (show raw value while editing) + const [focused, setFocused] = useState(null); + const [calcOut, setCalcOut] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const selCountry = countries.find(c => c.code === country); + const currency = selCountry?.currency?.split(" ")[0] || "NGN"; + + const parseVal = (k) => parseFloat(rawNum(calc[k])) || 0; + + const doCalc = (e) => { + e.preventDefault(); + if (!country || !calc.hscode) return; + setBusy(true); + setError(null); + api.duty({ + HSCode: calc.hscode, + Country: country, + ProductDesc: calc.product, + Cost: parseVal("cost"), + Freight: parseVal("freight"), + Insurance: parseVal("insurance"), + Currency: currency, + }).then((resp) => { + if (resp.success) { + setCalcOut(resp); + } else { + setError(resp.error?.[0] || "Calculation failed"); + setCalcOut(null); + } + }).catch((err) => { + setError(err.message || "Network error"); + setCalcOut(null); + }).finally(() => setBusy(false)); + }; + + const fmt = (n) => currency + " " + Number(n).toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + + const canSubmit = country && country === "NG"; + + const update = (k, v) => setCalc(prev => ({ ...prev, [k]: v })); + + // Allow only digits, one decimal point, and commas while typing + const handleMoneyChange = (k, raw) => { + const cleaned = raw.replace(/[^0-9.,]/g, ""); + update(k, cleaned); + }; + + // On blur: format with commas and 2 decimals, clamp to >= 0 + const handleMoneyBlur = (k) => { + setFocused(null); + const n = parseFloat(rawNum(calc[k])); + if (isNaN(n) || n <= 0) { + update(k, ""); + } else { + update(k, fmtInput(calc[k])); + } + }; + + // On focus: show raw number (no commas) for easy editing + const handleMoneyFocus = (k) => { + setFocused(k); + const n = parseFloat(rawNum(calc[k])); + if (!isNaN(n) && n > 0) { + update(k, String(n)); + } + }; + + // Display value: formatted when not focused, raw when focused + const moneyVal = (k) => focused === k ? calc[k] : (calc[k] ? fmtInput(calc[k]) : ""); + + return ( +
+

Import Duty Calculator

+

Estimate total import duties, taxes, and levies payable.

+ + {!country &&
} + +
+
+ {country && ( +
+ {selCountry?.flag} +
+
{selCountry?.name}
+
{currency}
+
+ +
+ )} + +
+ + update("product", e.target.value)} className={inputCls} /> +
+ +
+
+ + {onSearchCode && ( + + )} +
+ update("hscode", e.target.value)} className={inputCls} /> +
+ +
+ + handleMoneyChange("cost", e.target.value)} + onFocus={() => handleMoneyFocus("cost")} + onBlur={() => handleMoneyBlur("cost")} + className={inputCls} /> +
+ +
+ + handleMoneyChange("freight", e.target.value)} + onFocus={() => handleMoneyFocus("freight")} + onBlur={() => handleMoneyBlur("freight")} + className={inputCls} /> +
+ +
+ + handleMoneyChange("insurance", e.target.value)} + onFocus={() => handleMoneyFocus("insurance")} + onBlur={() => handleMoneyBlur("insurance")} + className={inputCls} /> +
+ + +
+ +
+ {error && ( +
+ {error} +
+ )} + {!calcOut && !error ? ( +
+ +

{!country ? "Select a country to get started" : country !== "NG" ? `Duty calculator for ${selCountry?.name || country} coming soon.` : "Fill in the form and click Calculate"}

+
+ ) : calcOut && ( +
+
+ {selCountry?.flag} +
+
{selCountry?.name} Import Duties
+
HS Code: {calcOut.hsCode}
+
+
+ {calcOut.hsCodeDescription && ( +
+ {calcOut.hsCodeDescription} +
+ )} +
CIF Summary ({currency})
+
+ {[ + ["Cost", fmt(calcOut.cost)], + ["Freight", fmt(calcOut.freight)], + ["Insurance", fmt(calcOut.insurance)], + ["CIF Total", fmt(calcOut.cif)], + ].map(([l, v], i) => ( +
+
{l}
+
{v}
+
+ ))} +
+
Duties, Taxes & Levies ({currency})
+
+ {(calcOut.breakdown || []).map((b, i) => ( +
+ {b.label} ({b.rate}%) + {fmt(b.amount)} +
+ ))} +
+
+ Total Duties & Taxes + {fmt(calcOut.totalDuty)} +
+

Disclaimer: Values are estimates only. Final duties determined by Customs at port of clearance.

+
+ )} +
+
+
+ ); +} diff --git a/Autumn.SPA/src/components/ConfBar.jsx b/Autumn.SPA/src/components/ConfBar.jsx new file mode 100644 index 0000000..c85e262 --- /dev/null +++ b/Autumn.SPA/src/components/ConfBar.jsx @@ -0,0 +1,14 @@ +export default function ConfBar({ v }) { + const pct = Math.round(v * 100); + const barColor = pct >= 85 ? 'bg-success' : pct >= 60 ? 'bg-accent' : 'bg-[#e8a820]'; + const textColor = pct >= 85 ? 'text-success' : pct >= 60 ? 'text-accent' : 'text-[#e8a820]'; + return ( +
+
+
+
+ {pct}% +
+ ); +} diff --git a/Autumn.SPA/src/components/CountrySelector.jsx b/Autumn.SPA/src/components/CountrySelector.jsx new file mode 100644 index 0000000..8b04ba8 --- /dev/null +++ b/Autumn.SPA/src/components/CountrySelector.jsx @@ -0,0 +1,52 @@ +import { useState, useRef, useEffect } from "react"; +import { Globe, ChevronDown } from "lucide-react"; + +export default function CountrySelector({ value, onChange, countries = [], compact }) { + const [open, setOpen] = useState(false); + const selected = countries.find(c => c.code === value); + const wrapRef = useRef(null); + + useEffect(() => { + const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; + document.addEventListener("mousedown", h); + return () => document.removeEventListener("mousedown", h); + }, []); + + return ( +
+ + {open && ( +
+ {countries.map(c => ( + + ))} +
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/Header.jsx b/Autumn.SPA/src/components/Header.jsx new file mode 100644 index 0000000..2a8adae --- /dev/null +++ b/Autumn.SPA/src/components/Header.jsx @@ -0,0 +1,32 @@ +import { Sun, Moon } from "lucide-react"; +import OwlLogo from "./OwlLogo"; +import CountrySelector from "./CountrySelector"; + +export default function Header({ country, setCountry, countries, view, setView, mode, setMode, onReset }) { + return ( +
+
+
+ + HS.Codes +
+
+ +
+ {[["home", "Home"], ["results", "Classify"], ["calculator", "Calculator"], ["browse", "Browse"]].map(([k, l]) => ( + + ))} +
+ +
+
+
+ ); +} diff --git a/Autumn.SPA/src/components/Home.jsx b/Autumn.SPA/src/components/Home.jsx new file mode 100644 index 0000000..7fe9717 --- /dev/null +++ b/Autumn.SPA/src/components/Home.jsx @@ -0,0 +1,55 @@ +import { Search, Calculator, LayoutGrid, Zap, ArrowRight } from "lucide-react"; +import SearchBar from "./SearchBar"; + +function IconBox({ icon: Icon }) { + return ( +
+ +
+ ); +} + +export default function Home({ query, onQueryChange, busy, onSearch, onNavigate }) { + const handleSubmit = (e) => { + e.preventDefault(); + if (query.trim()) onNavigate("results"); + }; + + const cards = [ + { icon: Search, title: "Import Classification", desc: "AI-powered search with predictions, synonym matching, and hierarchical HS code navigation.", go: "results" }, + { icon: Calculator, title: "Duty Calculator", desc: "Calculate import duties, VAT, levies, and total landed cost for your imports.", go: "calculator" }, + { icon: LayoutGrid, title: "Browse HS Codes", desc: "Navigate the complete Harmonized System — 21 sections, 96 chapters, 5000+ subheadings.", go: "browse" }, + ]; + + return ( +
+
+
+ AI-Powered Classification +
+

+ Commodity Codes &
Tariff Classification +

+

+ Classify commodities, calculate duties & taxes, and navigate the Harmonized System. +

+
+ +
+
+
+ {cards.map((c, i) => ( + + ))} +
+
+ ); +} diff --git a/Autumn.SPA/src/components/OwlLogo.jsx b/Autumn.SPA/src/components/OwlLogo.jsx new file mode 100644 index 0000000..e65fa44 --- /dev/null +++ b/Autumn.SPA/src/components/OwlLogo.jsx @@ -0,0 +1,18 @@ +export default function OwlLogo({ size = 30 }) { + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/Autumn.SPA/src/components/SearchBar.jsx b/Autumn.SPA/src/components/SearchBar.jsx new file mode 100644 index 0000000..e2cf924 --- /dev/null +++ b/Autumn.SPA/src/components/SearchBar.jsx @@ -0,0 +1,148 @@ +import { useRef, useEffect, useState, useCallback } from "react"; +import { Search } from "lucide-react"; +import { api } from "../api"; + +export default function SearchBar({ query, onQueryChange, busy, onSubmit, autoFocus, large }) { + const ref = useRef(null); + const debounceRef = useRef(null); + const blurRef = useRef(null); + const [suggestions, setSuggestions] = useState([]); + const [show, setShow] = useState(false); + const [activeIdx, setActiveIdx] = useState(-1); + + useEffect(() => { + if (autoFocus && ref.current) ref.current.focus(); + }, [autoFocus]); + + const fetchSuggestions = useCallback((q) => { + clearTimeout(debounceRef.current); + if (!q || q.trim().length < 2) { + setSuggestions([]); + setShow(false); + return; + } + debounceRef.current = setTimeout(() => { + api.products(q.trim()).then((resp) => { + if (resp.success && resp.results?.length > 0) { + setSuggestions(resp.results.slice(0, 8)); + setShow(true); + } else { + setSuggestions([]); + setShow(false); + } + }).catch(() => { + setSuggestions([]); + setShow(false); + }); + }, 300); + }, []); + + const handleChange = (e) => { + const val = e.target.value; + onQueryChange(val); + setActiveIdx(-1); + fetchSuggestions(val); + }; + + const selectSuggestion = (text) => { + onQueryChange(text); + setSuggestions([]); + setShow(false); + setActiveIdx(-1); + // Auto-submit after selecting + setTimeout(() => { + ref.current?.closest("form")?.requestSubmit(); + }, 0); + }; + + const handleKeyDown = (e) => { + if (!show || suggestions.length === 0) return; + if (e.key === "ArrowDown") { + e.preventDefault(); + setActiveIdx((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setActiveIdx((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1)); + } else if (e.key === "Enter" && activeIdx >= 0) { + e.preventDefault(); + selectSuggestion(suggestions[activeIdx].text); + } else if (e.key === "Escape") { + setShow(false); + setActiveIdx(-1); + } + }; + + const handleBlur = () => { + // Delay to allow click on suggestion + blurRef.current = setTimeout(() => setShow(false), 150); + }; + + const handleFocus = () => { + clearTimeout(blurRef.current); + if (suggestions.length > 0 && query.trim().length >= 2) setShow(true); + }; + + // Highlight the matching portion of a suggestion + const highlight = (text) => { + const q = query.trim().toLowerCase(); + const idx = text.toLowerCase().indexOf(q); + if (idx < 0) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + q.length)} + {text.slice(idx + q.length)} + + ); + }; + + return ( +
+
+
+
+ +
+ + +
+ + {show && suggestions.length > 0 && ( +
+ {suggestions.map((s, i) => ( + + ))} +
+ )} +
+
+ ); +} diff --git a/Autumn.SPA/src/components/SearchView.jsx b/Autumn.SPA/src/components/SearchView.jsx new file mode 100644 index 0000000..a5aa2fe --- /dev/null +++ b/Autumn.SPA/src/components/SearchView.jsx @@ -0,0 +1,311 @@ +import { useState, useEffect } from "react"; +import { ChevronDown, ChevronRight, Calculator } from "lucide-react"; +import { api } from "../api"; +import SearchBar from "./SearchBar"; +import Badge from "./Badge"; + +const RATE_LABELS = { + duty: "Import Duty", vat: "VAT", levy: "Levy", sur: "Surcharge", + etls: "ETL", ciss: "CISS", nac: "NAC", nhil: "NHIL", + getfund: "GETFund", idf: "IDF", rdf: "RDF", +}; + +const RATE_FIELDS = ["duty", "vat", "levy", "sur", "etls", "ciss", "nac", "nhil", "getfund", "idf", "rdf"]; + +function formatTariffEntries(tariff) { + if (!tariff) return []; + return RATE_FIELDS + .filter(f => tariff[f] && tariff[f] !== "0") + .map(f => [RATE_LABELS[f] || f, `${tariff[f]}%`]); +} + +// Deduplicate: remove results whose code is a parent of another result +function deduplicateResults(flat) { + const allParentCodes = new Set(); + flat.forEach(r => { + (r.parentHSCodes || []).forEach(p => allParentCodes.add(p.code)); + }); + return flat.filter(r => { + const code = r.hsCodes?.[0]?.code || r.code; + return !allParentCodes.has(code); + }); +} + +export default function SearchView({ query, onQueryChange, country, countries, setCountry, onCalc }) { + const [busy, setBusy] = useState(false); + const [results, setResults] = useState(null); + const [exp, setExp] = useState(null); + const [noteData, setNoteData] = useState({}); + // drill[idx] = { path: [{code,desc}], items: [...] | null, leaf: noteResp | null, loading: bool } + const [drill, setDrill] = useState({}); + + const doSearch = () => { + if (!query.trim()) return; + setBusy(true); + setResults(null); + setExp(null); + setNoteData({}); + setDrill({}); + api.search(query).then((resp) => { + if (resp.success && resp.records) { + const flat = []; + for (const [src, items] of Object.entries(resp.records)) { + for (const item of items) { + flat.push({ ...item, src }); + } + } + flat.sort((a, b) => b.rating - a.rating); + setResults(deduplicateResults(flat)); + } else { + setResults([]); + } + }).catch(() => setResults([])).finally(() => setBusy(false)); + }; + + // Auto-search on mount if query is present (e.g. coming from Home) + useEffect(() => { + if (query.trim() && !results && !busy) doSearch(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const handleSubmit = (e) => { + e.preventDefault(); + doSearch(); + }; + + // Try to load children using parentId + level; if none, load leaf tariff + const drillLoad = async (idx, parentId, leafCode, path, childLevel) => { + setDrill(prev => ({ ...prev, [idx]: { path, items: null, leaf: null, loading: true } })); + try { + const resp = await api.browse({ parentId, level: childLevel }); + if (resp.success && resp.records?.length > 0) { + setDrill(prev => ({ ...prev, [idx]: { path, items: resp.records.sort((a, b) => a.code.localeCompare(b.code)), leaf: null, loading: false } })); + } else { + // Leaf — load tariff + const noteResp = await api.note(leafCode, country || "NG"); + setDrill(prev => ({ ...prev, [idx]: { path, items: null, leaf: noteResp.success ? noteResp : null, loading: false } })); + } + } catch { + setDrill(prev => ({ ...prev, [idx]: { path, items: [], leaf: null, loading: false } })); + } + }; + + const handleDrillNav = (idx, child) => { + const dd = drill[idx] || { path: [] }; + const lastId = dd.path.length > 0 ? dd.path[dd.path.length - 1].id : (results[idx].hsCodes?.[0]?.id); + if (lastId === child.id) return; + const newPath = [...dd.path, { code: child.code, desc: child.description, level: child.level, id: child.id }]; + drillLoad(idx, child.id, child.code, newPath, child.level + 1); + }; + + const handleExpand = (idx) => { + if (exp === idx) { setExp(null); return; } + setExp(idx); + const r = results[idx]; + const code = r.hsCodes?.[0]?.code || r.code; + const level = r.hsCodes?.[0]?.level || 0; + if (code && country && !noteData[code + country]) { + api.note(code, country).then((resp) => { + if (resp.success) { + setNoteData(prev => ({ ...prev, [code + country]: resp })); + } + }).catch(() => {}); + } + // Auto-load children for non-leaf codes using ID-based navigation + const id = r.hsCodes?.[0]?.id; + if (level < 4 && !drill[idx] && id) { + drillLoad(idx, id, code, [], level + 1); + } + }; + + useEffect(() => { + if (results && exp != null && country) { + const r = results[exp]; + const code = r.hsCodes?.[0]?.code || r.code; + if (code && !noteData[code + country]) { + api.note(code, country).then((resp) => { + if (resp.success) setNoteData(prev => ({ ...prev, [code + country]: resp })); + }).catch(() => {}); + } + } + }, [country]); + + const selCountry = countries.find(c => c.code === country); + + return ( +
+

Classify Commodity

+

Describe your product to get predicted HS codes with confidence scores.

+
+ +
+ + {busy && ( +
+
+

Running classification…

+
+ )} + + {results && !busy && ( +
+
+ + + Found {results.length} classification{results.length !== 1 ? "s" : ""} for "{query}" + +
+
+ {results.map((r, i) => { + const code = r.hsCodes?.[0]?.code || r.code; + const desc = r.hsCodes?.[0]?.description || r.prediction; + const hierarchy = [...(r.parentHSCodes || []), ...(r.hsCodes || [])].sort((a, b) => a.level - b.level); + const nd = country ? noteData[code + country] : null; + const tariffEntries = nd?.tariff?.[0] ? formatTariffEntries(nd.tariff[0]) : []; + + return ( +
+ + {exp === i && (() => { + const dd = drill[i]; + const isLeafResult = (r.hsCodes?.[0]?.level || 0) >= 4; + const drillLeaf = dd?.leaf; + const drillTariffEntries = drillLeaf?.tariff?.[0] ? formatTariffEntries(drillLeaf.tariff[0]) : []; + + // Unified hierarchy: static parents + drill path + const fullHierarchy = [...hierarchy, ...(dd?.path || []).map(p => ({ code: p.code || "", description: p.desc, id: p.id, level: p.level }))]; + // If drill reached a leaf, add it to hierarchy + if (drillLeaf?.records?.[0]) { + const lr = drillLeaf.records[0]; + fullHierarchy.push({ code: lr.code, description: lr.description, id: lr.id, level: lr.level, isLeaf: true }); + } + + // Determine which tariff to show + const showTariff = isLeafResult ? tariffEntries : drillTariffEntries; + const showTariffReady = isLeafResult ? !!nd : !!drillLeaf; + + return ( +
+
HS Code Hierarchy
+
+ {fullHierarchy.map((h, hi) => { + const isLast = hi === fullHierarchy.length - 1 && (isLeafResult || drillLeaf || (!dd?.items)); + const isClickable = hi < fullHierarchy.length - 1 || (dd?.items && !isLast); + // Clicking a hierarchy item navigates back to show its children + const handleClick = () => { + if (hi < hierarchy.length) { + const target = hierarchy[hi]; + const targetId = target.id || r.hsCodes?.[0]?.id; + if (hi === hierarchy.length - 1) { + drillLoad(i, targetId, target.code, [], (target.level || 0) + 1); + } else { + return; + } + } else { + const drillIdx = hi - hierarchy.length; + const target = dd.path[drillIdx]; + const trimmedPath = dd.path.slice(0, drillIdx + 1); + drillLoad(i, target.id, target.code, trimmedPath, target.level + 1); + } + }; + return ( + + ); + })} +
+ + {/* Loading */} + {dd?.loading && ( +
Loading…
+ )} + + {/* Child items below the hierarchy */} + {!isLeafResult && dd?.items && !dd.loading && ( +
+ {dd.items.length === 0 ? ( +
No sub-items found.
+ ) : dd.items.map((child, ci) => ( + + ))} +
+ )} + + {/* Tariffs — shown for leaf results or when drill reaches a leaf */} + {(isLeafResult || drillLeaf) && !dd?.loading && (() => { + const leafCode = isLeafResult ? code : (drillLeaf?.records?.[0]?.code || code); + const leafDesc = isLeafResult ? desc : (drillLeaf?.records?.[0]?.description || desc); + return ( +
+
+
+ Applicable Tariffs {selCountry && — {selCountry.flag} {selCountry.name}} +
+ {onCalc && ( + + )} +
+ {!country ? ( +
Select a country to view tariffs.
+ ) : country !== "NG" ? ( +
Tariff data for {selCountry?.name || country} coming soon.
+ ) : showTariff.length > 0 ? ( +
+ {showTariff.map(([l, v], ti) => ( +
+
{l}
+
{v}
+
+ ))} +
+ ) : showTariffReady ? ( +
No tariff data for this code.
+ ) : ( +
Loading tariff data…
+ )} +
+ ); + })()} +
+ ); + })()} +
+ ); + })} +
+
+ )} +
+ ); +} diff --git a/Autumn.SPA/src/components/TariffPrompt.jsx b/Autumn.SPA/src/components/TariffPrompt.jsx new file mode 100644 index 0000000..b70e7b6 --- /dev/null +++ b/Autumn.SPA/src/components/TariffPrompt.jsx @@ -0,0 +1,12 @@ +import { Globe } from "lucide-react"; +import CountrySelector from "./CountrySelector"; + +export default function TariffPrompt({ onSelect, countries }) { + return ( +
+ + Select a country to view applicable tariffs and duties + +
+ ); +} diff --git a/Autumn.SPA/src/components/Toast.jsx b/Autumn.SPA/src/components/Toast.jsx new file mode 100644 index 0000000..2d685b4 --- /dev/null +++ b/Autumn.SPA/src/components/Toast.jsx @@ -0,0 +1,31 @@ +import { useState, useEffect, useCallback } from "react"; +import { AlertTriangle, X } from "lucide-react"; + +export default function Toast() { + const [visible, setVisible] = useState(false); + const [msg, setMsg] = useState(""); + + const show = useCallback((text) => { + setMsg(text); + setVisible(true); + setTimeout(() => setVisible(false), 5000); + }, []); + + useEffect(() => { + const handler = () => show("Too many requests — please wait a moment before trying again."); + window.addEventListener("api:ratelimit", handler); + return () => window.removeEventListener("api:ratelimit", handler); + }, [show]); + + if (!visible) return null; + + return ( +
+ + {msg} + +
+ ); +} diff --git a/Autumn.SPA/src/data/sections.js b/Autumn.SPA/src/data/sections.js new file mode 100644 index 0000000..4a7ea3a --- /dev/null +++ b/Autumn.SPA/src/data/sections.js @@ -0,0 +1,38 @@ +import { + Fish, Wheat, Droplets, Wine, Pickaxe, Beaker, Recycle, Briefcase, + TreePine, FileText, Shirt, Footprints, Mountain, Gem, Hammer, Cpu, + Ship, Glasses, Target, Armchair, Palette +} from "lucide-react"; + +export const ICON_MAP = { + "01-05": Fish, "06-14": Wheat, "15": Droplets, "16-24": Wine, + "25-27": Pickaxe, "28-38": Beaker, "39-40": Recycle, "41-43": Briefcase, + "44-46": TreePine, "47-49": FileText, "50-63": Shirt, "64-67": Footprints, + "68-70": Mountain, "71": Gem, "72-83": Hammer, "84-85": Cpu, + "86-89": Ship, "90-92": Glasses, "93": Target, "94-96": Armchair, "97": Palette, +}; + +// parentCode maps each section to the Roman numeral used in the DB +export const SECTIONS = [ + { code: "01-05", parentCode: "I", title: "Live Animals & Animal Products" }, + { code: "06-14", parentCode: "II", title: "Vegetable Products" }, + { code: "15", parentCode: "III", title: "Animal or Vegetable Fats & Oils" }, + { code: "16-24", parentCode: "IV", title: "Foodstuffs, Beverages & Tobacco" }, + { code: "25-27", parentCode: "V", title: "Mineral Products" }, + { code: "28-38", parentCode: "VI", title: "Chemical Products" }, + { code: "39-40", parentCode: "VII", title: "Plastics & Rubber" }, + { code: "41-43", parentCode: "VIII", title: "Hides, Skins & Leather" }, + { code: "44-46", parentCode: "IX", title: "Wood & Wood Products" }, + { code: "47-49", parentCode: "X", title: "Paper & Paperboard" }, + { code: "50-63", parentCode: "XI", title: "Textiles & Textile Articles" }, + { code: "64-67", parentCode: "XII", title: "Footwear, Headgear & Umbrellas" }, + { code: "68-70", parentCode: "XIII", title: "Stone, Ceramic & Glass" }, + { code: "71", parentCode: "XIV", title: "Precious Metals & Stones" }, + { code: "72-83", parentCode: "XV", title: "Base Metals & Articles" }, + { code: "84-85", parentCode: "XVI", title: "Machinery & Electrical Equipment" }, + { code: "86-89", parentCode: "XVII", title: "Vehicles, Aircraft & Vessels" }, + { code: "90-92", parentCode: "XVIII", title: "Optical, Medical & Musical Instruments" }, + { code: "93", parentCode: "XIX", title: "Arms & Ammunition" }, + { code: "94-96", parentCode: "XX", title: "Furniture, Toys & Misc. Goods" }, + { code: "97", parentCode: "XXI", title: "Works of Art & Antiques" }, +]; diff --git a/Autumn.SPA/src/main.jsx b/Autumn.SPA/src/main.jsx new file mode 100644 index 0000000..0c51ee7 --- /dev/null +++ b/Autumn.SPA/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './app.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/Autumn.SPA/vite.config.js b/Autumn.SPA/vite.config.js new file mode 100644 index 0000000..bf1bfef --- /dev/null +++ b/Autumn.SPA/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [tailwindcss(), react()], + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:5174' + } + } +}) diff --git a/Autumn.UI/Autumn.UI.csproj b/Autumn.UI/Autumn.UI.csproj index d9197d2..6d70d0e 100644 --- a/Autumn.UI/Autumn.UI.csproj +++ b/Autumn.UI/Autumn.UI.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable 5285dfc4-9770-4f88-8bf7-81ef89605414 diff --git a/Autumn.UIML.Model/Autumn.UIML.Model.csproj b/Autumn.UIML.Model/Autumn.UIML.Model.csproj index 4ad2636..4f2f05d 100644 --- a/Autumn.UIML.Model/Autumn.UIML.Model.csproj +++ b/Autumn.UIML.Model/Autumn.UIML.Model.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net10.0 diff --git a/Autumn.UIML.Model/ConsumeModel.cs b/Autumn.UIML.Model/ConsumeModel.cs index 7249f0a..bf24642 100644 --- a/Autumn.UIML.Model/ConsumeModel.cs +++ b/Autumn.UIML.Model/ConsumeModel.cs @@ -1,5 +1,3 @@ -// This file was auto-generated by ML.NET Model Builder. - using System; using System.Collections.Generic; using System.Linq; @@ -12,38 +10,26 @@ namespace Autumn_UIML.Model { public class ConsumeModel { - // For more info on consuming ML.NET models, visit https://aka.ms/model-builder-consume - // Method for consuming model in your app - public static ModelOutput Predict(ModelInput input) + // Cached prediction engine — loaded once, reused for all predictions + private static readonly Lazy<(PredictionEngine Engine, DataViewSchema Schema)> _cached = new(() => { - - // Create new MLContext - MLContext mlContext = new MLContext(); - - // Load model & create prediction engine + var mlContext = new MLContext(); string modelPath = AppDomain.CurrentDomain.BaseDirectory + "MLModel.zip"; - ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema); - var predEngine = mlContext.Model.CreatePredictionEngine(mlModel); + ITransformer mlModel = mlContext.Model.Load(modelPath, out _); + var engine = mlContext.Model.CreatePredictionEngine(mlModel); + return (engine, engine.OutputSchema); + }); - // Use model to make prediction on input data - ModelOutput result = predEngine.Predict(input); - return result; + public static ModelOutput Predict(ModelInput input) + { + return _cached.Value.Engine.Predict(input); } public static Dictionary Predict(ModelInput input, double threshold) { - - // Create new MLContext - MLContext mlContext = new MLContext(); - - // Load model & create prediction engine - string modelPath = AppDomain.CurrentDomain.BaseDirectory + "MLModel.zip"; - ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema); - var predEngine = mlContext.Model.CreatePredictionEngine(mlModel); - - // Use model to make prediction on input data - ModelOutput result = predEngine.Predict(input); - Dictionary confidence = GetScoresWithLabelsSorted(predEngine.OutputSchema, "Score", result.Score, threshold); + var (engine, schema) = _cached.Value; + ModelOutput result = engine.Predict(input); + Dictionary confidence = GetScoresWithLabelsSorted(schema, "Score", result.Score, threshold); return confidence; } @@ -55,13 +41,11 @@ private static Dictionary GetScoresWithLabelsSorted(DataViewSchem var slotNames = new VBuffer>(); column.Value.GetSlotNames(ref slotNames); - var names = new string[slotNames.Length]; var num = 0; foreach (var denseValue in slotNames.DenseValues()) { float scoreInternal = scores[num++]; - //if (scoreInternal > threshold) - result.Add(denseValue.ToString(), scoreInternal); + result.Add(denseValue.ToString(), scoreInternal); } return result.OrderByDescending(c => c.Value).Take(10).ToDictionary(i => i.Key, i => i.Value); diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b71271 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# Stage 1: Build React SPA +FROM node:20-alpine AS spa-build +WORKDIR /app +COPY Autumn.SPA/package*.json ./ +RUN npm ci +COPY Autumn.SPA/ ./ +RUN npm run build + +# Stage 2: Build .NET API +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS api-build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Autumn.API/Autumn.API.csproj", "Autumn.API/"] +COPY ["Autumn.BL/Autumn.Service.csproj", "Autumn.BL/"] +COPY ["Autumn.Domain/Autumn.Domain.csproj", "Autumn.Domain/"] +COPY ["Autumn.Repository/Autumn.Infrastructure.csproj", "Autumn.Repository/"] +COPY ["Autumn.UIML.Model/Autumn.UIML.Model.csproj", "Autumn.UIML.Model/"] +RUN dotnet restore "Autumn.API/Autumn.API.csproj" +COPY . . +WORKDIR "/src/Autumn.API" +RUN dotnet publish -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# Stage 3: Runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app +COPY --from=api-build /app/publish . +COPY --from=spa-build /app/dist ./wwwroot +EXPOSE 8080 +ENTRYPOINT ["dotnet", "Autumn.API.dll"] diff --git a/README.md b/README.md index cf4e076..cc6c130 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,66 @@ # HS Codes Classification System -A comprehensive .NET application for Harmonized System (HS) code classification and commodity search, featuring AI-powered prediction capabilities and a modern web interface. +A comprehensive .NET application for Harmonized System (HS) code classification and commodity search, featuring AI-powered prediction, LLM-assisted classification, and a modern React SPA. ## Overview -This system provides intelligent HS code classification for international trade, combining traditional database search with machine learning predictions to help users find the most appropriate HS codes for their products. +This system provides intelligent HS code classification for international trade, combining MongoDB Atlas Search, machine learning predictions, and LLM-powered classification (Groq) to help users find the most appropriate HS codes for their products. ## Features -- **AI-Powered Classification**: Machine learning model for automatic HS code prediction -- **Hierarchical Navigation**: Browse HS codes through structured categories -- **Product Search**: Search and classify products with detailed descriptions -- **Customs Tariff Information**: Access tariff rates and import/export requirements +- **Blended Search Pipeline**: Multi-stage search combining exact match, Atlas Search with fuzzy matching, description search, and LLM fallback +- **LLM Classification (Groq)**: Automatic fallback to Llama 3.1 for natural language product queries when database results are low-confidence +- **ML.NET Prediction**: Pre-trained classification model for HS code prediction +- **MongoDB Atlas Search**: Full-text search with fuzzy matching on products and HS code descriptions +- **Modern React SPA**: Tailwind CSS frontend with dark mode, autocomplete, and hierarchy navigation +- **Duty Calculator**: Import duty and tax calculation with multi-country support +- **Hierarchical Navigation**: Browse HS codes through structured categories (Section > Chapter > Heading) +- **Rate Limiting**: Per-IP request throttling to protect the API +- **Search Analytics**: Groq prediction logging with accuracy tracking +- **Auth0 Authentication**: Secured admin endpoints with JWT - **Currency Exchange Rates**: Real-time currency conversion support -- **Document Management**: Store and manage classification documents -- **Search Analytics**: Track and log search patterns for optimization -- **Multi-Environment Support**: Development, staging, and production configurations ## Architecture -The application follows a clean architecture pattern with the following layers: +The application follows a clean architecture pattern: -- **Autumn.UI**: Web application with Razor Pages -- **Autumn.API**: RESTful API with Swagger documentation -- **Autumn.BL**: Business logic and services -- **Autumn.Domain**: Domain models and entities -- **Autumn.Repository**: Data access layer -- **Autumn.UIML.Model**: Machine learning model for predictions +``` +├── Autumn.SPA/ # React SPA (Vite + Tailwind CSS) +├── Autumn.API/ # Minimal API endpoints (.NET 10) +├── Autumn.BL/ # Business logic & services +├── Autumn.Domain/ # Domain models & entities +├── Autumn.Repository/ # MongoDB & SQL data access +├── Autumn.UIML.Model/ # ML.NET prediction model +└── docker-compose.yml # Container orchestration +``` ## Technology Stack -- **.NET 9.0**: Core framework -- **ASP.NET Core**: Web framework -- **MongoDB**: Document database for HS codes and products +- **.NET 10.0**: Core framework +- **ASP.NET Core Minimal APIs**: RESTful endpoints with Swagger +- **React 19 + Vite**: Single-page application +- **Tailwind CSS 4**: Utility-first styling with light/dark theme +- **MongoDB Atlas**: Document database with Atlas Search - **SQL Server**: Relational database for structured data -- **ML.NET**: Machine learning framework -- **AutoMapper**: Object mapping -- **Auth0**: Authentication and authorization -- **Docker**: Containerization -- **Nginx**: Reverse proxy and load balancer +- **ML.NET 1.5**: Machine learning classification +- **Groq API (Llama 3.1 8B)**: LLM-powered HS code classification fallback +- **Auth0**: JWT authentication and authorization +- **Docker + Nginx**: Containerization and reverse proxy ## Prerequisites -- .NET 9.0 SDK -- MongoDB -- SQL Server +- .NET 10.0 SDK +- Node.js 18+ +- MongoDB (Atlas recommended for full-text search) - Docker (optional) ## Quick Start ### Using Docker (Recommended) -1. Clone the repository: - ```bash -git clone +git clone https://github.com/samabos/hscodesdotnet.git cd hscodesdotnet -``` - -2. Start the application: - -```bash docker-compose up -d ``` @@ -68,153 +68,145 @@ The application will be available at `http://localhost` ### Manual Setup -1. **Configure Databases**: - - - Set up MongoDB instance - - Configure SQL Server connection - - Update connection strings in `appsettings.json` - -2. **Build and Run**: - -```bash -dotnet restore -dotnet build -dotnet run --project Autumn.UI -``` - -3. **API Documentation**: - - Navigate to `/swagger` for API documentation - - Available at `http://localhost:5000/swagger` - -## Configuration - -### Database Settings - -Update the following in `appsettings.json`: +1. **Configure Settings** — Update `Autumn.API/appsettings.json`: ```json { "StoreDatabaseSettings": { - "ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb+srv://...", "DatabaseName": "ClassificationDb" }, - "ConnectionStrings": { - "DefaultConnection": "your-sql-server-connection-string" + "SiteSettings": { + "Threshold": "0.1", + "GroqApiKey": "your-groq-api-key", + "GroqModel": "llama-3.1-8b-instant" + }, + "Auth0": { + "Domain": "https://your-auth0-domain.auth0.com/", + "Audience": "autumnapi" } } ``` -### Authentication +2. **Run the API**: -Configure Auth0 settings: +```bash +dotnet run --project Autumn.API +``` -```json -{ - "Auth0": { - "Domain": "your-auth0-domain", - "ClientId": "your-client-id" - } -} +3. **Run the SPA** (development): + +```bash +cd Autumn.SPA +npm install +npm run dev ``` -## API Endpoints +4. **API Documentation**: Navigate to `/swagger` + +## Search Pipeline -### Search Operations +The search system uses a blended multi-stage approach: -- `GET /api/v1/search` - Search HS codes -- `GET /api/v1/classify/commodity` - Classify commodity -- `GET /api/v1/note` - Get classification notes +| Stage | Source | Confidence | Description | +|-------|--------|-----------|-------------| +| 1 | Exact Match | 0.88–0.97 | Direct keyword match in products collection | +| 2 | Atlas Search | 0.60–0.82 | Fuzzy full-text search with word splitting | +| 3 | Description Search | 0.40–0.73 | Atlas Search on HS code descriptions (Level 3–4) | +| 4 | Groq LLM | 0.45–0.75 | Llama 3.1 classification (fallback when best result < 0.70) | +| 5 | Synonyms | 0.35–0.58 | RapidAPI synonym expansion (fallback) | +| 6 | ML.NET Model | Variable | Pre-trained classifier (last resort) | -### Data Management +Stages 1–3 run concurrently. Stage 4 only triggers when no high-confidence results are found. Results are deduplicated by HS code, keeping the highest-confidence entry. -- `GET /api/v1/hscode` - Retrieve HS codes -- `GET /api/v1/product` - Product information -- `GET /api/v1/currency` - Currency exchange rates +## API Endpoints + +### Public Endpoints (rate limited: 30 req/min per IP) -## Machine Learning +- `GET /api/search` — Search and classify products +- `GET /api/browse` — Browse HS code hierarchy +- `GET /api/duty` — Calculate import duties and taxes +- `GET /api/note/{hscode}` — Get notes, documents, and tariffs +- `GET /api/codelist/countries` — List countries +- `GET /api/codelist/currency` — Currency exchange rates +- `GET /api/codelist/products/{query?}` — Product autocomplete -The system includes a pre-trained ML model for HS code prediction: +### Admin Endpoints (requires Auth0 JWT) -- **Model**: ML.NET classification model -- **Input**: Product descriptions and keywords -- **Output**: Predicted HS codes with confidence scores -- **Threshold**: Configurable confidence threshold (default: 0.02) +- `GET /api/admin/dashboard` — Dashboard statistics +- CRUD: `/api/admin/products`, `/api/admin/codes`, `/api/admin/tariffs` +- `GET /api/admin/querylogs` — Search analytics -## Development +## MongoDB Atlas Search Indexes -### Project Structure +Create these indexes on your Atlas cluster for optimal search: +**`product-index`** on `products` collection: +```json +{ "mappings": { "dynamic": true, "fields": { "Keyword": { "type": "string", "analyzer": "lucene.standard" } } } } ``` -├── Autumn.UI/ # Web application -├── Autumn.API/ # REST API -├── Autumn.BL/ # Business logic -├── Autumn.Domain/ # Domain models -├── Autumn.Repository/ # Data access -├── Autumn.UIML.Model/ # ML model -└── docker-compose.yml # Container orchestration + +**`hscodes-index`** on `hscodes` collection: +```json +{ "mappings": { "dynamic": true, "fields": { "Description": { "type": "string", "analyzer": "lucene.standard" }, "Level": { "type": "number" } } } } ``` -### Adding New Features +## Groq LLM Integration -1. Create domain models in `Autumn.Domain` -2. Implement repository interfaces in `Autumn.Repository` -3. Add business logic in `Autumn.BL` -4. Create API controllers in `Autumn.API` -5. Update UI pages in `Autumn.UI` +The system uses Groq's free API tier (Llama 3.1 8B) as a smart fallback: -## Deployment +- Only called when database stages return low-confidence results (< 0.70) +- Predictions are logged to `SearchLog` with `Source = "groq"` and `FoundInDb` flag for accuracy tracking +- Free tier: 14,400 requests/day, 30 requests/minute +- Get your API key at [console.groq.com](https://console.groq.com) -### Production Deployment +## Deployment -1. **Build Docker Image**: +### Production with Docker ```bash docker build -t hscodes:latest . -``` - -2. **Deploy with Docker Compose**: - -```bash docker-compose -f docker-compose.prod.yml up -d ``` -3. **Configure Nginx**: - - Update `nginx.conf` for your domain - - Set up SSL certificates - - Configure load balancing - ### Environment Variables -Set the following environment variables for production: - - `ASPNETCORE_ENVIRONMENT=Production` -- `ConnectionStrings__DefaultConnection` - `StoreDatabaseSettings__ConnectionString` +- `SiteSettings__GroqApiKey` - `Auth0__Domain` -- `Auth0__ClientId` +- `Auth0__Audience` ## Contributing 1. Fork the repository 2. Create a feature branch 3. Make your changes -4. Add tests if applicable -5. Submit a pull request +4. Submit a pull request ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -## Support - -For support and questions: - -- Create an issue in the repository -- Contact the development team -- Check the API documentation at `/swagger` - ## Changelog +### Version 3.0 + +- Upgraded to .NET 10.0 with Minimal APIs +- New React 19 SPA with Tailwind CSS 4 (replaces Razor Pages) +- Light/dark theme with modern UI +- MongoDB Atlas Search with fuzzy matching +- Groq LLM integration (Llama 3.1 8B) as smart classification fallback +- Blended concurrent search pipeline (exact + Atlas + description + LLM) +- Groq prediction logging with accuracy tracking (`Source`, `FoundInDb` fields) +- Per-IP rate limiting (30 req/min) +- Rate limit toast notifications in frontend +- Product autocomplete with debounced search +- Hierarchy-style browse navigation +- Import duty calculator with multi-country support +- Cached ML.NET model (Lazy singleton) +- Tokenized regex fallback for searches with spaces/hyphens + ### Version 1.09 - Updated to .NET 9.0 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8d92dfa..e728eb2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest @@ -7,7 +5,7 @@ services: restart: always ports: - "80:80" # Map HTTP traffic to NGINX - - "443:443" # Optional: Map HTTPS traffic if using SSL + - "443:443" # Map HTTPS traffic if using SSLtest volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro # Mount custom NGINX config - /etc/ssl/hs.codes:/etc/ssl/hs.codes:ro # Mount SSL files from VPS @@ -18,16 +16,19 @@ services: - autumn-ui-network web: - image: "sageprojects/hscodes:${GITHUB_RUN_NUMBER}" # Replace with your actual DockerHub image name + image: "sageprojects/hscodes:${GITHUB_RUN_NUMBER}" container_name: autumn-ui-web restart: always expose: - - "8080" # Expose app port to the NGINX service + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Production - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} - Auth0__Domain=${AUTH0_DOMAIN} - - Auth0__ClientId=${AUTH0_CLIENTID} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 4072e34..7689f1c 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest @@ -19,12 +17,15 @@ services: container_name: autumn-ui-web-staging restart: always expose: - - "9090" + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Staging - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} - Auth0__Domain=${AUTH0_DOMAIN} - - Auth0__ClientId=${AUTH0_CLIENTID} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/docker-compose.yml b/docker-compose.yml index 3919f0b..3a5d986 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: nginx: image: nginx:latest @@ -17,13 +15,19 @@ services: - autumn-ui-network web: - image: "sageprojects/hscodes:1.09" # Replace with your actual DockerHub image name + build: . container_name: autumn-ui-web restart: always expose: - - "8080" # Expose app port to the NGINX service + - "8080" environment: - ASPNETCORE_ENVIRONMENT=Development + - StoreDatabaseSettings__ConnectionString=${CONNECTION_STRING} + - Auth0__Domain=${AUTH0_DOMAIN} + - Auth0__Audience=${AUTH0_AUDIENCE} + - SiteSettings__GroqApiKey=${GROQAPIKEY} + - SiteSettings__GroqModel=llama-3.1-8b-instant + - SiteSettings__Threshold=0.1 networks: - autumn-ui-network diff --git a/nginx-staging.conf b/nginx-staging.conf index 2015662..f42df02 100644 --- a/nginx-staging.conf +++ b/nginx-staging.conf @@ -1,8 +1,9 @@ server { - listen 8443; + listen 80; + # Proxy all traffic to the .NET app (serves both API and SPA) location / { - proxy_pass http://web:9090; # web is the service name, and 8080 is the exposed port inside the container + proxy_pass http://web:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/nginx.conf b/nginx.conf index a334ddc..65f2ea9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,6 +1,6 @@ server { listen 80; - server_name www.hs.codes; + server_name www.hs.codes hs.codes; # Redirect all HTTP traffic to HTTPS return 301 https://$host$request_uri; @@ -8,21 +8,21 @@ server { server { listen 443 ssl; - server_name www.hs.codes; + server_name www.hs.codes hs.codes; # SSL Certificate files - ssl_certificate /etc/ssl/hs.codes/hs.codes.crt; # Path to your certificate file - ssl_certificate_key /etc/ssl/hs.codes/hs.codes.key; # Path to your private key - ssl_trusted_certificate /etc/ssl/hs.codes/ca_bundle.crt; # Path to the CA bundle + ssl_certificate /etc/ssl/hs.codes/hs.codes.crt; + ssl_certificate_key /etc/ssl/hs.codes/hs.codes.key; + ssl_trusted_certificate /etc/ssl/hs.codes/ca_bundle.crt; - # Security and Protocols (recommended settings) + # Security and Protocols ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; - # Proxy configuration + # Proxy all traffic to the .NET app (serves both API and SPA) location / { - proxy_pass http://web:8080; # Forward traffic to your backend + proxy_pass http://web:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;