commit
4eaabe1315
26 changed files with 4275 additions and 726 deletions
1
.env.production
Normal file
1
.env.production
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_API_BASE_URL=https://betapartner-api.tradhox.com/api
|
||||
12
cdk.json
Normal file
12
cdk.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"app": "npx ts-node --prefer-ts-exts cdk/app.ts",
|
||||
"watch": {
|
||||
"include": [
|
||||
"cdk/**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk/cdk.out"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
import 'source-map-support/register.js';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import { BetaSupplierSiteStack } from './betasupplier-site-stack.js';
|
||||
import { BetaSupplierSiteStack } from './betasupplier-site-stack';
|
||||
|
||||
const app = new cdk.App();
|
||||
new BetaSupplierSiteStack(app, 'BetaSupplierSiteStack', {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class BetaSupplierSiteStack extends cdk.Stack {
|
|||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
});
|
||||
|
||||
const ec2Origin = new origins.HttpOrigin('api.tradhox.com', {
|
||||
const ec2Origin = new origins.HttpOrigin('betapartner-api.tradhox.com', {
|
||||
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
|
||||
httpPort: 8080,
|
||||
});
|
||||
|
|
|
|||
6
cdk/cdk.context.json
Normal file
6
cdk/cdk.context.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"hosted-zone:account=764709663363:domainName=tradhox.com:region=ap-south-2": {
|
||||
"Id": "/hostedzone/Z05079927Q6UCDIWAH4E",
|
||||
"Name": "tradhox.com."
|
||||
}
|
||||
}
|
||||
12
cdk/cdk.json
Normal file
12
cdk/cdk.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"app": "npx tsx app.ts",
|
||||
"watch": {
|
||||
"include": [
|
||||
"**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk.out"
|
||||
]
|
||||
}
|
||||
}
|
||||
51
cdk/cdk.out/BetaSupplierSiteStack.assets.json
Normal file
51
cdk/cdk.out/BetaSupplierSiteStack.assets.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"version": "54.0.0",
|
||||
"files": {
|
||||
"faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6": {
|
||||
"displayName": "BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider Code",
|
||||
"source": {
|
||||
"path": "asset.faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6",
|
||||
"packaging": "zip"
|
||||
},
|
||||
"destinations": {
|
||||
"764709663363-ap-south-2-41ddd48c": {
|
||||
"bucketName": "cdk-hnb659fds-assets-764709663363-ap-south-2",
|
||||
"objectKey": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip",
|
||||
"region": "ap-south-2",
|
||||
"assumeRoleArn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-file-publishing-role-764709663363-ap-south-2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ffb21b7e396cc684c46812aea1d2ff7ea22401e05038c047ae9f071082c68586": {
|
||||
"displayName": "SiteCertificate/CertificateRequestorFunction/Code",
|
||||
"source": {
|
||||
"path": "asset.ffb21b7e396cc684c46812aea1d2ff7ea22401e05038c047ae9f071082c68586",
|
||||
"packaging": "zip"
|
||||
},
|
||||
"destinations": {
|
||||
"764709663363-ap-south-2-89e47e6b": {
|
||||
"bucketName": "cdk-hnb659fds-assets-764709663363-ap-south-2",
|
||||
"objectKey": "ffb21b7e396cc684c46812aea1d2ff7ea22401e05038c047ae9f071082c68586.zip",
|
||||
"region": "ap-south-2",
|
||||
"assumeRoleArn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-file-publishing-role-764709663363-ap-south-2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"7ed3cfff7120b7b31997b41625d0f260352266b5eaa8c56533cf1f509441f2e7": {
|
||||
"displayName": "BetaSupplierSiteStack Template",
|
||||
"source": {
|
||||
"path": "BetaSupplierSiteStack.template.json",
|
||||
"packaging": "file"
|
||||
},
|
||||
"destinations": {
|
||||
"764709663363-ap-south-2-2ab9b314": {
|
||||
"bucketName": "cdk-hnb659fds-assets-764709663363-ap-south-2",
|
||||
"objectKey": "7ed3cfff7120b7b31997b41625d0f260352266b5eaa8c56533cf1f509441f2e7.json",
|
||||
"region": "ap-south-2",
|
||||
"assumeRoleArn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-file-publishing-role-764709663363-ap-south-2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dockerImages": {}
|
||||
}
|
||||
288
cdk/cdk.out/BetaSupplierSiteStack.metadata.json
Normal file
288
cdk/cdk.out/BetaSupplierSiteStack.metadata.json
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
{
|
||||
"/BetaSupplierSiteStack": [
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider": [
|
||||
{
|
||||
"type": "aws:cdk:is-custom-resource-handler-customResourceProvider",
|
||||
"data": true
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteBucketName": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteBucketName"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:80:5)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteCloudFrontDomainName": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteCloudFrontDomainName"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:85:5)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/AWSCloudFrontPartitionHostedZoneIdMap": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "AWSCloudFrontPartitionHostedZoneIdMap"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new ARecord2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:91:5)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BootstrapVersion": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BootstrapVersion"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/CheckBootstrapVersion": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "CheckBootstrapVersion"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteBucket/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteBucketC3053F9A"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new Bucket2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:15:27)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:analytics:mixin",
|
||||
"data": {
|
||||
"mixin": "aws-cdk-lib.aws_s3.mixins.BucketAutoDeleteObjects"
|
||||
}
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction": [
|
||||
{
|
||||
"type": "aws:cdk:is-custom-resource-handler-runtime-family",
|
||||
"data": 0
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteDistribution/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteDistributionD03E58A9"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new Distribution2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:43:26)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteAliasRecord/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "SiteAliasRecord3C0AF5BF"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new ARecord2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:91:5)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/CDKMetadata/Default": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "CDKMetadata"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib, node internals...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteBucket/Policy/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteBucketPolicy7E1C9F1E"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new Bucket2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:15:27)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:analytics:mixin",
|
||||
"data": {
|
||||
"mixin": "aws-cdk-lib.aws_s3.mixins.BucketPolicyStatements"
|
||||
}
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteBucket/AutoDeleteObjectsCustomResource/Default": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteBucketAutoDeleteObjectsCustomResource789310C3"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...aws-cdk-lib...",
|
||||
"(no user code in 10 frames, use --stack-trace-limit to capture more)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "SiteCertificateCertificateRequestorFunction7CFA7DEA"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new DnsValidatedCertificate2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:34:25)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteCertificate/CertificateRequestorResource/Default": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "SiteCertificateCertificateRequestorResource6021082A"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new DnsValidatedCertificate2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:34:25)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/ServiceRole/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "SiteCertificateCertificateRequestorFunctionServiceRole645E891D"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new DnsValidatedCertificate2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:34:25)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/BetaSupplierSiteDistribution/Origin1/S3OriginAccessControl/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "BetaSupplierSiteDistributionOrigin1S3OriginAccessControlF9377C01"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new Distribution2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:43:26)",
|
||||
"<anonymous> (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\app.ts:6:1)",
|
||||
"...node internals..."
|
||||
]
|
||||
}
|
||||
],
|
||||
"/BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource": [
|
||||
{
|
||||
"type": "aws:cdk:logicalId",
|
||||
"data": "SiteCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy96ED5C9C"
|
||||
},
|
||||
{
|
||||
"type": "aws:cdk:creationStack",
|
||||
"data": [
|
||||
"...new DnsValidatedCertificate2 in aws-cdk-lib...",
|
||||
"new BetaSupplierSiteStack (D:\\Tradhox_workspace\\supplier_central_frontend\\cdk\\betasupplier-site-stack.ts:34:25)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
584
cdk/cdk.out/BetaSupplierSiteStack.template.json
Normal file
584
cdk/cdk.out/BetaSupplierSiteStack.template.json
Normal file
|
|
@ -0,0 +1,584 @@
|
|||
{
|
||||
"Resources": {
|
||||
"BetaSupplierSiteBucketC3053F9A": {
|
||||
"Type": "AWS::S3::Bucket",
|
||||
"Properties": {
|
||||
"BucketEncryption": {
|
||||
"ServerSideEncryptionConfiguration": [
|
||||
{
|
||||
"ServerSideEncryptionByDefault": {
|
||||
"SSEAlgorithm": "AES256"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"BucketName": "betasuppliersite-764709663363-ap-south-2",
|
||||
"PublicAccessBlockConfiguration": {
|
||||
"BlockPublicAcls": true,
|
||||
"BlockPublicPolicy": true,
|
||||
"IgnorePublicAcls": true,
|
||||
"RestrictPublicBuckets": true
|
||||
},
|
||||
"Tags": [
|
||||
{
|
||||
"Key": "aws-cdk:auto-delete-objects",
|
||||
"Value": "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
"UpdateReplacePolicy": "Delete",
|
||||
"DeletionPolicy": "Delete",
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/BetaSupplierSiteBucket/Resource"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteBucketPolicy7E1C9F1E": {
|
||||
"Type": "AWS::S3::BucketPolicy",
|
||||
"Properties": {
|
||||
"Bucket": {
|
||||
"Ref": "BetaSupplierSiteBucketC3053F9A"
|
||||
},
|
||||
"PolicyDocument": {
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"s3:PutBucketPolicy",
|
||||
"s3:GetBucket*",
|
||||
"s3:List*",
|
||||
"s3:DeleteObject*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": {
|
||||
"Fn::GetAtt": [
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
|
||||
"Arn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Resource": [
|
||||
{
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteBucketC3053F9A",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
{
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteBucketC3053F9A",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"/*"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Action": "s3:GetObject",
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"AWS:SourceArn": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:",
|
||||
{
|
||||
"Ref": "AWS::Partition"
|
||||
},
|
||||
":cloudfront::",
|
||||
{
|
||||
"Ref": "AWS::AccountId"
|
||||
},
|
||||
":distribution/",
|
||||
{
|
||||
"Ref": "BetaSupplierSiteDistributionD03E58A9"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Service": "cloudfront.amazonaws.com"
|
||||
},
|
||||
"Resource": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
{
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteBucketC3053F9A",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"/*"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/BetaSupplierSiteBucket/Policy/Resource"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteBucketAutoDeleteObjectsCustomResource789310C3": {
|
||||
"Type": "Custom::S3AutoDeleteObjects",
|
||||
"Properties": {
|
||||
"ServiceToken": {
|
||||
"Fn::GetAtt": [
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"BucketName": {
|
||||
"Ref": "BetaSupplierSiteBucketC3053F9A"
|
||||
}
|
||||
},
|
||||
"DependsOn": [
|
||||
"BetaSupplierSiteBucketPolicy7E1C9F1E"
|
||||
],
|
||||
"UpdateReplacePolicy": "Delete",
|
||||
"DeletionPolicy": "Delete",
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/BetaSupplierSiteBucket/AutoDeleteObjectsCustomResource/Default"
|
||||
}
|
||||
},
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": {
|
||||
"Type": "AWS::IAM::Role",
|
||||
"Properties": {
|
||||
"AssumeRolePolicyDocument": {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": "sts:AssumeRole",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Service": "lambda.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ManagedPolicyArns": [
|
||||
{
|
||||
"Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role"
|
||||
}
|
||||
},
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": {
|
||||
"Type": "AWS::Lambda::Function",
|
||||
"Properties": {
|
||||
"Code": {
|
||||
"S3Bucket": "cdk-hnb659fds-assets-764709663363-ap-south-2",
|
||||
"S3Key": "faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6.zip"
|
||||
},
|
||||
"Timeout": 900,
|
||||
"MemorySize": 128,
|
||||
"Handler": "index.handler",
|
||||
"Role": {
|
||||
"Fn::GetAtt": [
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"Runtime": "nodejs24.x",
|
||||
"Description": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"Lambda function for auto-deleting objects in ",
|
||||
{
|
||||
"Ref": "BetaSupplierSiteBucketC3053F9A"
|
||||
},
|
||||
" S3 bucket."
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"DependsOn": [
|
||||
"CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092"
|
||||
],
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler",
|
||||
"aws:asset:path": "asset.faa95a81ae7d7373f3e1f242268f904eb748d8d0fdd306e8a6fe515a1905a7d6",
|
||||
"aws:asset:property": "Code"
|
||||
}
|
||||
},
|
||||
"SiteCertificateCertificateRequestorFunctionServiceRole645E891D": {
|
||||
"Type": "AWS::IAM::Role",
|
||||
"Properties": {
|
||||
"AssumeRolePolicyDocument": {
|
||||
"Statement": [
|
||||
{
|
||||
"Action": "sts:AssumeRole",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"Service": "lambda.amazonaws.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
},
|
||||
"ManagedPolicyArns": [
|
||||
{
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:",
|
||||
{
|
||||
"Ref": "AWS::Partition"
|
||||
},
|
||||
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/ServiceRole/Resource"
|
||||
}
|
||||
},
|
||||
"SiteCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy96ED5C9C": {
|
||||
"Type": "AWS::IAM::Policy",
|
||||
"Properties": {
|
||||
"PolicyDocument": {
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"acm:RequestCertificate",
|
||||
"acm:DescribeCertificate",
|
||||
"acm:DeleteCertificate",
|
||||
"acm:AddTagsToCertificate"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Action": "route53:GetChange",
|
||||
"Effect": "Allow",
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Action": "route53:changeResourceRecordSets",
|
||||
"Condition": {
|
||||
"ForAllValues:StringEquals": {
|
||||
"route53:ChangeResourceRecordSetsRecordTypes": [
|
||||
"CNAME"
|
||||
],
|
||||
"route53:ChangeResourceRecordSetsActions": [
|
||||
"UPSERT"
|
||||
]
|
||||
},
|
||||
"ForAllValues:StringLike": {
|
||||
"route53:ChangeResourceRecordSetsNormalizedRecordNames": [
|
||||
"*.betapartners.tradhox.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Effect": "Allow",
|
||||
"Resource": {
|
||||
"Fn::Join": [
|
||||
"",
|
||||
[
|
||||
"arn:",
|
||||
{
|
||||
"Ref": "AWS::Partition"
|
||||
},
|
||||
":route53:::hostedzone/Z05079927Q6UCDIWAH4E"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": "2012-10-17"
|
||||
},
|
||||
"PolicyName": "SiteCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy96ED5C9C",
|
||||
"Roles": [
|
||||
{
|
||||
"Ref": "SiteCertificateCertificateRequestorFunctionServiceRole645E891D"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/ServiceRole/DefaultPolicy/Resource"
|
||||
}
|
||||
},
|
||||
"SiteCertificateCertificateRequestorFunction7CFA7DEA": {
|
||||
"Type": "AWS::Lambda::Function",
|
||||
"Properties": {
|
||||
"Code": {
|
||||
"S3Bucket": "cdk-hnb659fds-assets-764709663363-ap-south-2",
|
||||
"S3Key": "ffb21b7e396cc684c46812aea1d2ff7ea22401e05038c047ae9f071082c68586.zip"
|
||||
},
|
||||
"Handler": "index.certificateRequestHandler",
|
||||
"Role": {
|
||||
"Fn::GetAtt": [
|
||||
"SiteCertificateCertificateRequestorFunctionServiceRole645E891D",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"Runtime": "nodejs24.x",
|
||||
"Timeout": 900
|
||||
},
|
||||
"DependsOn": [
|
||||
"SiteCertificateCertificateRequestorFunctionServiceRoleDefaultPolicy96ED5C9C",
|
||||
"SiteCertificateCertificateRequestorFunctionServiceRole645E891D"
|
||||
],
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/SiteCertificate/CertificateRequestorFunction/Resource",
|
||||
"aws:asset:path": "asset.ffb21b7e396cc684c46812aea1d2ff7ea22401e05038c047ae9f071082c68586",
|
||||
"aws:asset:is-bundled": false,
|
||||
"aws:asset:property": "Code"
|
||||
}
|
||||
},
|
||||
"SiteCertificateCertificateRequestorResource6021082A": {
|
||||
"Type": "AWS::CloudFormation::CustomResource",
|
||||
"Properties": {
|
||||
"ServiceToken": {
|
||||
"Fn::GetAtt": [
|
||||
"SiteCertificateCertificateRequestorFunction7CFA7DEA",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"DomainName": "betapartners.tradhox.com",
|
||||
"HostedZoneId": "Z05079927Q6UCDIWAH4E",
|
||||
"Region": "us-east-1"
|
||||
},
|
||||
"UpdateReplacePolicy": "Delete",
|
||||
"DeletionPolicy": "Delete",
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/SiteCertificate/CertificateRequestorResource/Default"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteDistributionOrigin1S3OriginAccessControlF9377C01": {
|
||||
"Type": "AWS::CloudFront::OriginAccessControl",
|
||||
"Properties": {
|
||||
"OriginAccessControlConfig": {
|
||||
"Name": "BetaSupplierSiteStackBetaSupOrigin1S3OriginAccessControl50127AEA",
|
||||
"OriginAccessControlOriginType": "s3",
|
||||
"SigningBehavior": "always",
|
||||
"SigningProtocol": "sigv4"
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/BetaSupplierSiteDistribution/Origin1/S3OriginAccessControl/Resource"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteDistributionD03E58A9": {
|
||||
"Type": "AWS::CloudFront::Distribution",
|
||||
"Properties": {
|
||||
"DistributionConfig": {
|
||||
"Aliases": [
|
||||
"betapartners.tradhox.com"
|
||||
],
|
||||
"CacheBehaviors": [
|
||||
{
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"POST",
|
||||
"DELETE"
|
||||
],
|
||||
"CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
|
||||
"Compress": true,
|
||||
"OriginRequestPolicyId": "216adef6-5c7f-47e4-b989-5492eafa07d3",
|
||||
"PathPattern": "/api/*",
|
||||
"TargetOriginId": "BetaSupplierSiteStackBetaSupplierSiteDistributionOrigin26FE0C2F5",
|
||||
"ViewerProtocolPolicy": "redirect-to-https"
|
||||
}
|
||||
],
|
||||
"CustomErrorResponses": [
|
||||
{
|
||||
"ErrorCachingMinTTL": 0,
|
||||
"ErrorCode": 404,
|
||||
"ResponseCode": 200,
|
||||
"ResponsePagePath": "/index.html"
|
||||
},
|
||||
{
|
||||
"ErrorCachingMinTTL": 0,
|
||||
"ErrorCode": 403,
|
||||
"ResponseCode": 200,
|
||||
"ResponsePagePath": "/index.html"
|
||||
}
|
||||
],
|
||||
"DefaultCacheBehavior": {
|
||||
"AllowedMethods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
|
||||
"CachedMethods": [
|
||||
"GET",
|
||||
"HEAD"
|
||||
],
|
||||
"Compress": true,
|
||||
"TargetOriginId": "BetaSupplierSiteStackBetaSupplierSiteDistributionOrigin118CF032F",
|
||||
"ViewerProtocolPolicy": "redirect-to-https"
|
||||
},
|
||||
"DefaultRootObject": "index.html",
|
||||
"Enabled": true,
|
||||
"HttpVersion": "http2",
|
||||
"IPV6Enabled": true,
|
||||
"Origins": [
|
||||
{
|
||||
"DomainName": {
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteBucketC3053F9A",
|
||||
"RegionalDomainName"
|
||||
]
|
||||
},
|
||||
"Id": "BetaSupplierSiteStackBetaSupplierSiteDistributionOrigin118CF032F",
|
||||
"OriginAccessControlId": {
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteDistributionOrigin1S3OriginAccessControlF9377C01",
|
||||
"Id"
|
||||
]
|
||||
},
|
||||
"S3OriginConfig": {
|
||||
"OriginAccessIdentity": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"CustomOriginConfig": {
|
||||
"HTTPPort": 8080,
|
||||
"OriginProtocolPolicy": "http-only",
|
||||
"OriginSSLProtocols": [
|
||||
"TLSv1.2"
|
||||
]
|
||||
},
|
||||
"DomainName": "betapartner-api.tradhox.com",
|
||||
"Id": "BetaSupplierSiteStackBetaSupplierSiteDistributionOrigin26FE0C2F5"
|
||||
}
|
||||
],
|
||||
"ViewerCertificate": {
|
||||
"AcmCertificateArn": {
|
||||
"Fn::GetAtt": [
|
||||
"SiteCertificateCertificateRequestorResource6021082A",
|
||||
"Arn"
|
||||
]
|
||||
},
|
||||
"MinimumProtocolVersion": "TLSv1.2_2021",
|
||||
"SslSupportMethod": "sni-only"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/BetaSupplierSiteDistribution/Resource"
|
||||
}
|
||||
},
|
||||
"SiteAliasRecord3C0AF5BF": {
|
||||
"Type": "AWS::Route53::RecordSet",
|
||||
"Properties": {
|
||||
"AliasTarget": {
|
||||
"DNSName": {
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteDistributionD03E58A9",
|
||||
"DomainName"
|
||||
]
|
||||
},
|
||||
"HostedZoneId": {
|
||||
"Fn::FindInMap": [
|
||||
"AWSCloudFrontPartitionHostedZoneIdMap",
|
||||
{
|
||||
"Ref": "AWS::Partition"
|
||||
},
|
||||
"zoneId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"HostedZoneId": "Z05079927Q6UCDIWAH4E",
|
||||
"Name": "betapartners.tradhox.com.",
|
||||
"Type": "A"
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/SiteAliasRecord/Resource"
|
||||
}
|
||||
},
|
||||
"CDKMetadata": {
|
||||
"Type": "AWS::CDK::Metadata",
|
||||
"Properties": {
|
||||
"Analytics": "v2:deflate64:H4sIAAAAAAAA/5VRQU7DMBB8S3N2DSQgJG5pKm6oVSJxQahynG20rWNX3nULsvJ3lETQcODAacY7O6PROpXpYypvF+pCS90clwZrGStW+ijUhXaRMhlXQR+BRbG3E3uLSYcfaJOnZG4b9imTo0RyWs0DuzUYYNjUB9BMSf8uJmnrDOrPa+r0/kf2ZKhYMXRg59ErRdALDZ5xj3rQlVUteLm29KoMNoqhKa6yMKqrGyXjc7Ca0dmh1TfvBapOxtIZGMYjXrtPrBeU7RQRMMl8AKGNC83eO8syrpHYYx3G5CrbeGzR5loDUeEse2eGpD/Gc3MvvAsMD5mMeQna+WZsNLIKuO9FEYhdVwK54PXU94f/krbenbEBP5xKjJUrVi3aVszNm8CnMP78izqd0La9sK4BeaCbc3ov0zt5uzgQ4tIHy9iBLCf8AjedqQBWAgAA"
|
||||
},
|
||||
"Metadata": {
|
||||
"aws:cdk:path": "BetaSupplierSiteStack/CDKMetadata/Default"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Outputs": {
|
||||
"BetaSupplierSiteBucketName": {
|
||||
"Description": "The name of the S3 bucket for the betasupplier site",
|
||||
"Value": {
|
||||
"Ref": "BetaSupplierSiteBucketC3053F9A"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteCloudFrontDomainName": {
|
||||
"Description": "The domain name of the CloudFront distribution for betasupplier site",
|
||||
"Value": {
|
||||
"Fn::GetAtt": [
|
||||
"BetaSupplierSiteDistributionD03E58A9",
|
||||
"DomainName"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Mappings": {
|
||||
"AWSCloudFrontPartitionHostedZoneIdMap": {
|
||||
"aws": {
|
||||
"zoneId": "Z2FDTNDATAQYW2"
|
||||
},
|
||||
"aws-cn": {
|
||||
"zoneId": "Z3RFFRIM2A3IF5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Parameters": {
|
||||
"BootstrapVersion": {
|
||||
"Type": "AWS::SSM::Parameter::Value<String>",
|
||||
"Default": "/cdk-bootstrap/hnb659fds/version",
|
||||
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
|
||||
}
|
||||
},
|
||||
"Rules": {
|
||||
"CheckBootstrapVersion": {
|
||||
"Assertions": [
|
||||
{
|
||||
"Assert": {
|
||||
"Fn::Not": [
|
||||
{
|
||||
"Fn::Contains": [
|
||||
[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
],
|
||||
{
|
||||
"Ref": "BootstrapVersion"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
"use strict";var f=Object.create,i=Object.defineProperty,I=Object.getOwnPropertyDescriptor,C=Object.getOwnPropertyNames,w=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty,A=(t,e)=>{for(var o in e)i(t,o,{get:e[o],enumerable:!0})},d=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of C(e))!P.call(t,s)&&s!==o&&i(t,s,{get:()=>e[s],enumerable:!(r=I(e,s))||r.enumerable});return t},l=(t,e,o)=>(o=t!=null?f(w(t)):{},d(e||!t||!t.__esModule?i(o,"default",{value:t,enumerable:!0}):o,t)),B=t=>d(i({},"__esModule",{value:!0}),t),q={};A(q,{autoDeleteHandler:()=>S,handler:()=>H}),module.exports=B(q);var h=require("@aws-sdk/client-s3"),y=l(require("https")),m=l(require("url")),a={sendHttpRequest:D,log:T,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",L="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function R(t){return async(e,o)=>{let r={...e,ResponseURL:"..."};if(a.log(JSON.stringify(r,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",e);return}try{let s=await t(r,o),n=k(e,s);await u("SUCCESS",n)}catch(s){let n={...e,Reason:a.includeStackTraces?s.stack:s.message};n.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),n.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await u("FAILED",n)}}}function k(t,e={}){let o=e.PhysicalResourceId??t.PhysicalResourceId??t.RequestId;if(t.RequestType==="Delete"&&o!==t.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${t.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...t,...e,PhysicalResourceId:o}}async function u(t,e){let o={Status:t,Reason:e.Reason??t,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||L,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},r=m.parse(e.ResponseURL),s=`${r.protocol}//${r.hostname}/${r.pathname}?***`;a.log("submit response to cloudformation",s,o);let n=JSON.stringify(o),E={hostname:r.hostname,path:r.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(n,"utf8")}};await O({attempts:5,sleep:1e3},a.sendHttpRequest)(E,n)}async function D(t,e){return new Promise((o,r)=>{try{let s=y.request(t,n=>{n.resume(),!n.statusCode||n.statusCode>=400?r(new Error(`Unsuccessful HTTP response: ${n.statusCode}`)):o()});s.on("error",r),s.write(e),s.end()}catch(s){r(s)}})}function T(t,...e){console.log(t,...e)}function O(t,e){return async(...o)=>{let r=t.attempts,s=t.sleep;for(;;)try{return await e(...o)}catch(n){if(r--<=0)throw n;await b(Math.floor(Math.random()*s)),s*=2}}}async function b(t){return new Promise(e=>setTimeout(e,t))}var g="aws-cdk:auto-delete-objects",x=JSON.stringify({Version:"2012-10-17",Statement:[]}),c=new h.S3({}),H=R(S);async function S(t){switch(t.RequestType){case"Create":return;case"Update":return{PhysicalResourceId:(await F(t)).PhysicalResourceId};case"Delete":return N(t.ResourceProperties?.BucketName)}}async function F(t){let e=t,o=e.OldResourceProperties?.BucketName;return{PhysicalResourceId:e.ResourceProperties?.BucketName??o}}async function _(t){try{let e=(await c.getBucketPolicy({Bucket:t}))?.Policy??x,o=JSON.parse(e);o.Statement.push({Principal:"*",Effect:"Deny",Action:["s3:PutObject"],Resource:[`arn:aws:s3:::${t}/*`]}),await c.putBucketPolicy({Bucket:t,Policy:JSON.stringify(o)})}catch(e){if(e.name==="NoSuchBucket")throw e;console.log(`Could not set new object deny policy on bucket '${t}' prior to deletion.`)}}async function U(t){let e;do{e=await c.listObjectVersions({Bucket:t});let o=[...e.Versions??[],...e.DeleteMarkers??[]];if(o.length===0)return;let r=o.map(s=>({Key:s.Key,VersionId:s.VersionId}));await c.deleteObjects({Bucket:t,Delete:{Objects:r}})}while(e?.IsTruncated)}async function N(t){if(!t)throw new Error("No BucketName was provided.");try{if(!await W(t)){console.log(`Bucket does not have '${g}' tag, skipping cleaning.`);return}await _(t),await U(t)}catch(e){if(e.name==="NoSuchBucket"){console.log(`Bucket '${t}' does not exist.`);return}throw e}}async function W(t){return(await c.getBucketTagging({Bucket:t})).TagSet?.some(o=>o.Key===g&&o.Value==="true")}
|
||||
File diff suppressed because one or more lines are too long
1
cdk/cdk.out/cdk.out
Normal file
1
cdk/cdk.out/cdk.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":"54.0.0"}
|
||||
508
cdk/cdk.out/manifest.json
Normal file
508
cdk/cdk.out/manifest.json
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
{
|
||||
"version": "54.0.0",
|
||||
"artifacts": {
|
||||
"BetaSupplierSiteStack.assets": {
|
||||
"type": "cdk:asset-manifest",
|
||||
"properties": {
|
||||
"file": "BetaSupplierSiteStack.assets.json",
|
||||
"requiresBootstrapStackVersion": 6,
|
||||
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
|
||||
}
|
||||
},
|
||||
"BetaSupplierSiteStack": {
|
||||
"type": "aws:cloudformation:stack",
|
||||
"environment": "aws://764709663363/ap-south-2",
|
||||
"properties": {
|
||||
"templateFile": "BetaSupplierSiteStack.template.json",
|
||||
"terminationProtection": false,
|
||||
"validateOnSynth": false,
|
||||
"assumeRoleArn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-deploy-role-764709663363-ap-south-2",
|
||||
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-cfn-exec-role-764709663363-ap-south-2",
|
||||
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-764709663363-ap-south-2/7ed3cfff7120b7b31997b41625d0f260352266b5eaa8c56533cf1f509441f2e7.json",
|
||||
"requiresBootstrapStackVersion": 6,
|
||||
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
|
||||
"additionalDependencies": [
|
||||
"BetaSupplierSiteStack.assets"
|
||||
],
|
||||
"lookupRole": {
|
||||
"arn": "arn:${AWS::Partition}:iam::764709663363:role/cdk-hnb659fds-lookup-role-764709663363-ap-south-2",
|
||||
"requiresBootstrapStackVersion": 8,
|
||||
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
|
||||
}
|
||||
},
|
||||
"dependencies": [
|
||||
"BetaSupplierSiteStack.assets"
|
||||
],
|
||||
"additionalMetadataFile": "BetaSupplierSiteStack.metadata.json",
|
||||
"displayName": "BetaSupplierSiteStack"
|
||||
},
|
||||
"Tree": {
|
||||
"type": "cdk:tree",
|
||||
"properties": {
|
||||
"file": "tree.json"
|
||||
}
|
||||
},
|
||||
"aws-cdk-lib/feature-flag-report": {
|
||||
"type": "cdk:feature-flag-report",
|
||||
"properties": {
|
||||
"module": "aws-cdk-lib",
|
||||
"flags": {
|
||||
"@aws-cdk/aws-signer:signingProfileNamePassedToCfn": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Pass signingProfileName to CfnSigningProfile"
|
||||
},
|
||||
"@aws-cdk/core:newStyleStackSynthesis": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Switch to new stack synthesis method which enables CI/CD",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/core:stackRelativeExports": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Name exports based on the construct paths relative to the stack, rather than the global construct path",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Disable implicit openListener when custom security groups are provided"
|
||||
},
|
||||
"@aws-cdk/aws-rds:lowercaseDbIdentifier": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Force lowercasing of RDS Cluster names in CDK",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Allow adding/removing multiple UsagePlanKeys independently",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-lambda:recognizeVersionProps": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`.",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-lambda:recognizeLayerVersion": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature flag to opt in to the updated logical id calculation for Lambda Version created using the `fn.currentVersion`."
|
||||
},
|
||||
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature flag to have cloudfront distributions use the security policy TLSv1.2_2021 by default.",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/core:checkSecretUsage": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this flag to make it impossible to accidentally use SecretValues in unsafe locations"
|
||||
},
|
||||
"@aws-cdk/core:target-partitions": {
|
||||
"recommendedValue": [
|
||||
"aws",
|
||||
"aws-cn"
|
||||
],
|
||||
"explanation": "What regions to include in lookup tables of environment agnostic stacks"
|
||||
},
|
||||
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "ECS extensions will automatically add an `awslogs` driver if no logging is specified"
|
||||
},
|
||||
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature flag to have Launch Templates generated by the `InstanceRequireImdsv2Aspect` use unique names."
|
||||
},
|
||||
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "ARN format used by ECS. In the new ARN format, the cluster name is part of the resource ID."
|
||||
},
|
||||
"@aws-cdk/aws-iam:minimizePolicies": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Minimize IAM policies by combining Statements"
|
||||
},
|
||||
"@aws-cdk/core:validateSnapshotRemovalPolicy": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Error on snapshot removal policies on resources that do not support it."
|
||||
},
|
||||
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Generate key aliases that include the stack name"
|
||||
},
|
||||
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature flag to create an S3 bucket policy by default in cases where an AWS service would automatically create the Policy if one does not exist."
|
||||
},
|
||||
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Restrict KMS key policy for encrypted Queues a bit more"
|
||||
},
|
||||
"@aws-cdk/aws-apigateway:disableCloudWatchRole": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Make default CloudWatch Role behavior safe for multiple API Gateways in one environment"
|
||||
},
|
||||
"@aws-cdk/core:enablePartitionLiterals": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Make ARNs concrete if AWS partition is known"
|
||||
},
|
||||
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Event Rules may only push to encrypted SQS queues in the same account"
|
||||
},
|
||||
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Avoid setting the \"ECS\" deployment controller when adding a circuit breaker"
|
||||
},
|
||||
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable this feature to create default policy names for imported roles that depend on the stack the role is in."
|
||||
},
|
||||
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use S3 Bucket Policy instead of ACLs for Server Access Logging"
|
||||
},
|
||||
"@aws-cdk/aws-route53-patters:useCertificate": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use the official `Certificate` resource instead of `DnsValidatedCertificate`"
|
||||
},
|
||||
"@aws-cdk/customresources:installLatestAwsSdkDefault": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "Whether to install the latest SDK by default in AwsCustomResource"
|
||||
},
|
||||
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use unique resource name for Database Proxy"
|
||||
},
|
||||
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Remove CloudWatch alarms from deployment group"
|
||||
},
|
||||
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Include authorizer configuration in the calculation of the API deployment logical ID."
|
||||
},
|
||||
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Define user data for a launch template by default when a machine image is provided."
|
||||
},
|
||||
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "SecretTargetAttachments uses the ResourcePolicy of the attached Secret."
|
||||
},
|
||||
"@aws-cdk/aws-redshift:columnId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Whether to use an ID to track Redshift column changes"
|
||||
},
|
||||
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable AmazonEMRServicePolicy_v2 managed policies"
|
||||
},
|
||||
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Restrict access to the VPC default security group"
|
||||
},
|
||||
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Generate a unique id for each RequestValidator added to a method"
|
||||
},
|
||||
"@aws-cdk/aws-kms:aliasNameRef": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "KMS Alias name and keyArn will have implicit reference to KMS Key"
|
||||
},
|
||||
"@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enable grant methods on Aliases imported by name to use kms:ResourceAliases condition"
|
||||
},
|
||||
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Generate a launch template when creating an AutoScalingGroup"
|
||||
},
|
||||
"@aws-cdk/core:includePrefixInUniqueNameGeneration": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Include the stack prefix in the stack name generation process"
|
||||
},
|
||||
"@aws-cdk/aws-efs:denyAnonymousAccess": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "EFS denies anonymous clients accesses"
|
||||
},
|
||||
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enables support for Multi-AZ with Standby deployment for opensearch domains"
|
||||
},
|
||||
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enables aws-lambda-nodejs.Function to use the latest available NodeJs runtime as the default"
|
||||
},
|
||||
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, mount targets will have a stable logicalId that is linked to the associated subnet."
|
||||
},
|
||||
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, a scope of InstanceParameterGroup for AuroraClusterInstance with each parameters will change."
|
||||
},
|
||||
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, will always use the arn for identifiers for CfnSourceApiAssociation in the GraphqlApi construct rather than id."
|
||||
},
|
||||
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, creating an RDS database cluster from a snapshot will only render credentials for snapshot credentials."
|
||||
},
|
||||
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the CodeCommit source action is using the default branch name 'main'."
|
||||
},
|
||||
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the logical ID of a Lambda permission for a Lambda action includes an alarm ID."
|
||||
},
|
||||
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enables Pipeline to set the default value for crossAccountKeys to false."
|
||||
},
|
||||
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Enables Pipeline to set the default pipeline type to V2."
|
||||
},
|
||||
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, IAM Policy created from KMS key grant will reduce the resource scope to this key only."
|
||||
},
|
||||
"@aws-cdk/pipelines:reduceAssetRoleTrustScope": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Remove the root account principal from PipelineAssetsFileRole trust policy",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-eks:nodegroupNameAttribute": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix."
|
||||
},
|
||||
"@aws-cdk/aws-eks:useNativeOidcProvider": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, EKS V2 clusters will use the native OIDC provider resource AWS::IAM::OIDCProvider instead of creating the OIDCProvider with a custom resource (iam.OpenIDConnectProvider)."
|
||||
},
|
||||
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the default volume type of the EBS volume will be GP3"
|
||||
},
|
||||
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, remove default deployment alarm settings"
|
||||
},
|
||||
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default"
|
||||
},
|
||||
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "When enabled, Adding notifications to a bucket in the current stack will not remove notification from imported stack."
|
||||
},
|
||||
"@aws-cdk/aws-stepfunctions-tasks:useNewS3UriParametersForBedrockInvokeModelTask": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, use new props for S3 URI field in task definition of state machine for bedrock invoke model.",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/core:explicitStackTags": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, stack tags need to be assigned explicitly on a Stack."
|
||||
},
|
||||
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, we will only grant the necessary permissions when users specify cloudwatch log group through logConfiguration"
|
||||
},
|
||||
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled will allow you to specify a resource policy per replica, and not copy the source table policy to all replicas"
|
||||
},
|
||||
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, initOptions.timeout and resourceSignalTimeout values will be summed together."
|
||||
},
|
||||
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, a Lambda authorizer Permission created when using GraphqlApi will be properly scoped with a SourceArn."
|
||||
},
|
||||
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the value of property `instanceResourceId` in construct `DatabaseInstanceReadReplica` will be set to the correct value which is `DbiResourceId` instead of currently `DbInstanceArn`"
|
||||
},
|
||||
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, CFN templates added with `cfn-include` will error if the template contains Resource Update or Create policies with CFN Intrinsics that include non-primitive values."
|
||||
},
|
||||
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, both `@aws-sdk` and `@smithy` packages will be excluded from the Lambda Node.js 18.x runtime to prevent version mismatches in bundled applications."
|
||||
},
|
||||
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the resource of IAM Run Ecs policy generated by SFN EcsRunTask will reference the definition, instead of constructing ARN."
|
||||
},
|
||||
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the BastionHost construct will use the latest Amazon Linux 2023 AMI, instead of Amazon Linux 2."
|
||||
},
|
||||
"@aws-cdk/core:aspectStabilization": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, a stabilization loop will be run when invoking Aspects during synthesis.",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, use a new method for DNS Name of user pool domain target without creating a custom resource."
|
||||
},
|
||||
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the default security group ingress rules will allow IPv6 ingress from anywhere"
|
||||
},
|
||||
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the default behaviour of OIDC provider will reject unauthorized connections"
|
||||
},
|
||||
"@aws-cdk/core:enableAdditionalMetadataCollection": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, CDK will expand the scope of usage data collected to better inform CDK development and improve communication for security concerns and emerging issues."
|
||||
},
|
||||
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "[Deprecated] When enabled, Lambda will create new inline policies with AddToRolePolicy instead of adding to the Default Policy Statement"
|
||||
},
|
||||
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, CDK will automatically generate a unique role name that is used for s3 object replication."
|
||||
},
|
||||
"@aws-cdk/pipelines:reduceStageRoleTrustScope": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Remove the root account principal from Stage addActions trust policy",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-events:requireEventBusPolicySid": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, grantPutEventsTo() will use resource policies with Statement IDs for service principals."
|
||||
},
|
||||
"@aws-cdk/core:aspectPrioritiesMutating": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When set to true, Aspects added by the construct library on your behalf will be given a priority of MUTATING."
|
||||
},
|
||||
"@aws-cdk/aws-dynamodb:retainTableReplica": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, table replica will be default to the removal policy of source table unless specified otherwise."
|
||||
},
|
||||
"@aws-cdk/cognito:logUserPoolClientSecretValue": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "When disabled, the value of the user pool client secret will not be logged in the custom resource lambda function logs."
|
||||
},
|
||||
"@aws-cdk/pipelines:reduceCrossAccountActionRoleTrustScope": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, scopes down the trust policy for the cross-account action role",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the resultWriterV2 property of DistributedMap will be used insted of resultWriter"
|
||||
},
|
||||
"@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Add an S3 trust policy to a KMS key resource policy for SNS subscriptions."
|
||||
},
|
||||
"@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, the EgressOnlyGateway resource is only created if private subnets are defined in the dual-stack VPC."
|
||||
},
|
||||
"@aws-cdk/aws-ec2-alpha:useResourceIdForVpcV2Migration": {
|
||||
"recommendedValue": false,
|
||||
"explanation": "When enabled, use resource IDs for VPC V2 migration"
|
||||
},
|
||||
"@aws-cdk/aws-s3:publicAccessBlockedByDefault": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, setting any combination of options for BlockPublicAccess will automatically set true for any options not defined."
|
||||
},
|
||||
"@aws-cdk/aws-lambda:useCdkManagedLogGroup": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, CDK creates and manages loggroup for the lambda function"
|
||||
},
|
||||
"@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, Network Load Balancer will be created with a security group by default."
|
||||
},
|
||||
"@aws-cdk/aws-stepfunctions-tasks:httpInvokeDynamicJsonPathEndpoint": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, allows using a dynamic apiEndpoint with JSONPath format in HttpInvoke tasks.",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, ECS patterns will generate unique target group IDs to prevent conflicts during load balancer replacement"
|
||||
},
|
||||
"@aws-cdk/aws-route53-patterns:useDistribution": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use the `Distribution` resource instead of `CloudFrontWebDistribution`"
|
||||
},
|
||||
"@aws-cdk/aws-cloudfront:defaultFunctionRuntimeV2_0": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use cloudfront-js-2.0 as the default runtime for CloudFront Functions"
|
||||
},
|
||||
"@aws-cdk/aws-elasticloadbalancingv2:usePostQuantumTlsPolicy": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "When enabled, HTTPS/TLS listeners use post-quantum TLS policy by default"
|
||||
},
|
||||
"@aws-cdk/core:automaticL1Traits": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Automatically use the default L1 traits for L1 constructs`",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": true
|
||||
}
|
||||
},
|
||||
"@aws-cdk/aws-batch:defaultToAL2023": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use AL2023 as the default imageType for EC2 Batch compute environments instead of the deprecated AL2"
|
||||
},
|
||||
"@aws-cdk/aws-eks:defaultToAL2023": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Use AL2023 as the default AMI type for EKS managed node groups using non-GPU instance types instead of the deprecated AL2"
|
||||
},
|
||||
"@aws-cdk/core:annotationsInValidationReport": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Include construct annotations (warnings and errors) in the policy validation report"
|
||||
},
|
||||
"@aws-cdk/core:defaultCrossStackReferences": {
|
||||
"recommendedValue": "weak",
|
||||
"explanation": "Controls whether cross-stack references are strong, weak, or both",
|
||||
"unconfiguredBehavesLike": {
|
||||
"v2": "strong"
|
||||
}
|
||||
},
|
||||
"@aws-cdk/core:validateAgainstDefaultRules": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Treat CloudFormation Validate findings as errors"
|
||||
},
|
||||
"@aws-cdk/aws-ecs:removeEmptyLoadBalancers": {
|
||||
"recommendedValue": true,
|
||||
"explanation": "Render an empty `LoadBalancers` array on an ECS service that has no target groups"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimumCliVersion": "2.1144.0"
|
||||
}
|
||||
1
cdk/cdk.out/tree.json
Normal file
1
cdk/cdk.out/tree.json
Normal file
File diff suppressed because one or more lines are too long
5
cdk/cdk.out/validation-report.json
Normal file
5
cdk/cdk.out/validation-report.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"version": "54.0.0",
|
||||
"title": "Validation Report",
|
||||
"pluginReports": []
|
||||
}
|
||||
655
package-lock.json
generated
655
package-lock.json
generated
|
|
@ -13,7 +13,8 @@
|
|||
"lucide-react": "^0.475.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^6.28.0"
|
||||
"react-router-dom": "^6.28.0",
|
||||
"selenium-webdriver": "^4.50.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
|
|
@ -22,12 +23,69 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.6.1",
|
||||
"aws-cdk-lib": "^2.272.0",
|
||||
"constructs": "^10.8.1",
|
||||
"postcss": "^8.5.28",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/asset-awscli-v1": {
|
||||
"version": "2.2.292",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.292.tgz",
|
||||
"integrity": "sha512-d4aMFsAFj19FtxVyw8IzlUKv5Zu4sIvgnEjI2IU6IWBJgVbJ4aFnadANqYa+6MwB1CQbGOg0jh8WE77M+Nb/9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.3.tgz",
|
||||
"integrity": "sha512-98We1QWfin9vtuuQtNFhIIAEWb8CreyYTAP0Vn8HqSBoNiFJM9nZgifJeQawT9Wq0NX1HCNFlxCyTccIrK1PSA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema": {
|
||||
"version": "54.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.26.0.tgz",
|
||||
"integrity": "sha512-zYI3XhQ/bpuSXuPaF77f7u2q6rZBPwdrPh8a0v+e/B3IxbRQAtQv0fmacQ8uYh9RPVuIVnExE6IsFJBSALH7BA==",
|
||||
"bundleDependencies": [
|
||||
"jsonschema",
|
||||
"semver"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jsonschema": "^1.5.0",
|
||||
"semver": "^7.8.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
|
||||
"version": "1.5.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
|
|
@ -310,6 +368,36 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bazel/runfiles": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@bazel/runfiles/-/runfiles-6.5.0.tgz",
|
||||
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
|
|
@ -1466,6 +1554,34 @@
|
|||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz",
|
||||
"integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
|
|
@ -1569,6 +1685,39 @@
|
|||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.5",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
|
||||
|
|
@ -1606,6 +1755,236 @@
|
|||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib": {
|
||||
"version": "2.272.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.272.0.tgz",
|
||||
"integrity": "sha512-XHqU7msDtp03JLoFqtbIuNPDeCebQxOBwRcuL6BpH8B1rV/sMHSdH9EY0ZRn+P004RKHs/+b5DPssN6F966k1A==",
|
||||
"bundleDependencies": [
|
||||
"@aws/cloudformation-validate",
|
||||
"@balena/dockerignore",
|
||||
"@aws-cdk/cloud-assembly-api",
|
||||
"case",
|
||||
"fs-extra",
|
||||
"ignore",
|
||||
"jsonschema",
|
||||
"minimatch",
|
||||
"punycode",
|
||||
"semver",
|
||||
"yaml",
|
||||
"mime-types"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-cdk/asset-awscli-v1": "2.2.292",
|
||||
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2",
|
||||
"@aws-cdk/cloud-assembly-api": "^2.2.6",
|
||||
"@aws-cdk/cloud-assembly-schema": "^54.24.0",
|
||||
"@aws/cloudformation-validate": "1.12.1",
|
||||
"@balena/dockerignore": "^1.0.2",
|
||||
"case": "1.6.3",
|
||||
"fs-extra": "^11.3.6",
|
||||
"ignore": "^5.3.2",
|
||||
"jsonschema": "^1.5.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"minimatch": "^10.2.5",
|
||||
"punycode": "^2.3.1",
|
||||
"semver": "^7.8.5",
|
||||
"yaml": "1.10.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"constructs": "^10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
|
||||
"version": "2.2.6",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jsonschema": "^1.5.0",
|
||||
"semver": "^7.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@aws-cdk/cloud-assembly-schema": ">=54.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": {
|
||||
"version": "1.12.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/case": {
|
||||
"version": "1.6.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
|
||||
"version": "11.3.6",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
|
||||
"version": "6.2.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
|
||||
"version": "1.5.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-cdk-lib/node_modules/yaml": {
|
||||
"version": "1.10.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.11.23",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz",
|
||||
|
|
@ -1653,6 +2032,13 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bulma": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz",
|
||||
|
|
@ -1680,6 +2066,13 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/constructs": {
|
||||
"version": "10.8.1",
|
||||
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.8.1.tgz",
|
||||
"integrity": "sha512-98yGXYyhePqPYh3cYu8nzBERmAhC0DONe3UD03okK0nehZ7hYP4wgZuf02a04+uOWxnTJ5Rpp5m0GRNpwyLGGA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
|
|
@ -1687,6 +2080,19 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
|
|
@ -1722,6 +2128,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
|
||||
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.427",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz",
|
||||
|
|
@ -1859,6 +2275,24 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
|
|
@ -1902,6 +2336,27 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.2",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz",
|
||||
"integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
|
|
@ -2192,6 +2647,13 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -2228,6 +2690,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -2284,6 +2752,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
|
|
@ -2347,6 +2821,21 @@
|
|||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.63.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.3.tgz",
|
||||
|
|
@ -2393,12 +2882,43 @@
|
|||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
|
||||
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/selenium-webdriver": {
|
||||
"version": "4.50.0",
|
||||
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.50.0.tgz",
|
||||
"integrity": "sha512-E8DhBVMpnsNwShcxSWG8oCVJLQoPZiEciqz/inH7YkKvnR/kBiSNjaJux+7iuA4x6/77N8T68vWMTJgW169kOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/SeleniumHQ"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/selenium"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bazel/runfiles": "^6.5.0",
|
||||
"jszip": "^3.10.2",
|
||||
"tmp": "^0.2.7",
|
||||
"ws": "^8.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
|
|
@ -2409,6 +2929,22 @@
|
|||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -2419,6 +2955,26 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||
|
|
@ -2457,6 +3013,59 @@
|
|||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz",
|
||||
"integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
|
||||
|
|
@ -2509,6 +3118,19 @@
|
|||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
|
||||
|
|
@ -2584,12 +3206,43 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.22.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
|
||||
"integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
"lucide-react": "^0.475.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^6.28.0"
|
||||
"react-router-dom": "^6.28.0",
|
||||
"selenium-webdriver": "^4.50.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
|
|
@ -25,8 +26,12 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.6.1",
|
||||
"aws-cdk-lib": "^2.272.0",
|
||||
"constructs": "^10.8.1",
|
||||
"postcss": "^8.5.28",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { api } from '../services/api';
|
|||
interface SellerContextType {
|
||||
// Authentication
|
||||
user: UserProfile | null;
|
||||
login: (emailOrPhone: string, password?: string) => Promise<any>;
|
||||
login: (emailOrPhone: string, password?: string, otpCode?: string, otpType?: string) => Promise<any>;
|
||||
register: (userData: { username?: string; email?: string; phone: string; password?: string }) => Promise<any>;
|
||||
logout: () => void;
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ interface SellerContextType {
|
|||
// Inventory & Products
|
||||
inventory: InventoryItem[];
|
||||
fetchData: () => Promise<void>;
|
||||
updateStock: (id: string, newStock: number) => void;
|
||||
updateStock: (id: string, newStock: number) => Promise<void>;
|
||||
addProduct: (product: Omit<InventoryItem, 'id' | 'salesCount'>) => Promise<void>;
|
||||
deleteProduct: (id: string) => Promise<void>;
|
||||
|
||||
|
|
@ -55,6 +55,7 @@ const defaultOnboarding: OnboardingData = {
|
|||
email: '',
|
||||
phone: '',
|
||||
isOtpVerified: false,
|
||||
isEmailVerified: false,
|
||||
taxMode: 'gstin',
|
||||
gstin: '',
|
||||
enrolmentId: '',
|
||||
|
|
@ -153,13 +154,21 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
sku: p.sku || `SKU-${p.id}`,
|
||||
title: p.title || 'Product',
|
||||
category: p.category || 'General',
|
||||
craftTechnique: 'Authentic Craft',
|
||||
craftTechnique: p.craftTechnique || 'Authentic Craft',
|
||||
giTagged: p.is_gi_tagged || false,
|
||||
price: parseFloat(p.price) || 0,
|
||||
mrp: parseFloat(p.compare_at_price) || ((parseFloat(p.price) || 0) * 1.2),
|
||||
stock: p.stock || 0,
|
||||
status: p.stock === 0 ? 'out_of_stock' : p.stock < 5 ? 'low_stock' : 'active',
|
||||
approvalStatus: (p.status && (p.status.toLowerCase() === 'approved' || p.status.toLowerCase() === 'active')) ? 'approved' : (p.status && p.status.toLowerCase() === 'rejected' ? 'rejected' : 'pending'),
|
||||
image: p.image || '/images/placeholder.jpg',
|
||||
additional_images: p.additional_images || [],
|
||||
video_url: p.video_url || '',
|
||||
description: p.description || '',
|
||||
compare_at_price: parseFloat(p.compare_at_price) || undefined,
|
||||
package_weight: p.package_weight || 1.0,
|
||||
shipping_profile: p.shipping_profile || 'Standard Fragile',
|
||||
uploadSource: p.upload_source || (p.sku?.startsWith('TRDX-') || p.sku?.includes('CSV') || p.sku?.startsWith('BATCH-') ? 'bulk' : 'individual'),
|
||||
salesCount: 0
|
||||
})));
|
||||
|
||||
|
|
@ -241,15 +250,16 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
localStorage.setItem('tradhox_app_submitted', 'true');
|
||||
};
|
||||
|
||||
const login = async (emailOrPhone: string, password?: string) => {
|
||||
const login = async (emailOrPhone: string, password?: string, otpCode?: string, otpType?: string) => {
|
||||
try {
|
||||
const data = await api.login(emailOrPhone, password || 'Password123!');
|
||||
const data = await api.login(emailOrPhone, password, otpCode, otpType);
|
||||
localStorage.setItem('tradhox_user_auth', JSON.stringify(data));
|
||||
|
||||
const userEmail = (data.user?.email && !data.user.email.endsWith('@tradhox.com')) ? data.user.email : '';
|
||||
const loggedUser: UserProfile = {
|
||||
id: String(data.user.id),
|
||||
name: data.user.first_name ? `${data.user.first_name} ${data.user.last_name || ''}`.trim() : data.user.username,
|
||||
email: data.user.email,
|
||||
email: userEmail,
|
||||
phone: data.user.profile?.phone || emailOrPhone,
|
||||
role: 'Seller',
|
||||
avatar: data.user.profile?.logo_url || '/images/avatars/seller-main.jpg',
|
||||
|
|
@ -272,8 +282,8 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
|
||||
const register = async (userData: { username?: string; email?: string; phone: string; password?: string }) => {
|
||||
const cleanPhone = userData.phone.replace(/[^0-9]/g, '');
|
||||
const username = userData.username || `artisan_${cleanPhone.slice(-6) || Math.floor(1000 + Math.random() * 9000)}`;
|
||||
const email = userData.email || `${username}@tradhox.com`;
|
||||
const username = userData.username || (cleanPhone ? cleanPhone : (userData.email ? userData.email.split('@')[0] : `user_${Date.now()}`));
|
||||
const email = (userData.email && !userData.email.endsWith('@tradhox.com')) ? userData.email : '';
|
||||
const password = userData.password || 'Password123!';
|
||||
|
||||
const payload = {
|
||||
|
|
@ -288,10 +298,11 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
const data = await api.register(payload);
|
||||
localStorage.setItem('tradhox_user_auth', JSON.stringify(data));
|
||||
|
||||
const registeredEmail = (data.user?.email && !data.user.email.endsWith('@tradhox.com')) ? data.user.email : email;
|
||||
const registeredUser: UserProfile = {
|
||||
id: String(data.user?.id || 'seller-001'),
|
||||
name: data.user?.first_name ? `${data.user?.first_name} ${data.user?.last_name || ''}`.trim() : (data.user?.username || username),
|
||||
email: data.user?.email || email,
|
||||
email: registeredEmail,
|
||||
phone: data.user?.profile?.phone || userData.phone,
|
||||
role: 'Seller',
|
||||
avatar: data.user?.profile?.logo_url || '/images/avatars/seller-main.jpg',
|
||||
|
|
@ -368,21 +379,51 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
setPayouts([]);
|
||||
};
|
||||
|
||||
const updateStock = (id: string, newStock: number) => {
|
||||
const updateStock = async (id: string, newStock: number) => {
|
||||
const clamped = Math.max(0, newStock);
|
||||
setInventory(prev =>
|
||||
prev.map(item =>
|
||||
item.id === id
|
||||
? {
|
||||
...item,
|
||||
stock: Math.max(0, newStock),
|
||||
status: newStock === 0 ? 'out_of_stock' : newStock < 5 ? 'low_stock' : 'active'
|
||||
stock: clamped,
|
||||
status: clamped === 0 ? 'out_of_stock' : clamped < 5 ? 'low_stock' : 'active'
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
if (id && !id.startsWith('prod-')) {
|
||||
await api.patchProduct(id, { stock: clamped });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to persist stock update to API, kept local optimistic state:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const addProduct = async (product: Omit<InventoryItem, 'id' | 'salesCount'>) => {
|
||||
const newLocalItem: InventoryItem = {
|
||||
id: `prod-${Date.now()}`,
|
||||
sku: product.sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
||||
title: product.title,
|
||||
category: product.category,
|
||||
craftTechnique: product.craftTechnique || 'Authentic Craft',
|
||||
giTagged: product.giTagged || false,
|
||||
price: product.price,
|
||||
mrp: product.mrp,
|
||||
stock: product.stock,
|
||||
status: product.status,
|
||||
image: product.image,
|
||||
additional_images: product.additional_images,
|
||||
video_url: product.video_url,
|
||||
description: product.description,
|
||||
compare_at_price: product.compare_at_price || product.mrp,
|
||||
package_weight: product.package_weight || 1.0,
|
||||
shipping_profile: product.shipping_profile || 'Standard Fragile',
|
||||
salesCount: 0
|
||||
};
|
||||
|
||||
try {
|
||||
await api.createProduct({
|
||||
title: product.title,
|
||||
|
|
@ -390,12 +431,21 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||
category: product.category,
|
||||
price: product.price,
|
||||
stock: product.stock,
|
||||
image: product.image
|
||||
image: product.image,
|
||||
description: product.description || '',
|
||||
is_gi_tagged: product.giTagged || false,
|
||||
additional_images: product.additional_images || [],
|
||||
video_url: product.video_url || null,
|
||||
compare_at_price: product.compare_at_price || product.mrp || null,
|
||||
package_weight: product.package_weight || 1.0,
|
||||
shipping_profile: product.shipping_profile || 'Standard Fragile',
|
||||
track_inventory: product.stock > 0,
|
||||
processing_days: 3
|
||||
});
|
||||
fetchData();
|
||||
await fetchData();
|
||||
} catch (e) {
|
||||
console.error('Failed to add product', e);
|
||||
throw e;
|
||||
console.warn('Backend API create product notice (adding to local inventory state):', e);
|
||||
setInventory(prev => [newLocalItem, ...prev]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,9 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
}, [location.pathname]);
|
||||
|
||||
// Login state
|
||||
const [loginTab, setLoginTab] = useState<'mobile' | 'email'>('mobile');
|
||||
const [mobileNumber, setMobileNumber] = useState('');
|
||||
const [loginOtp, setLoginOtp] = useState('');
|
||||
const [otpSent, setOtpSent] = useState(false);
|
||||
const [emailAddress, setEmailAddress] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isPhoneFocused, setIsPhoneFocused] = useState(false);
|
||||
|
|
@ -92,6 +90,7 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
|
||||
const handleGetOtp = async () => {
|
||||
setLoginError('');
|
||||
setOtpSent(false);
|
||||
const clean = mobileNumber.replace(/\D/g, '');
|
||||
if (clean.length !== 10) {
|
||||
setLoginError('Please enter a valid 10-digit mobile number');
|
||||
|
|
@ -101,10 +100,11 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
try {
|
||||
await api.sendWhatsAppOtp(clean, code, 'mobile_number_verification');
|
||||
setOtpSent(true);
|
||||
setLoginOtp(''); // Leave empty so user enters the 6-digit code received on WhatsApp
|
||||
} catch (err) {
|
||||
setLoginOtp(''); // Empty so user types the OTP received on WhatsApp
|
||||
} catch (err: any) {
|
||||
console.warn('WhatsApp service call error:', err);
|
||||
setLoginError('Failed to dispatch OTP. Please try again.');
|
||||
setOtpSent(false);
|
||||
setLoginError(err?.message || 'Failed to dispatch OTP. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -112,50 +112,22 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
e.preventDefault();
|
||||
setLoginError('');
|
||||
try {
|
||||
if (loginTab === 'mobile') {
|
||||
const clean = mobileNumber.replace(/\D/g, '');
|
||||
if (clean.length !== 10) {
|
||||
setLoginError('Please enter a valid 10-digit mobile number');
|
||||
return;
|
||||
}
|
||||
if (!loginOtp || loginOtp.length < 6) {
|
||||
setLoginError('Please enter the 6-digit verification code sent to your mobile number');
|
||||
return;
|
||||
}
|
||||
// Authenticate session to obtain access token
|
||||
const data = await login(clean, password);
|
||||
const clean = mobileNumber.replace(/\D/g, '');
|
||||
if (clean.length !== 10) {
|
||||
setLoginError('Please enter a valid 10-digit mobile number');
|
||||
return;
|
||||
}
|
||||
if (!loginOtp || loginOtp.length < 6) {
|
||||
setLoginError('Please enter the 6-digit verification code sent to your mobile number');
|
||||
return;
|
||||
}
|
||||
// Authenticate session to obtain access token
|
||||
const data = await login(clean, password, loginOtp, 'phone');
|
||||
|
||||
// Verify OTP strictly against backend OTPRecord table
|
||||
try {
|
||||
const verifyRes = await api.verifyOtp('phone', loginOtp);
|
||||
if (!verifyRes || !verifyRes.verified) {
|
||||
throw new Error(verifyRes?.error || 'Invalid or expired OTP code. Please enter the latest OTP received.');
|
||||
}
|
||||
} catch (otpErr: any) {
|
||||
// Clear session since OTP verification failed
|
||||
localStorage.removeItem('tradhox_user_auth');
|
||||
localStorage.removeItem('tradhox_user');
|
||||
const msg = otpErr?.message || 'Invalid or expired OTP code. Please enter the latest OTP received.';
|
||||
setLoginError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.user?.profile?.is_profile_complete) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
navigate('/onboarding');
|
||||
}
|
||||
if (data?.user?.profile?.is_profile_complete) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
if (!emailAddress) {
|
||||
setLoginError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
const data = await login(emailAddress, password);
|
||||
if (data?.user?.profile?.is_profile_complete) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
navigate('/onboarding');
|
||||
}
|
||||
navigate('/onboarding');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Login error:', err);
|
||||
|
|
@ -446,55 +418,6 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
Access your dashboard, manage orders, and grow your authentic craft brand.
|
||||
</p>
|
||||
|
||||
{/* Segmented Toggle Tabs */}
|
||||
<div
|
||||
className="mb-5 p-1 is-flex"
|
||||
style={{
|
||||
backgroundColor: '#F1F3F5',
|
||||
borderRadius: '9999px',
|
||||
width: '100%',
|
||||
maxWidth: '420px'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLoginTab('mobile')}
|
||||
className="button is-small is-flex-grow-1"
|
||||
style={{
|
||||
backgroundColor: loginTab === 'mobile' ? '#FFFFFF' : 'transparent',
|
||||
color: loginTab === 'mobile' ? '#1A202C' : '#718096',
|
||||
fontWeight: loginTab === 'mobile' ? 700 : 500,
|
||||
border: 'none',
|
||||
borderRadius: '9999px',
|
||||
boxShadow: loginTab === 'mobile' ? '0 2px 6px rgba(0, 0, 0, 0.08)' : 'none',
|
||||
height: '36px',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<Smartphone size={14} className="mr-2" />
|
||||
<span>Mobile OTP Login</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLoginTab('email')}
|
||||
className="button is-small is-flex-grow-1"
|
||||
style={{
|
||||
backgroundColor: loginTab === 'email' ? '#FFFFFF' : 'transparent',
|
||||
color: loginTab === 'email' ? '#1A202C' : '#718096',
|
||||
fontWeight: loginTab === 'email' ? 700 : 500,
|
||||
border: 'none',
|
||||
borderRadius: '9999px',
|
||||
boxShadow: loginTab === 'email' ? '0 2px 6px rgba(0, 0, 0, 0.08)' : 'none',
|
||||
height: '36px',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<Mail size={14} className="mr-2" />
|
||||
<span>Email & Password</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loginError && (
|
||||
<div className="notification is-danger is-light p-2.5 mb-4 is-size-7">
|
||||
{loginError}
|
||||
|
|
@ -503,236 +426,129 @@ export const LoginPage: React.FC<LoginPageProps> = ({ initialMode }) => {
|
|||
|
||||
{/* Login Form Contents */}
|
||||
<form onSubmit={handleLoginSubmit}>
|
||||
{loginTab === 'mobile' ? (
|
||||
<>
|
||||
{/* Mobile Number Field */}
|
||||
<div className="field mb-1">
|
||||
<label
|
||||
className="label mb-1.5"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
REGISTERED MOBILE NUMBER
|
||||
</label>
|
||||
{/* Mobile Number Field */}
|
||||
<div className="field mb-1">
|
||||
<label
|
||||
className="label mb-1.5"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
REGISTERED MOBILE NUMBER
|
||||
</label>
|
||||
|
||||
<div
|
||||
className="is-flex is-align-items-center"
|
||||
style={{
|
||||
border: isPhoneFocused ? '1.5px solid var(--trad-maroon)' : '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
overflow: 'hidden',
|
||||
boxShadow: isPhoneFocused ? '0 0 0 3px rgba(103, 27, 38, 0.12)' : 'none',
|
||||
transition: 'border-color 0.15s ease, box-shadow 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="pl-3.5 pr-2.5 is-flex is-align-items-center"
|
||||
style={{
|
||||
color: '#4A5568',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
IN +91
|
||||
</span>
|
||||
<div style={{ width: '1px', height: '22px', backgroundColor: '#E2E8F0', flexShrink: 0 }} />
|
||||
<input
|
||||
type="tel"
|
||||
maxLength={10}
|
||||
placeholder="Enter 10–digit mobile number"
|
||||
value={mobileNumber}
|
||||
onChange={(e) => setMobileNumber(e.target.value)}
|
||||
onFocus={() => setIsPhoneFocused(true)}
|
||||
onBlur={() => setIsPhoneFocused(false)}
|
||||
style={{
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C',
|
||||
flex: 1,
|
||||
padding: '0 12px',
|
||||
minWidth: 0
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetOtp}
|
||||
className="button is-ghost px-3 px-4-tablet"
|
||||
style={{
|
||||
color: 'var(--trad-maroon)',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
height: '46px',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
Get OTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="is-flex is-align-items-center"
|
||||
style={{
|
||||
border: isPhoneFocused ? '1.5px solid var(--trad-maroon)' : '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#FFFFFF',
|
||||
overflow: 'hidden',
|
||||
boxShadow: isPhoneFocused ? '0 0 0 3px rgba(103, 27, 38, 0.12)' : 'none',
|
||||
transition: 'border-color 0.15s ease, box-shadow 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="pl-3.5 pr-2.5 is-flex is-align-items-center"
|
||||
style={{
|
||||
color: '#4A5568',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
IN +91
|
||||
</span>
|
||||
<div style={{ width: '1px', height: '22px', backgroundColor: '#E2E8F0', flexShrink: 0 }} />
|
||||
<input
|
||||
type="tel"
|
||||
maxLength={10}
|
||||
placeholder="Enter 10–digit mobile number"
|
||||
value={mobileNumber}
|
||||
onChange={(e) => setMobileNumber(e.target.value)}
|
||||
onFocus={() => setIsPhoneFocused(true)}
|
||||
onBlur={() => setIsPhoneFocused(false)}
|
||||
style={{
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
boxShadow: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C',
|
||||
flex: 1,
|
||||
padding: '0 12px',
|
||||
minWidth: 0
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGetOtp}
|
||||
className="button is-ghost px-3 px-4-tablet"
|
||||
style={{
|
||||
color: 'var(--trad-maroon)',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.85rem',
|
||||
height: '46px',
|
||||
textDecoration: 'none',
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
Get OTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verification SMS notice & Forgot password row */}
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-4 mt-1.5 px-0.5">
|
||||
<span className="is-size-7" style={{ color: '#718096' }}>
|
||||
A 6-digit verification code will be sent via SMS.
|
||||
</span>
|
||||
<span
|
||||
onClick={switchToReset}
|
||||
style={{
|
||||
color: '#1A202C',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Forgot password?
|
||||
</span>
|
||||
</div>
|
||||
{/* Verification SMS notice & Forgot password row */}
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-4 mt-1.5 px-0.5">
|
||||
<span className="is-size-7" style={{ color: '#718096' }}>
|
||||
A 6-digit verification code will be sent via SMS.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* OTP Input Field */}
|
||||
<div className="field mb-5">
|
||||
<label
|
||||
className="label mb-1.5"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
ONE-TIME PASSWORD (OTP)
|
||||
</label>
|
||||
<div className="control">
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
placeholder="Enter 6–digit OTP received"
|
||||
value={loginOtp}
|
||||
onChange={(e) => setLoginOtp(e.target.value)}
|
||||
className="input"
|
||||
style={{
|
||||
border: '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{otpSent && (
|
||||
<p className="help has-text-success mt-1">
|
||||
✓ Verification code dispatched to +91 {mobileNumber} via WhatsApp Service.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Email Address Field */}
|
||||
<div className="field mb-4">
|
||||
<label
|
||||
className="label mb-1.5"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
REGISTERED EMAIL ADDRESS
|
||||
</label>
|
||||
<div className="control has-icons-left">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter registered email address"
|
||||
value={emailAddress}
|
||||
onChange={(e) => setEmailAddress(e.target.value)}
|
||||
className="input"
|
||||
style={{
|
||||
border: '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C'
|
||||
}}
|
||||
/>
|
||||
<span className="icon is-small is-left" style={{ height: '46px' }}>
|
||||
<Mail size={16} color="#718096" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Password Field */}
|
||||
<div className="field mb-1">
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-1.5">
|
||||
<label
|
||||
className="label mb-0"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
PASSWORD
|
||||
</label>
|
||||
<span
|
||||
onClick={switchToReset}
|
||||
style={{
|
||||
color: '#1A202C',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Forgot password?
|
||||
</span>
|
||||
</div>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter your account password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="input"
|
||||
style={{
|
||||
border: '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C'
|
||||
}}
|
||||
/>
|
||||
<span className="icon is-small is-left" style={{ height: '46px' }}>
|
||||
<Lock size={16} color="#718096" />
|
||||
</span>
|
||||
<span
|
||||
className="icon is-small is-right is-clickable"
|
||||
style={{ height: '46px', cursor: 'pointer', pointerEvents: 'all' }}
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} color="#718096" /> : <Eye size={16} color="#718096" />}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* OTP Input Field */}
|
||||
<div className="field mb-5">
|
||||
<label
|
||||
className="label mb-1.5"
|
||||
style={{
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 700,
|
||||
color: '#718096',
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase'
|
||||
}}
|
||||
>
|
||||
ONE-TIME PASSWORD (OTP)
|
||||
</label>
|
||||
<div className="control">
|
||||
<input
|
||||
type="text"
|
||||
maxLength={6}
|
||||
placeholder="Enter 6–digit OTP received"
|
||||
value={loginOtp}
|
||||
onChange={(e) => setLoginOtp(e.target.value)}
|
||||
className="input"
|
||||
style={{
|
||||
border: '1.5px solid #E2E8F0',
|
||||
borderRadius: '12px',
|
||||
height: '46px',
|
||||
fontSize: '0.9rem',
|
||||
color: '#1A202C'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{otpSent && (
|
||||
<p className="help has-text-success mt-1">
|
||||
✓ Verification code dispatched to +91 {mobileNumber} via WhatsApp Service.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remember Device Checkbox */}
|
||||
<div className="field mb-4">
|
||||
|
|
|
|||
|
|
@ -90,6 +90,93 @@ export const OnboardingPage: React.FC = () => {
|
|||
const [step2EnrolmentIdInput, setStep2EnrolmentIdInput] = useState('');
|
||||
const [step2EnrolmentVerified, setStep2EnrolmentVerified] = useState(false);
|
||||
|
||||
// Step 2 Email OTP Verification State
|
||||
const [emailOtp, setEmailOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||
const [emailOtpSent, setEmailOtpSent] = useState(false);
|
||||
const [emailOtpCountdown, setEmailOtpCountdown] = useState(0);
|
||||
const [emailOtpLoading, setEmailOtpLoading] = useState(false);
|
||||
const [emailOtpError, setEmailOtpError] = useState('');
|
||||
const [emailOtpSuccessMsg, setEmailOtpSuccessMsg] = useState('');
|
||||
const [isEmailVerified, setIsEmailVerified] = useState(onboarding.isEmailVerified || false);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
if (emailOtpCountdown > 0) {
|
||||
timer = setTimeout(() => setEmailOtpCountdown(emailOtpCountdown - 1), 1000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [emailOtpCountdown]);
|
||||
|
||||
const handleSendEmailOtp = async () => {
|
||||
setEmailOtpError('');
|
||||
setEmailOtpSuccessMsg('');
|
||||
const email = (onboarding.email || '').trim();
|
||||
if (!email || !email.includes('@')) {
|
||||
setEmailOtpError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
setEmailOtpLoading(true);
|
||||
try {
|
||||
await api.sendEmailOtp(email, undefined, 'verification');
|
||||
setEmailOtpSent(true);
|
||||
setEmailOtp(['', '', '', '', '', '']);
|
||||
setEmailOtpSuccessMsg(`✓ Verification code dispatched to ${email}`);
|
||||
setEmailOtpCountdown(60);
|
||||
setTimeout(() => {
|
||||
document.getElementById('email-otp-0')?.focus();
|
||||
}, 300);
|
||||
} catch (err: any) {
|
||||
console.warn('Failed to send email OTP:', err);
|
||||
setEmailOtpError(err?.message || 'Failed to dispatch verification code. Please try again.');
|
||||
} finally {
|
||||
setEmailOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailOtpChange = (index: number, val: string) => {
|
||||
const clean = val.replace(/[^0-9]/g, '').slice(-1);
|
||||
const newOtp = [...emailOtp];
|
||||
newOtp[index] = clean;
|
||||
setEmailOtp(newOtp);
|
||||
if (clean && index < 5) {
|
||||
document.getElementById(`email-otp-${index + 1}`)?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailOtpKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Backspace' && !emailOtp[index] && index > 0) {
|
||||
const prev = document.getElementById(`email-otp-${index - 1}`);
|
||||
prev?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyEmailOtp = async () => {
|
||||
setEmailOtpError('');
|
||||
setEmailOtpSuccessMsg('');
|
||||
const email = (onboarding.email || '').trim();
|
||||
const code = emailOtp.join('');
|
||||
if (code.length < 6) {
|
||||
setEmailOtpError('Please enter the complete 6-digit OTP code');
|
||||
return;
|
||||
}
|
||||
setEmailOtpLoading(true);
|
||||
try {
|
||||
const res = await api.verifyOtp('email', code, email);
|
||||
if (res && res.verified) {
|
||||
setIsEmailVerified(true);
|
||||
updateOnboarding({ isEmailVerified: true, email });
|
||||
setEmailOtpSuccessMsg('✓ Email address verified successfully!');
|
||||
setEmailOtpSent(false);
|
||||
} else {
|
||||
setEmailOtpError(res?.error || 'Invalid or expired OTP code.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
setEmailOtpError(err?.message || 'Invalid or expired OTP code.');
|
||||
} finally {
|
||||
setEmailOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3 Specific State matching stitch wireframe
|
||||
const [step3SameAsBusiness, setStep3SameAsBusiness] = useState(false);
|
||||
const [step3Landmark, setStep3Landmark] = useState('');
|
||||
|
|
@ -221,7 +308,7 @@ export const OnboardingPage: React.FC = () => {
|
|||
setStep1Loading(true);
|
||||
try {
|
||||
// Real-time backend OTP verification call with strict validation
|
||||
const res = await api.verifyOtp('phone', enteredOtp);
|
||||
const res = await api.verifyOtp('phone', enteredOtp, cleanPhone);
|
||||
if (res && res.verified) {
|
||||
updateOnboarding({ isOtpVerified: true, phone: cleanPhone });
|
||||
handleNext();
|
||||
|
|
@ -1132,7 +1219,7 @@ export const OnboardingPage: React.FC = () => {
|
|||
}, 300);
|
||||
} catch (resendErr: any) {
|
||||
console.warn('WhatsApp service call exception:', resendErr);
|
||||
setStep1Error('Failed to resend OTP. Please try again.');
|
||||
setStep1Error(resendErr?.message || 'Failed to resend OTP. Please try again.');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
|
|
@ -1142,14 +1229,27 @@ export const OnboardingPage: React.FC = () => {
|
|||
setStep1OtpSuccessMsg(`✓ Verification code dispatched to +91 ${clean} via WhatsApp Service.`);
|
||||
setOtpCountdown(90);
|
||||
setHasSentOtp(true);
|
||||
setStep1Error('');
|
||||
setTimeout(() => {
|
||||
document.getElementById('step1-otp-0')?.focus();
|
||||
}, 300);
|
||||
} catch (err: any) {
|
||||
console.warn('Registration failed:', err);
|
||||
const msg = err?.message || 'Mobile number is already registered. Please log in instead.';
|
||||
setStep1Error(msg.includes('already registered') ? 'Mobile number is already registered. Please log in instead.' : msg);
|
||||
setStep1OtpSuccessMsg('');
|
||||
console.warn('Registration notice, dispatching OTP directly:', err);
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
try {
|
||||
await api.sendWhatsAppOtp(clean, code, 'mobile_number_verification');
|
||||
setStep1Otp(['', '', '', '', '', '']);
|
||||
setStep1OtpSuccessMsg(`✓ Verification code dispatched to +91 ${clean} via WhatsApp Service.`);
|
||||
setOtpCountdown(90);
|
||||
setHasSentOtp(true);
|
||||
setStep1Error('');
|
||||
setTimeout(() => {
|
||||
document.getElementById('step1-otp-0')?.focus();
|
||||
}, 300);
|
||||
} catch (otpErr: any) {
|
||||
setStep1Error(otpErr?.message || 'Failed to dispatch verification code. Please try again.');
|
||||
setStep1OtpSuccessMsg('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1506,7 +1606,8 @@ export const OnboardingPage: React.FC = () => {
|
|||
border: '1.5px solid #CBD5E0',
|
||||
fontSize: '0.88rem',
|
||||
color: '#111827',
|
||||
boxShadow: 'none'
|
||||
boxShadow: 'none',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
import { api } from '../../services/api';
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
|
|
@ -72,8 +73,60 @@ const CLUSTERS: Record<string, string[]> = {
|
|||
};
|
||||
|
||||
export const AddProductPage: React.FC = () => {
|
||||
const [step, setStep] = useState(2); // Setting to 2 by default for demonstration
|
||||
const [step, setStep] = useState(1);
|
||||
const [autoSaved, setAutoSaved] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [uploadingCover, setUploadingCover] = useState(false);
|
||||
const [uploadingSupporting, setUploadingSupporting] = useState<boolean[]>([false, false, false, false]);
|
||||
const [uploadingVideo, setUploadingVideo] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Step 2 state - S3 URLs for API submission
|
||||
const [image, setImage] = useState('');
|
||||
const [supportingImages, setSupportingImages] = useState<string[]>(['', '', '', '']);
|
||||
const [videoUrl, setVideoUrl] = useState('');
|
||||
const [videoName, setVideoName] = useState('');
|
||||
|
||||
// Step 2 state - Local previews for seamless, unblocked browser rendering
|
||||
const [coverPreview, setCoverPreview] = useState<string>('');
|
||||
const [supportingPreviews, setSupportingPreviews] = useState<string[]>(['', '', '', '']);
|
||||
const [videoPreview, setVideoPreview] = useState<string>('');
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setUploadingCover(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
// 1. Instant reliable local preview
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
setCoverPreview(localUrl);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
if (!image) setImage(reader.result as string);
|
||||
simulateAutoSave();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// 2. Upload to S3 storage bucket
|
||||
try {
|
||||
const presignedData = await api.getPresignedUrl('cover', file.type || 'image/jpeg', sku);
|
||||
if (presignedData && presignedData.presigned_url) {
|
||||
await api.uploadToS3(presignedData.presigned_url, file);
|
||||
console.log('Successfully uploaded product cover to S3:', presignedData.s3_key);
|
||||
if (presignedData.s3_key) {
|
||||
setImage(`https://betasupplierdocumentstorage.s3.ap-south-2.amazonaws.com/${presignedData.s3_key}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('S3 direct upload notice (using local preview fallback):', err);
|
||||
} finally {
|
||||
setUploadingCover(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const { addProduct } = useSeller();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -84,8 +137,86 @@ export const AddProductPage: React.FC = () => {
|
|||
const [giTagged, setGiTagged] = useState(false);
|
||||
const [giCertRef, setGiCertRef] = useState('');
|
||||
|
||||
// Step 2 state
|
||||
const [image, setImage] = useState('');
|
||||
const handleSupportingPhotoUpload = async (index: number, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setUploadingSupporting(prev => {
|
||||
const n = [...prev];
|
||||
n[index] = true;
|
||||
return n;
|
||||
});
|
||||
|
||||
// 1. Local preview first
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
setSupportingPreviews(prev => {
|
||||
const n = [...prev];
|
||||
n[index] = localUrl;
|
||||
return n;
|
||||
});
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setSupportingImages(prev => {
|
||||
const next = [...prev];
|
||||
if (!next[index]) next[index] = reader.result as string;
|
||||
return next;
|
||||
});
|
||||
simulateAutoSave();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// 2. S3 direct upload
|
||||
try {
|
||||
const presignedData = await api.getPresignedUrl(`gallery_${index + 1}`, file.type || 'image/jpeg', sku);
|
||||
if (presignedData && presignedData.presigned_url) {
|
||||
await api.uploadToS3(presignedData.presigned_url, file);
|
||||
if (presignedData.s3_key) {
|
||||
const s3Url = `https://betasupplierdocumentstorage.s3.ap-south-2.amazonaws.com/${presignedData.s3_key}`;
|
||||
setSupportingImages(prev => {
|
||||
const next = [...prev];
|
||||
next[index] = s3Url;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Supporting photo S3 upload fallback:', err);
|
||||
} finally {
|
||||
setUploadingSupporting(prev => {
|
||||
const n = [...prev];
|
||||
n[index] = false;
|
||||
return n;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setUploadingVideo(true);
|
||||
setVideoName(file.name);
|
||||
const localUrl = URL.createObjectURL(file);
|
||||
setVideoPreview(localUrl);
|
||||
setVideoUrl(localUrl);
|
||||
simulateAutoSave();
|
||||
|
||||
try {
|
||||
const presignedData = await api.getPresignedUrl('video', file.type || 'video/mp4', sku);
|
||||
if (presignedData && presignedData.presigned_url) {
|
||||
await api.uploadToS3(presignedData.presigned_url, file);
|
||||
if (presignedData.s3_key) {
|
||||
const s3Url = `https://betasupplierdocumentstorage.s3.ap-south-2.amazonaws.com/${presignedData.s3_key}`;
|
||||
setVideoUrl(s3Url);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Video S3 upload fallback:', err);
|
||||
} finally {
|
||||
setUploadingVideo(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3 state
|
||||
const [title, setTitle] = useState('');
|
||||
|
|
@ -115,22 +246,50 @@ export const AddProductPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleFinish = async () => {
|
||||
if (!title.trim()) {
|
||||
setErrorMessage('Please provide a Product Title in Step 3 before publishing.');
|
||||
setStep(3);
|
||||
return;
|
||||
}
|
||||
if (!price || Number(price) <= 0) {
|
||||
setErrorMessage('Please enter a valid Selling Price in Step 4.');
|
||||
setStep(4);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
const activeSupportingImages = supportingImages.filter(img => img && img.trim().length > 0);
|
||||
await addProduct({
|
||||
sku: sku || `TRDX-${Date.now().toString().slice(-5)}`,
|
||||
title: title || 'Authentic Handcrafted Item',
|
||||
category: category || 'Handicrafts & Art',
|
||||
description: story || `${title} - Handcrafted ${category}`,
|
||||
craftTechnique: craftType || craftTechnique || 'Traditional',
|
||||
giTagged,
|
||||
price: Number(price) || 1000,
|
||||
mrp: Number(mrp) || 1500,
|
||||
price: Number(price),
|
||||
mrp: Number(mrp) || Math.round(Number(price) * 1.2),
|
||||
compare_at_price: Number(mrp) || Math.round(Number(price) * 1.2),
|
||||
stock: madeToOrder ? 0 : (Number(stock) || 10),
|
||||
status: madeToOrder ? 'active' : (Number(stock) === 0 ? 'out_of_stock' : Number(stock) < 5 ? 'low_stock' : 'active'),
|
||||
status: 'pending' as any,
|
||||
image: image || '/terracotta_vases.jpg',
|
||||
additional_images: activeSupportingImages,
|
||||
video_url: videoUrl || '',
|
||||
package_weight: Number(weightKg) || 1.0,
|
||||
shipping_profile: fragile ? 'Fragile Handcrafted' : 'Standard Fragile',
|
||||
dimensions: dimensions || undefined,
|
||||
materials: materials || undefined,
|
||||
care_instructions: careInstructions || undefined,
|
||||
tags: tagsList
|
||||
});
|
||||
navigate('/dashboard/inventory');
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.error('Failed to add product:', e);
|
||||
setErrorMessage(e.message || 'Failed to publish product. Please check required fields.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -251,6 +410,13 @@ export const AddProductPage: React.FC = () => {
|
|||
{/* ── Step Content Card ── */}
|
||||
<div style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '20px', padding: '2.5rem', boxShadow: '0 4px 12px rgba(0,0,0,0.02)' }}>
|
||||
|
||||
{errorMessage && (
|
||||
<div style={{ backgroundColor: '#FEF2F2', border: '1px solid #F87171', borderRadius: '12px', padding: '12px 16px', marginBottom: '1.5rem', display: 'flex', alignItems: 'center', gap: '10px', color: '#991B1B', fontSize: '0.85rem', fontWeight: 600 }}>
|
||||
<AlertTriangle size={18} color="#DC2626" />
|
||||
<span>{errorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ━━━━ STEP 1: Category ━━━━ */}
|
||||
{step === 1 && (
|
||||
<div>
|
||||
|
|
@ -313,14 +479,56 @@ export const AddProductPage: React.FC = () => {
|
|||
<label style={{ fontSize: '0.84rem', fontWeight: 700, color: '#111827' }}>Primary Cover Photo <span style={{ color: '#DC2626' }}>*</span></label>
|
||||
<span style={{ fontSize: '0.75rem', color: '#6B7280' }}>Shown on search cards & storefront hero</span>
|
||||
</div>
|
||||
<div style={{ border: '2px dashed #D1D5DB', borderRadius: '16px', padding: '3rem 2rem', textAlign: 'center', backgroundColor: '#FAFAFA', cursor: 'pointer', transition: 'background 0.2s ease' }} onMouseOver={e => e.currentTarget.style.backgroundColor = '#F3F4F6'} onMouseOut={e => e.currentTarget.style.backgroundColor = '#FAFAFA'}>
|
||||
<div style={{ width: '48px', height: '48px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px' }}>
|
||||
<ImageIcon size={20} color="#991B1B" />
|
||||
</div>
|
||||
<p style={{ fontSize: '1rem', fontWeight: 700, color: '#111827' }}>
|
||||
Drag and drop high-res cover image, or <span style={{ color: '#701A29', textDecoration: 'underline' }}>Browse Files</span>
|
||||
</p>
|
||||
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginTop: '6px' }}>Recommended 2000×2000px · Clean lighting · Up to 15MB (JPG, PNG, WebP)</p>
|
||||
<div
|
||||
style={{ border: '2px dashed #D1D5DB', borderRadius: '16px', padding: image ? '1.5rem' : '3rem 2rem', textAlign: 'center', backgroundColor: '#FAFAFA', position: 'relative', cursor: 'pointer', transition: 'background 0.2s ease' }}
|
||||
onMouseOver={e => e.currentTarget.style.backgroundColor = '#F3F4F6'}
|
||||
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FAFAFA'}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileChange}
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer', zIndex: 10 }}
|
||||
/>
|
||||
{coverPreview || image ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<img
|
||||
src={coverPreview || image}
|
||||
alt="Product Cover Preview"
|
||||
style={{ maxHeight: '220px', borderRadius: '12px', objectFit: 'contain', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
||||
onError={(e) => {
|
||||
console.warn('Cover preview failed to load directly, resetting fallback');
|
||||
}}
|
||||
/>
|
||||
{uploadingCover ? (
|
||||
<p style={{ fontSize: '0.8rem', color: '#701A29', fontWeight: 700 }}>Uploading cover to secure cloud storage...</p>
|
||||
) : (
|
||||
<p style={{ fontSize: '0.75rem', color: '#166534', fontWeight: 600 }}>✓ Cover photo ready</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImage('');
|
||||
setCoverPreview('');
|
||||
}}
|
||||
style={{ fontSize: '0.8rem', fontWeight: 700, color: '#DC2626', background: '#FEE2E2', border: 'none', borderRadius: '8px', padding: '6px 16px', cursor: 'pointer', zIndex: 20 }}
|
||||
>
|
||||
Remove / Change Cover Photo
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ width: '48px', height: '48px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '0 auto 12px' }}>
|
||||
<ImageIcon size={20} color="#991B1B" />
|
||||
</div>
|
||||
<p style={{ fontSize: '1rem', fontWeight: 700, color: '#111827' }}>
|
||||
{uploadingCover ? 'Uploading cover photo...' : 'Drag and drop high-res cover image, or '}
|
||||
{!uploadingCover && <span style={{ color: '#701A29', textDecoration: 'underline' }}>Browse Files</span>}
|
||||
</p>
|
||||
<p style={{ fontSize: '0.78rem', color: '#6B7280', marginTop: '6px' }}>Recommended 2000x2000px | Clean lighting | Up to 15MB (JPG, PNG, WebP)</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -332,41 +540,110 @@ export const AddProductPage: React.FC = () => {
|
|||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
|
||||
{[
|
||||
{ title: 'Detail / Texture', desc: 'Slot 2 · Glaze & Carving' },
|
||||
{ title: 'Scale & Context', desc: 'Slot 3 · In-Hand or Room' },
|
||||
{ title: 'Artisan Making', desc: 'Slot 4 · Workshop Craft' },
|
||||
{ title: 'Packaging & GI Tag', desc: 'Slot 5 · Safe Box & Seal' },
|
||||
].map((slot, i) => (
|
||||
<div key={i} style={{ border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.5rem 1rem', textAlign: 'center', backgroundColor: '#FFFFFF', cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '160px' }}>
|
||||
<div style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: '#FFF1F2', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '12px' }}>
|
||||
<Plus size={16} color="#991B1B" />
|
||||
{ title: 'Detail / Texture', desc: 'Slot 2 - Glaze & Carving' },
|
||||
{ title: 'Scale & Context', desc: 'Slot 3 - In-Hand or Room' },
|
||||
{ title: 'Artisan Making', desc: 'Slot 4 - Workshop Craft' },
|
||||
{ title: 'Packaging & GI Tag', desc: 'Slot 5 - Safe Box & Seal' },
|
||||
].map((slot, i) => {
|
||||
const slotPreview = supportingPreviews[i] || supportingImages[i];
|
||||
const isUploading = uploadingSupporting[i];
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
border: '1px solid #E5E7EB', borderRadius: '16px', padding: slotPreview ? '0.75rem' : '1.5rem 1rem',
|
||||
textAlign: 'center', backgroundColor: '#FFFFFF', position: 'relative', cursor: 'pointer',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '160px'
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(e) => handleSupportingPhotoUpload(i, e)}
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer', zIndex: 10 }}
|
||||
/>
|
||||
{slotPreview ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px', zIndex: 15, width: '100%' }}>
|
||||
<img src={slotPreview} alt={slot.title} style={{ width: '100%', height: '90px', objectFit: 'cover', borderRadius: '10px' }} />
|
||||
{isUploading ? (
|
||||
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: '#701A29' }}>Uploading...</span>
|
||||
) : (
|
||||
<span style={{ fontSize: '0.68rem', fontWeight: 600, color: '#166534' }}>✓ Attached</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSupportingImages(prev => {
|
||||
const next = [...prev];
|
||||
next[i] = '';
|
||||
return next;
|
||||
});
|
||||
setSupportingPreviews(prev => {
|
||||
const next = [...prev];
|
||||
next[i] = '';
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
style={{ fontSize: '0.68rem', fontWeight: 700, color: '#DC2626', background: '#FEE2E2', border: 'none', borderRadius: '6px', padding: '4px 10px', cursor: 'pointer', zIndex: 20 }}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: '#FFF1F2', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '12px' }}>
|
||||
<Plus size={16} color="#991B1B" />
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#111827' }}>
|
||||
{isUploading ? 'Uploading...' : slot.title}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#6B7280', marginTop: '2px' }}>{slot.desc}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', fontWeight: 700, color: '#111827' }}>{slot.title}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#6B7280', marginTop: '2px' }}>{slot.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Artisan Process Video */}
|
||||
<div style={{ border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.5rem', backgroundColor: '#FAFAFA', display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div style={{ width: '48px', height: '48px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Video size={20} color="#991B1B" />
|
||||
<div style={{ border: '1px solid #E5E7EB', borderRadius: '16px', padding: '1.5rem', backgroundColor: '#FAFAFA', display: 'flex', flexDirection: 'column', gap: '1rem', marginBottom: '2rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div style={{ width: '48px', height: '48px', borderRadius: '50%', backgroundColor: '#FEE2E2', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
||||
<Video size={20} color="#991B1B" />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ fontSize: '0.94rem', fontWeight: 800, color: '#111827' }}>Artisan Process Video (Short Reel)</div>
|
||||
<span style={{ backgroundColor: '#DCFCE7', color: '#166534', fontSize: '0.65rem', fontWeight: 700, padding: '2px 8px', borderRadius: '9999px' }}>+70% Buyer Trust</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', color: '#4B5563', marginTop: '4px', maxWidth: '420px', lineHeight: 1.4 }}>
|
||||
Upload a 10-30s video showing how this piece was handcrafted. Highlight raw natural materials, wheel shaping, or fine brush detailing.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ fontSize: '0.94rem', fontWeight: 800, color: '#111827' }}>Artisan Process Video (Short Reel)</div>
|
||||
<span style={{ backgroundColor: '#DCFCE7', color: '#166534', fontSize: '0.65rem', fontWeight: 700, padding: '2px 8px', borderRadius: '9999px' }}>+70% Buyer Trust</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.78rem', color: '#4B5563', marginTop: '4px', maxWidth: '420px', lineHeight: 1.4 }}>
|
||||
Upload a 10-30s video showing how this piece was handcrafted. Highlight raw natural materials, wheel shaping, or fine brush detailing.
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type="file"
|
||||
accept="video/*"
|
||||
onChange={handleVideoUpload}
|
||||
style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer', zIndex: 10 }}
|
||||
/>
|
||||
<button type="button" style={{ backgroundColor: videoName ? '#DCFCE7' : '#FFFFFF', border: `1px solid ${videoName ? '#166534' : '#D1D5DB'}`, borderRadius: '9999px', padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600, color: videoName ? '#166534' : '#374151', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
|
||||
<UploadCloud size={14} /> {uploadingVideo ? 'Uploading Video...' : videoName ? 'Change Video' : 'Upload Video'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button style={{ backgroundColor: '#FFFFFF', border: '1px solid #D1D5DB', borderRadius: '9999px', padding: '8px 16px', fontSize: '0.82rem', fontWeight: 600, color: '#374151', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer' }}>
|
||||
<UploadCloud size={14} /> Upload Video
|
||||
</button>
|
||||
{(videoPreview || videoUrl) && (
|
||||
<div style={{ borderTop: '1px solid #E5E7EB', paddingTop: '12px', display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<video src={videoPreview || videoUrl} controls style={{ maxHeight: '180px', borderRadius: '12px', backgroundColor: '#000' }} />
|
||||
<div style={{ fontSize: '0.78rem', color: '#166534', fontWeight: 600 }}>
|
||||
✓ Video attached: {videoName || 'Product Video Preview'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Heritage Media Guidelines */}
|
||||
|
|
@ -645,9 +922,9 @@ export const AddProductPage: React.FC = () => {
|
|||
<ArrowRight size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={handleFinish}
|
||||
style={{ fontSize: '0.9rem', fontWeight: 700, color: '#FFFFFF', backgroundColor: '#701A29', border: 'none', borderRadius: '9999px', padding: '10px 28px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: '0 4px 12px rgba(112,26,41,0.2)' }}>
|
||||
Publish Handcrafted Product 🚀
|
||||
<button type="button" onClick={handleFinish} disabled={isSubmitting}
|
||||
style={{ fontSize: '0.9rem', fontWeight: 700, color: '#FFFFFF', backgroundColor: isSubmitting ? '#9CA3AF' : '#701A29', border: 'none', borderRadius: '9999px', padding: '10px 28px', cursor: isSubmitting ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', gap: '8px', boxShadow: isSubmitting ? 'none' : '0 4px 12px rgba(112,26,41,0.2)' }}>
|
||||
{isSubmitting ? 'Publishing Product...' : 'Publish Handcrafted Product 🚀'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,22 +1,23 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
import { api } from '../../services/api';
|
||||
import {
|
||||
Search,
|
||||
Filter,
|
||||
Trash2,
|
||||
UploadCloud,
|
||||
Plus,
|
||||
Minus,
|
||||
Award,
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
Package,
|
||||
IndianRupee,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Edit3,
|
||||
ShieldCheck
|
||||
ShieldCheck,
|
||||
CheckCircle2,
|
||||
FileSpreadsheet
|
||||
} from 'lucide-react';
|
||||
|
||||
const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string; dot: string }> = {
|
||||
|
|
@ -27,8 +28,8 @@ const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string;
|
|||
};
|
||||
|
||||
function healthFromStock(stock: number): string {
|
||||
if (stock <= 4) return 'Critical';
|
||||
if (stock <= 10) return 'Low';
|
||||
if (stock === 0) return 'Critical';
|
||||
if (stock <= 4) return 'Low';
|
||||
if (stock <= 20) return 'Adequate';
|
||||
return 'Healthy';
|
||||
}
|
||||
|
|
@ -36,38 +37,82 @@ function healthFromStock(stock: number): string {
|
|||
const PAGE_SIZE = 10;
|
||||
|
||||
export const InventoryPage: React.FC = () => {
|
||||
const { inventory, updateStock, deleteProduct } = useSeller();
|
||||
const { inventory, updateStock, deleteProduct, fetchData } = useSeller();
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const getItemSourceInfo = (item: any) => {
|
||||
const isBulk = item.uploadSource === 'bulk' ||
|
||||
item.sku?.startsWith('TRDX-') ||
|
||||
item.sku?.startsWith('BATCH-') ||
|
||||
item.sku?.includes('CSV');
|
||||
if (isBulk) {
|
||||
return {
|
||||
type: 'bulk',
|
||||
label: 'Bulk CSV',
|
||||
icon: FileSpreadsheet,
|
||||
bg: '#EFF6FF',
|
||||
color: '#1D4ED8',
|
||||
border: '#BFDBFE'
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'individual',
|
||||
label: 'Single Craft',
|
||||
icon: Plus,
|
||||
bg: '#F0FDF4',
|
||||
color: '#15803D',
|
||||
border: '#BBF7D0'
|
||||
};
|
||||
};
|
||||
|
||||
const allSkus = inventory.map(inv => ({
|
||||
...inv,
|
||||
subtitle: inv.craftTechnique || inv.category,
|
||||
health: healthFromStock(inv.stock)
|
||||
health: healthFromStock(inv.stock),
|
||||
sourceInfo: getItemSourceInfo(inv)
|
||||
}));
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [stockFilter, setStockFilter] = useState<'all' | 'in_stock' | 'low_stock'>('all');
|
||||
const [sourceFilter, setSourceFilter] = useState<'all' | 'bulk' | 'individual'>('all');
|
||||
const [categoryFilter, setCategoryFilter] = useState('All');
|
||||
const [stockValues, setStockValues] = useState<Record<string, number>>(
|
||||
Object.fromEntries(allSkus.map(s => [s.id, s.stock]))
|
||||
);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [stockValues, setStockValues] = useState<Record<string, number>>({});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [editingItem, setEditingItem] = useState<any>(null);
|
||||
const [editPrice, setEditPrice] = useState<number | ''>('');
|
||||
const [isSavingEdit, setIsSavingEdit] = useState(false);
|
||||
|
||||
const categories = ['All', 'Pottery & Ceramics', 'Brass & Metal Craft', 'Woodcraft & Carvings', 'Handloom & Textiles', 'Jewellery & Ornaments'];
|
||||
useEffect(() => {
|
||||
setStockValues(prev => {
|
||||
const next = { ...prev };
|
||||
inventory.forEach(inv => {
|
||||
next[inv.id] = inv.stock;
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}, [inventory]);
|
||||
|
||||
const categories = ['All', ...Array.from(new Set(allSkus.map(s => s.category || 'General')))];
|
||||
|
||||
const filtered = allSkus.filter(item => {
|
||||
const q = searchTerm.toLowerCase();
|
||||
const matchSearch = !q || item.title.toLowerCase().includes(q) || item.sku.toLowerCase().includes(q) || item.category.toLowerCase().includes(q);
|
||||
const stk = stockValues[item.id] ?? item.stock;
|
||||
const matchStock = stockFilter === 'all' ? true : stockFilter === 'in_stock' ? stk > 4 : stk <= 4;
|
||||
const matchStock = stockFilter === 'all' ? true : stockFilter === 'in_stock' ? stk > 0 : stk <= 4;
|
||||
const matchSource = sourceFilter === 'all' ? true : sourceFilter === item.sourceInfo.type;
|
||||
const matchCat = categoryFilter === 'All' || item.category === categoryFilter;
|
||||
return matchSearch && matchStock && matchCat;
|
||||
return matchSearch && matchStock && matchSource && matchCat;
|
||||
});
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
||||
const paginated = filtered.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
||||
|
||||
const totalListed = allSkus.length;
|
||||
const bulkCount = allSkus.filter(s => s.sourceInfo.type === 'bulk').length;
|
||||
const singleCount = allSkus.filter(s => s.sourceInfo.type === 'individual').length;
|
||||
const lowStockCount = allSkus.filter(s => (stockValues[s.id] ?? s.stock) <= 4).length;
|
||||
const catalogValue = allSkus.reduce((sum, s) => sum + (stockValues[s.id] ?? s.stock) * s.price, 0);
|
||||
const saleableValue = allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 0)
|
||||
|
|
@ -75,7 +120,8 @@ export const InventoryPage: React.FC = () => {
|
|||
|
||||
const handleStepStock = (id: string, delta: number) => {
|
||||
setStockValues(prev => {
|
||||
const next = Math.max(0, (prev[id] ?? 0) + delta);
|
||||
const current = prev[id] ?? (inventory.find(i => i.id === id)?.stock ?? 0);
|
||||
const next = Math.max(0, current + delta);
|
||||
updateStock(id, next);
|
||||
return { ...prev, [id]: next };
|
||||
});
|
||||
|
|
@ -87,6 +133,39 @@ export const InventoryPage: React.FC = () => {
|
|||
updateStock(id, n);
|
||||
};
|
||||
|
||||
const handleDeleteProduct = async (id: string, title: string) => {
|
||||
if (window.confirm(`Are you sure you want to delete "${title}" from your inventory catalog?`)) {
|
||||
try {
|
||||
await deleteProduct(id);
|
||||
await fetchData();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Could not delete product');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEdit = async () => {
|
||||
if (!editingItem) return;
|
||||
setIsSavingEdit(true);
|
||||
const currentStock = stockValues[editingItem.id] ?? editingItem.stock;
|
||||
const patchPayload: any = { stock: currentStock };
|
||||
if (editPrice !== '' && !isNaN(Number(editPrice))) {
|
||||
patchPayload.price = Number(editPrice);
|
||||
}
|
||||
try {
|
||||
if (editingItem.id && !String(editingItem.id).startsWith('prod-')) {
|
||||
await api.patchProduct(editingItem.id, patchPayload);
|
||||
}
|
||||
await updateStock(editingItem.id, currentStock);
|
||||
await fetchData();
|
||||
} catch (err) {
|
||||
console.warn('Failed to update product via quick edit drawer:', err);
|
||||
} finally {
|
||||
setIsSavingEdit(false);
|
||||
setEditingItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
|
||||
|
|
@ -97,7 +176,7 @@ export const InventoryPage: React.FC = () => {
|
|||
Catalog & Inventory
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.82rem', color: '#6B7280', marginTop: '4px' }}>
|
||||
Manage your artisan SKUs, stock levels, and pricing across all craft disciplines.
|
||||
Manage your artisan SKUs (both single and bulk uploaded items), stock levels, and pricing.
|
||||
</p>
|
||||
</div>
|
||||
<div className="is-flex" style={{ gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||
|
|
@ -122,10 +201,11 @@ export const InventoryPage: React.FC = () => {
|
|||
<Package size={14} color="#BE123C" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 800, color: '#111827', lineHeight: 1.1 }}>{totalListed} <span style={{ fontSize: '1rem', fontWeight: 600, color: '#6B7280' }}>SKUs</span></div>
|
||||
<div style={{ fontSize: '1.8rem', fontWeight: 800, color: '#111827', lineHeight: 1.1 }}>{totalListed} <span style={{ fontSize: '0.9rem', fontWeight: 600, color: '#6B7280' }}>SKUs</span></div>
|
||||
<div className="is-flex is-align-items-center mt-2" style={{ gap: '6px', fontSize: '0.74rem' }}>
|
||||
<span style={{ color: '#16A34A', fontWeight: 700 }}>↑ +6 this week</span>
|
||||
<span style={{ color: '#6B7280' }}>· {allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 0).length} active</span>
|
||||
<span style={{ color: '#2563EB', fontWeight: 700 }}>{bulkCount} Bulk</span>
|
||||
<span style={{ color: '#6B7280' }}>·</span>
|
||||
<span style={{ color: '#16A34A', fontWeight: 700 }}>{singleCount} Single</span>
|
||||
</div>
|
||||
<div style={{ width: '36px', height: '3px', backgroundColor: '#701A29', borderRadius: '2px', marginTop: '8px' }} />
|
||||
</div>
|
||||
|
|
@ -139,8 +219,8 @@ export const InventoryPage: React.FC = () => {
|
|||
<AlertTriangle size={14} color="#DC2626" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '2rem', fontWeight: 800, color: lowStockCount > 0 ? '#DC2626' : '#16A34A', lineHeight: 1.1 }}>
|
||||
{lowStockCount} <span style={{ fontSize: '1rem', fontWeight: 600, color: '#6B7280' }}>Crafts</span>
|
||||
<div style={{ fontSize: '1.8rem', fontWeight: 800, color: lowStockCount > 0 ? '#DC2626' : '#16A34A', lineHeight: 1.1 }}>
|
||||
{lowStockCount} <span style={{ fontSize: '0.9rem', fontWeight: 600, color: '#6B7280' }}>Crafts</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.74rem', color: '#6B7280', marginTop: '6px' }}>
|
||||
{lowStockCount > 0 ? 'Restock urgently to avoid cancellations' : 'All SKUs adequately stocked'}
|
||||
|
|
@ -171,19 +251,43 @@ export const InventoryPage: React.FC = () => {
|
|||
{/* Filter Controls */}
|
||||
<div className="p-4" style={{ backgroundColor: '#FFFFFF', border: '1px solid #E5E7EB', borderRadius: '16px', boxShadow: '0 1px 3px rgba(0,0,0,0.02)' }}>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center is-flex-wrap-wrap" style={{ gap: '0.75rem' }}>
|
||||
|
||||
<div className="is-flex" style={{ gap: '6px', flexWrap: 'wrap' }}>
|
||||
{([
|
||||
{ key: 'all', label: `All (${allSkus.length})` },
|
||||
{ key: 'in_stock', label: `In Stock (${allSkus.filter(s => (stockValues[s.id] ?? s.stock) > 4).length})` },
|
||||
{ key: 'low_stock', label: `Low Stock (${lowStockCount})` },
|
||||
] as const).map(f => (
|
||||
<button key={f.key} type="button" onClick={() => { setStockFilter(f.key); setCurrentPage(1); }}
|
||||
className="button is-small"
|
||||
style={{ borderRadius: '9999px', backgroundColor: stockFilter === f.key ? '#701A29' : '#F9FAFB', borderColor: stockFilter === f.key ? '#701A29' : '#E5E7EB', color: stockFilter === f.key ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px', transition: 'all 0.15s ease' }}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSourceFilter('all'); setStockFilter('all'); setCurrentPage(1); }}
|
||||
className="button is-small"
|
||||
style={{ borderRadius: '9999px', backgroundColor: (sourceFilter === 'all' && stockFilter === 'all') ? '#701A29' : '#F9FAFB', borderColor: (sourceFilter === 'all' && stockFilter === 'all') ? '#701A29' : '#E5E7EB', color: (sourceFilter === 'all' && stockFilter === 'all') ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
|
||||
>
|
||||
All ({allSkus.length})
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSourceFilter('bulk'); setCurrentPage(1); }}
|
||||
className="button is-small"
|
||||
style={{ borderRadius: '9999px', backgroundColor: sourceFilter === 'bulk' ? '#2563EB' : '#F9FAFB', borderColor: sourceFilter === 'bulk' ? '#2563EB' : '#E5E7EB', color: sourceFilter === 'bulk' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
|
||||
>
|
||||
Bulk Uploaded ({bulkCount})
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSourceFilter('individual'); setCurrentPage(1); }}
|
||||
className="button is-small"
|
||||
style={{ borderRadius: '9999px', backgroundColor: sourceFilter === 'individual' ? '#16A34A' : '#F9FAFB', borderColor: sourceFilter === 'individual' ? '#16A34A' : '#E5E7EB', color: sourceFilter === 'individual' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
|
||||
>
|
||||
Single Uploaded ({singleCount})
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStockFilter('low_stock'); setCurrentPage(1); }}
|
||||
className="button is-small"
|
||||
style={{ borderRadius: '9999px', backgroundColor: stockFilter === 'low_stock' ? '#DC2626' : '#F9FAFB', borderColor: stockFilter === 'low_stock' ? '#DC2626' : '#E5E7EB', color: stockFilter === 'low_stock' ? '#FFFFFF' : '#374151', fontWeight: 600, fontSize: '0.78rem', height: '32px', padding: '0 14px' }}
|
||||
>
|
||||
Low Stock ({lowStockCount})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="is-flex is-align-items-center" style={{ gap: '0.6rem', flexWrap: 'wrap' }}>
|
||||
|
|
@ -222,7 +326,7 @@ export const InventoryPage: React.FC = () => {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ backgroundColor: '#FAFAFA', borderBottom: '1px solid #E5E7EB' }}>
|
||||
{['Craft Product & SKU', 'Category & GI Tag', 'Pricing', 'Stock Qty', 'Health', 'Actions'].map(h => (
|
||||
{['Craft Product & SKU', 'Category & GI Tag', 'Pricing', 'Stock Qty', 'Approval Status', 'Health', 'Actions'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 16px', fontSize: '0.68rem', fontWeight: 700, color: '#6B7280', letterSpacing: '0.04em', textTransform: 'uppercase', textAlign: 'left', whiteSpace: 'nowrap' }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -232,6 +336,8 @@ export const InventoryPage: React.FC = () => {
|
|||
const stk = stockValues[item.id] ?? item.stock;
|
||||
const health = (item as { health?: string }).health || healthFromStock(stk);
|
||||
const hConf = HEALTH_CONFIG[health] || HEALTH_CONFIG['Healthy'];
|
||||
const SrcIcon = item.sourceInfo.icon;
|
||||
|
||||
return (
|
||||
<tr key={item.id} style={{ borderBottom: idx < paginated.length - 1 ? '1px solid #F3F4F6' : 'none' }}
|
||||
onMouseEnter={e => (e.currentTarget.style.backgroundColor = '#FAFAFA')}
|
||||
|
|
@ -239,16 +345,33 @@ export const InventoryPage: React.FC = () => {
|
|||
>
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '260px' }}>
|
||||
<div className="is-flex is-align-items-center" style={{ gap: '12px' }}>
|
||||
<img src={item.image} alt={item.title}
|
||||
<img src={item.image || '/images/placeholder.jpg'} alt={item.title}
|
||||
style={{ width: '48px', height: '48px', borderRadius: '10px', objectFit: 'cover', border: '1px solid #E5E7EB', flexShrink: 0 }}
|
||||
onError={e => { (e.target as HTMLImageElement).src = '/terracotta_vases.jpg'; }}
|
||||
onError={e => { (e.target as HTMLImageElement).src = 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=200'; }}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.86rem', fontWeight: 700, color: '#111827', lineHeight: 1.3 }}>{item.title}</div>
|
||||
<div style={{ fontSize: '0.72rem', color: '#6B7280', marginTop: '2px' }}>
|
||||
{(item as { subtitle?: string }).subtitle || item.craftTechnique}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.68rem', fontWeight: 700, color: '#701A29', marginTop: '3px' }}>{item.sku}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '3px' }}>
|
||||
<span style={{ fontSize: '0.68rem', fontWeight: 700, color: '#701A29' }}>{item.sku}</span>
|
||||
<span style={{
|
||||
fontSize: '0.62rem',
|
||||
fontWeight: 700,
|
||||
backgroundColor: item.sourceInfo.bg,
|
||||
color: item.sourceInfo.color,
|
||||
border: `1px solid ${item.sourceInfo.border}`,
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px'
|
||||
}}>
|
||||
<SrcIcon size={9} />
|
||||
{item.sourceInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -265,8 +388,12 @@ export const InventoryPage: React.FC = () => {
|
|||
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
|
||||
<div style={{ fontSize: '1rem', fontWeight: 800, color: '#111827' }}>₹{item.price.toLocaleString('en-IN')}</div>
|
||||
<div style={{ fontSize: '0.72rem', color: '#9CA3AF', textDecoration: 'line-through', marginTop: '2px' }}>MRP ₹{item.mrp.toLocaleString('en-IN')}</div>
|
||||
<div style={{ fontSize: '0.68rem', color: '#16A34A', fontWeight: 700, marginTop: '2px' }}>{Math.round(((item.mrp - item.price) / item.mrp) * 100)}% off</div>
|
||||
{item.mrp && item.mrp > item.price && (
|
||||
<div style={{ fontSize: '0.72rem', color: '#9CA3AF', textDecoration: 'line-through', marginTop: '2px' }}>MRP ₹{item.mrp.toLocaleString('en-IN')}</div>
|
||||
)}
|
||||
{item.mrp && item.mrp > item.price && (
|
||||
<div style={{ fontSize: '0.68rem', color: '#16A34A', fontWeight: 700, marginTop: '2px' }}>{Math.round(((item.mrp - item.price) / item.mrp) * 100)}% off</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '140px' }}>
|
||||
|
|
@ -282,41 +409,61 @@ export const InventoryPage: React.FC = () => {
|
|||
<Plus size={12} color="#374151" />
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.68rem', color: '#9CA3AF', marginTop: '4px' }}>{item.salesCount} sold all-time</div>
|
||||
</td>
|
||||
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: hConf.bg, color: hConf.color, fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: hConf.dot, flexShrink: 0 }} />
|
||||
{hConf.label}
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '130px' }}>
|
||||
<span className="is-inline-flex is-align-items-center" style={{
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
padding: '4px 10px',
|
||||
borderRadius: '9999px',
|
||||
backgroundColor: item.approvalStatus === 'approved' ? '#DCFCE7' : item.approvalStatus === 'rejected' ? '#FEE2E2' : '#FEF3C7',
|
||||
color: item.approvalStatus === 'approved' ? '#166534' : item.approvalStatus === 'rejected' ? '#991B1B' : '#92400E',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<CheckCircle2 size={12} />
|
||||
<span>{item.approvalStatus === 'approved' ? 'Approved' : item.approvalStatus === 'rejected' ? 'Rejected' : 'Pending'}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '80px' }}>
|
||||
<div className="is-flex" style={{ gap: '4px' }}>
|
||||
<button type="button" title="Quick Edit SKU" onClick={() => setEditingItem(item)}
|
||||
style={{ width: '30px', height: '30px', borderRadius: '8px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' }}
|
||||
onMouseOver={e => e.currentTarget.style.backgroundColor = '#F3F4F6'}
|
||||
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FFFFFF'}
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
|
||||
<span className="is-inline-flex is-align-items-center" style={{
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
backgroundColor: hConf.bg,
|
||||
color: hConf.color,
|
||||
padding: '4px 10px',
|
||||
borderRadius: '9999px',
|
||||
gap: '5px'
|
||||
}}>
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: hConf.dot }}></span>
|
||||
<span>{hConf.label}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td style={{ padding: '14px 16px', verticalAlign: 'middle', textAlign: 'right' }}>
|
||||
<div className="is-flex is-align-items-center is-justify-content-end" style={{ gap: '6px' }}>
|
||||
<button type="button" onClick={() => { setEditingItem(item); setEditPrice(item.price); }}
|
||||
className="button is-small" style={{ borderRadius: '6px', height: '28px', padding: '0 8px', border: '1px solid #D1D5DB', backgroundColor: '#FFFFFF', color: '#374151' }}
|
||||
>
|
||||
<Edit3 size={13} color="#6B7280" />
|
||||
<Edit3 size={12} />
|
||||
</button>
|
||||
<button type="button" onClick={() => deleteProduct(item.id)} title="Delete SKU"
|
||||
style={{ width: '30px', height: '30px', borderRadius: '8px', border: '1px solid #FFE4E6', backgroundColor: '#FFF5F5', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' }}
|
||||
onMouseOver={e => e.currentTarget.style.backgroundColor = '#FEE2E2'}
|
||||
onMouseOut={e => e.currentTarget.style.backgroundColor = '#FFF5F5'}
|
||||
<button type="button" onClick={() => handleDeleteProduct(item.id, item.title)}
|
||||
className="button is-small" style={{ borderRadius: '6px', height: '28px', padding: '0 8px', border: '1px solid #FCA5A5', backgroundColor: '#FFFFFF', color: '#DC2626' }}
|
||||
>
|
||||
<Trash2 size={13} color="#DC2626" />
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
);
|
||||
}) : (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ padding: '3rem', textAlign: 'center' }}>
|
||||
<AlertCircle size={32} color="#D1D5DB" style={{ marginBottom: '8px' }} />
|
||||
<p style={{ fontSize: '0.88rem', color: '#6B7280' }}>No matching craft SKUs found.</p>
|
||||
<td colSpan={7} style={{ padding: '48px 24px', textAlign: 'center', color: '#6B7280' }}>
|
||||
<AlertTriangle size={24} color="#9CA3AF" style={{ margin: '0 auto 8px', display: 'block' }} />
|
||||
<p style={{ fontSize: '0.86rem', fontWeight: 600, color: '#374151' }}>No matching craft SKUs found.</p>
|
||||
<p style={{ fontSize: '0.78rem', color: '#9CA3AF', marginTop: '4px' }}>Try resetting your search query or filter settings above.</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
|
@ -324,117 +471,32 @@ export const InventoryPage: React.FC = () => {
|
|||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination Strip */}
|
||||
<div className="px-5 py-3 is-flex is-align-items-center is-justify-content-space-between" style={{ borderTop: '1px solid #F3F4F6', backgroundColor: '#FAFAFA', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.76rem', color: '#6B7280' }}>
|
||||
Showing <strong style={{ color: '#111827' }}>{Math.min(filtered.length, currentPage * PAGE_SIZE)}</strong> of{' '}
|
||||
<strong style={{ color: '#111827' }}>{filtered.length > 0 ? filtered.length : allSkus.length} Handcrafted SKUs</strong> · All GI Tags Verified
|
||||
</span>
|
||||
<div className="is-flex is-align-items-center" style={{ gap: '4px' }}>
|
||||
<button type="button" onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}
|
||||
style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', opacity: currentPage === 1 ? 0.4 : 1 }}>
|
||||
<ChevronLeft size={14} color="#374151" />
|
||||
</button>
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(pg => (
|
||||
<button key={pg} type="button" onClick={() => setCurrentPage(pg)}
|
||||
style={{ minWidth: '28px', height: '28px', borderRadius: '6px', border: `1px solid ${pg === currentPage ? '#701A29' : '#E5E7EB'}`, backgroundColor: pg === currentPage ? '#701A29' : '#FFFFFF', color: pg === currentPage ? '#FFFFFF' : '#374151', fontSize: '0.78rem', fontWeight: 600, cursor: 'pointer', padding: '0 6px' }}>
|
||||
{pg}
|
||||
{/* Pagination Footer */}
|
||||
{filtered.length > 0 && (
|
||||
<div className="px-5 py-3 is-flex is-align-items-center is-justify-content-space-between" style={{ borderTop: '1px solid #F3F4F6', fontSize: '0.78rem', color: '#6B7280' }}>
|
||||
<div>
|
||||
Showing <strong style={{ color: '#111827' }}>{paginated.length}</strong> of <strong style={{ color: '#111827' }}>{filtered.length}</strong> Handcrafted SKUs
|
||||
</div>
|
||||
<div className="is-flex is-align-items-center" style={{ gap: '6px' }}>
|
||||
<button type="button" onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1}
|
||||
className="button is-small" style={{ borderRadius: '6px', height: '28px', width: '28px', padding: 0, opacity: currentPage === 1 ? 0.4 : 1 }}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
</button>
|
||||
))}
|
||||
<button type="button" onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}
|
||||
style={{ width: '28px', height: '28px', borderRadius: '6px', border: '1px solid #E5E7EB', backgroundColor: '#FFFFFF', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: currentPage === totalPages ? 'not-allowed' : 'pointer', opacity: currentPage === totalPages ? 0.4 : 1 }}>
|
||||
<ChevronRight size={14} color="#374151" />
|
||||
</button>
|
||||
<span style={{ fontWeight: 700, color: '#701A29', backgroundColor: '#FFF0F2', width: '28px', height: '28px', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '6px' }}>
|
||||
{currentPage}
|
||||
</span>
|
||||
<button type="button" onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages}
|
||||
className="button is-small" style={{ borderRadius: '6px', height: '28px', width: '28px', padding: 0, opacity: currentPage === totalPages ? 0.4 : 1 }}
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Quick Edit Drawer */}
|
||||
{editingItem && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 100, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.7)', backdropFilter: 'blur(3px)' }}
|
||||
onClick={() => setEditingItem(null)}
|
||||
/>
|
||||
|
||||
{/* Drawer Panel */}
|
||||
<div style={{ position: 'relative', width: '440px', backgroundColor: '#FFFFFF', height: '100%', display: 'flex', flexDirection: 'column', boxShadow: '-4px 0 24px rgba(0,0,0,0.06)' }}>
|
||||
|
||||
{/* Drawer Header */}
|
||||
<div style={{ padding: '24px 32px', display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 800, color: '#111827' }}>Quick Edit SKU</h2>
|
||||
<div style={{ fontSize: '0.8rem', color: '#6B7280', marginTop: '2px' }}>SKU: {editingItem.sku}</div>
|
||||
</div>
|
||||
<button type="button" onClick={() => setEditingItem(null)} style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: '#F3F4F6', border: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#374151" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Drawer Content Scrollable */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0 32px 32px 32px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
|
||||
{/* Product Info Card */}
|
||||
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<img src={editingItem.image} alt="Product" style={{ width: '48px', height: '48px', borderRadius: '8px', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).src = '/terracotta_vases.jpg'; }} />
|
||||
<div>
|
||||
<div style={{ fontSize: '0.9rem', fontWeight: 700, color: '#111827', lineHeight: 1.2 }}>{editingItem.title}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6B7280', marginTop: '2px' }}>{editingItem.category} · Handcrafted</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}>
|
||||
{/* Toggle Switch */}
|
||||
<div style={{ width: '40px', height: '22px', backgroundColor: '#701A29', borderRadius: '11px', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '2px', cursor: 'pointer' }}>
|
||||
<div style={{ width: '18px', height: '18px', backgroundColor: '#FFFFFF', borderRadius: '50%' }} />
|
||||
</div>
|
||||
<div style={{ fontSize: '0.65rem', fontWeight: 700, color: '#166534' }}>Live on Storefront</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selling Price Card */}
|
||||
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.85rem', fontWeight: 600, color: '#374151', marginBottom: '8px' }}>Selling Price</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span style={{ position: 'absolute', left: '16px', top: '50%', transform: 'translateY(-50%)', fontWeight: 600, color: '#374151' }}>₹</span>
|
||||
<input type="number" defaultValue={editingItem.price} style={{ width: '100%', border: 'none', borderRadius: '4px', padding: '12px 16px 12px 32px', fontSize: '1rem', fontWeight: 700, color: '#111827', outline: 'none' }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', fontSize: '0.75rem' }}>
|
||||
<span style={{ color: '#6B7280' }}>Artisan Payout (100% retained)</span>
|
||||
<span style={{ fontWeight: 700, color: '#701A29', fontSize: '0.9rem' }}>₹{editingItem.price}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stock on Hand Card */}
|
||||
<div style={{ backgroundColor: '#FAF7F7', borderRadius: '12px', padding: '16px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151' }}>Stock on Hand</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#6B7280', marginTop: '2px' }}>Finished sets ready for dispatch</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', backgroundColor: '#FFFFFF', borderRadius: '4px', border: 'none', overflow: 'hidden' }}>
|
||||
<button type="button" onClick={() => handleStepStock(editingItem.id, -1)} style={{ width: '32px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: 'none', backgroundColor: '#FFFFFF', cursor: 'pointer', fontSize: '1.2rem', color: '#374151' }}>−</button>
|
||||
<div style={{ width: '36px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.9rem', fontWeight: 700, color: '#111827' }}>{(stockValues[editingItem.id] ?? editingItem.stock)}</div>
|
||||
<button type="button" onClick={() => handleStepStock(editingItem.id, 1)} style={{ width: '32px', height: '36px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: 'none', backgroundColor: '#FFFFFF', cursor: 'pointer', fontSize: '1.2rem', color: '#374151' }}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151', paddingLeft: '4px' }}>Lead Time / Dispatch Readiness</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Drawer Footer */}
|
||||
<div style={{ padding: '24px 32px', display: 'flex', justifyContent: 'flex-end', gap: '12px' }}>
|
||||
<button type="button" onClick={() => setEditingItem(null)} style={{ padding: '10px 24px', borderRadius: '9999px', border: 'none', backgroundColor: '#F3F4F6', color: '#374151', fontSize: '0.85rem', fontWeight: 600, cursor: 'pointer' }}>Cancel</button>
|
||||
<button type="button" onClick={() => setEditingItem(null)} style={{ padding: '10px 24px', borderRadius: '9999px', border: 'none', backgroundColor: '#701A29', color: '#FFFFFF', fontSize: '0.85rem', fontWeight: 700, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api';
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://betapartner-api.tradhox.com/api';
|
||||
|
||||
const getAuthHeaders = (isMultipart = false) => {
|
||||
const userStr = localStorage.getItem('tradhox_user_auth');
|
||||
|
|
@ -20,12 +20,41 @@ const getAuthHeaders = (isMultipart = false) => {
|
|||
};
|
||||
|
||||
export const api = {
|
||||
// S3 Presigned Uploads
|
||||
getPresignedUrl: async (fileType: string, contentType: string = 'image/jpeg', productId?: string) => {
|
||||
const res = await fetch(`${BASE_URL}/profile/presigned-url/`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ file_type: fileType, content_type: contentType, product_id: productId })
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to get presigned upload URL');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
uploadToS3: async (uploadUrl: string, file: File) => {
|
||||
const res = await fetch(uploadUrl, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': file.type || 'image/jpeg' },
|
||||
body: file
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to upload file to S3');
|
||||
return res;
|
||||
},
|
||||
|
||||
// Auth
|
||||
login: async (username: string, password?: string) => {
|
||||
login: async (username: string, password?: string, otpCode?: string, otpType?: string) => {
|
||||
const payload: any = {
|
||||
username,
|
||||
otp_code: otpCode,
|
||||
otp_type: otpType
|
||||
};
|
||||
if (password) {
|
||||
payload.password = password;
|
||||
}
|
||||
const res = await fetch(`${BASE_URL}/auth/login/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password: password || 'Password123!' })
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
|
|
@ -45,6 +74,7 @@ export const api = {
|
|||
const errData = await res.json().catch(() => ({}));
|
||||
let msg = 'Registration failed';
|
||||
if (errData.detail) msg = errData.detail;
|
||||
else if (errData.error) msg = errData.error;
|
||||
else if (errData.message) msg = errData.message;
|
||||
else if (typeof errData === 'object') {
|
||||
const firstKey = Object.keys(errData)[0];
|
||||
|
|
@ -58,6 +88,37 @@ export const api = {
|
|||
return await res.json();
|
||||
},
|
||||
|
||||
sendEmailOtp: async (email: string, otpCode?: string, templateName = 'verification') => {
|
||||
const url = `${BASE_URL}/auth/send-email-otp/`;
|
||||
try {
|
||||
const payload: any = { email, template_name: templateName };
|
||||
if (otpCode) payload.otp_code = otpCode;
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || data.detail || 'Your account status is inactive. Please contact Tradhox support for assistance.');
|
||||
}
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.warn('Email service call notice:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
sendWhatsAppOtp: async (mobileNumber: string, otpCode: string, templateName = 'mobile_number_verification') => {
|
||||
const cleanDigits = mobileNumber.replace(/\D/g, '');
|
||||
const formattedMobile = cleanDigits.startsWith('91') ? cleanDigits : `91${cleanDigits}`;
|
||||
|
|
@ -72,14 +133,14 @@ export const api = {
|
|||
template_name: templateName
|
||||
})
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
console.warn('WhatsApp API response notice:', errData);
|
||||
throw new Error(data.error || data.detail || data.message || 'Failed to dispatch OTP. Please try again.');
|
||||
}
|
||||
return await res.json().catch(() => ({ status: 'success' }));
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.warn('WhatsApp service network boundary notice:', err);
|
||||
return { status: 'success', message: 'WhatsApp message dispatched' };
|
||||
console.warn('WhatsApp service call notice:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -92,18 +153,49 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
verifyOtp: async (type: string, otp: string) => {
|
||||
const res = await fetch(`${BASE_URL}/auth/verify-otp/`, {
|
||||
verifyOtp: async (type: string, otp: string, phoneOrEmail?: string) => {
|
||||
const url = `${BASE_URL}/auth/verify-otp/`;
|
||||
const payload = {
|
||||
type,
|
||||
otp,
|
||||
phone: type === 'phone' ? phoneOrEmail : undefined,
|
||||
email: type === 'email' ? phoneOrEmail : undefined
|
||||
};
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify({ type, otp })
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem('tradhox_user_auth');
|
||||
res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
const msg = errData.error || errData.message || errData.detail || 'Failed to verify OTP';
|
||||
throw new Error(msg);
|
||||
}
|
||||
return res.json();
|
||||
const data = await res.json();
|
||||
if (data.access_token && data.user) {
|
||||
localStorage.setItem('tradhox_user_auth', JSON.stringify(data));
|
||||
localStorage.setItem('tradhox_user', JSON.stringify({
|
||||
id: String(data.user.id),
|
||||
name: data.user.first_name ? `${data.user.first_name} ${data.user.last_name || ''}`.trim() : data.user.username,
|
||||
email: data.user.email,
|
||||
phone: data.user.profile?.phone || (type === 'phone' ? phoneOrEmail : ''),
|
||||
role: 'Seller',
|
||||
avatar: data.user.profile?.logo_url || '/images/avatars/seller-main.jpg',
|
||||
storeName: data.user.profile?.store_name || 'My Store'
|
||||
}));
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
// Profile & Settings
|
||||
|
|
@ -149,7 +241,11 @@ export const api = {
|
|||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(productData)
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create product');
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
const msg = errData.detail || errData.error || errData.message || `Failed to create product (${res.status})`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
|
|
@ -163,6 +259,19 @@ export const api = {
|
|||
return res.json();
|
||||
},
|
||||
|
||||
patchProduct: async (id: string | number, productData: any) => {
|
||||
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||
method: 'PATCH',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(productData)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.detail || errData.error || 'Failed to update product');
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
deleteProduct: async (id: string | number) => {
|
||||
const res = await fetch(`${BASE_URL}/products/${id}/`, {
|
||||
method: 'DELETE',
|
||||
|
|
@ -181,11 +290,29 @@ export const api = {
|
|||
});
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.error || errData.message || 'Bulk upload failed');
|
||||
const err: any = new Error(errData.error || errData.message || 'Bulk upload failed');
|
||||
err.response = errData;
|
||||
throw err;
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
downloadBulkUploadErrorReport: async (logId: string) => {
|
||||
const res = await fetch(`${BASE_URL}/products/bulk-upload/error-report/?log_id=${encodeURIComponent(logId)}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to download error report');
|
||||
return res.blob();
|
||||
},
|
||||
|
||||
getBulkUploadLogs: async () => {
|
||||
const res = await fetch(`${BASE_URL}/products/bulk-upload/logs/`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch bulk upload logs');
|
||||
return res.json();
|
||||
},
|
||||
|
||||
// Orders & Forward Shipping
|
||||
getOrders: async () => {
|
||||
const res = await fetch(`${BASE_URL}/orders/`, { headers: getAuthHeaders() });
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface OnboardingData {
|
|||
email: string;
|
||||
phone: string;
|
||||
isOtpVerified: boolean;
|
||||
isEmailVerified?: boolean;
|
||||
|
||||
// Step 2: GST / Enrolment ID
|
||||
taxMode: 'gstin' | 'enrolment';
|
||||
|
|
@ -69,8 +70,19 @@ export interface InventoryItem {
|
|||
mrp: number;
|
||||
stock: number;
|
||||
status: 'active' | 'low_stock' | 'out_of_stock';
|
||||
approvalStatus?: 'approved' | 'pending' | 'rejected';
|
||||
image: string;
|
||||
salesCount: number;
|
||||
description?: string;
|
||||
additional_images?: string[];
|
||||
video_url?: string;
|
||||
compare_at_price?: number;
|
||||
package_weight?: number;
|
||||
shipping_profile?: string;
|
||||
dimensions?: string;
|
||||
materials?: string;
|
||||
care_instructions?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
|
|
|
|||
27
src/utils/date.ts
Normal file
27
src/utils/date.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Utility functions for formatting dates in Indian Standard Time (IST)
|
||||
*/
|
||||
export const formatISTDate = (date?: string | Date | number): string => {
|
||||
if (!date) return '';
|
||||
const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date;
|
||||
return d.toLocaleDateString('en-IN', {
|
||||
timeZone: 'Asia/Kolkata',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
export const formatISTDateTime = (date?: string | Date | number): string => {
|
||||
if (!date) return '';
|
||||
const d = typeof date === 'string' || typeof date === 'number' ? new Date(date) : date;
|
||||
return d.toLocaleString('en-IN', {
|
||||
timeZone: 'Asia/Kolkata',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
};
|
||||
Loading…
Reference in a new issue