Merge pull request 'adding production setup' (#1) from beta into main
Reviewed-on: http://16.113.106.152:3000/vignesh/supplier_central_frontend/pulls/1
|
|
@ -8,6 +8,7 @@ on:
|
|||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -33,6 +34,7 @@ jobs:
|
|||
deploy-beta:
|
||||
needs: build-and-test
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Download Build Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
|
|
@ -40,10 +42,12 @@ jobs:
|
|||
name: dist
|
||||
path: dist
|
||||
|
||||
- name: Deploy to Beta via FTP
|
||||
env:
|
||||
FTP_USERNAME: ${{ secrets.BETA_FTP_USERNAME }}
|
||||
FTP_PASSWORD: ${{ secrets.BETA_FTP_PASSWORD }}
|
||||
FTP_HOST: ${{ secrets.FTP_HOST }}
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
lftp -d -u "$FTP_USERNAME","$FTP_PASSWORD" -e "set ssl:verify-certificate no; set ftp:ssl-allow yes; set ftp:ssl-force true; set ftp:ssl-protect-data true; set ftp:passive-mode true; mirror -R dist/ ./; quit" $FTP_HOST
|
||||
apt-get update && apt-get install -y awscli
|
||||
|
||||
- name: Deploy to S3
|
||||
run: aws s3 sync dist/ s3://${{ secrets.AWS_S3_BUCKET_BETA }} --delete
|
||||
|
||||
- name: Invalidate CloudFront
|
||||
run: aws cloudfront create-invalidation --distribution-id E2AYEICRZKJ9TM --paths "/*"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
name: Production CI/CD Pipeline
|
||||
name: Beta CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -8,6 +8,7 @@ on:
|
|||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
|
@ -30,11 +31,10 @@ jobs:
|
|||
name: dist
|
||||
path: dist/
|
||||
|
||||
deploy-prod:
|
||||
deploy-beta:
|
||||
needs: build-and-test
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: production
|
||||
container: node:22
|
||||
steps:
|
||||
- name: Download Build Artifact
|
||||
uses: actions/download-artifact@v3
|
||||
|
|
@ -42,10 +42,12 @@ jobs:
|
|||
name: dist
|
||||
path: dist
|
||||
|
||||
- name: Deploy to Production via FTP
|
||||
env:
|
||||
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
|
||||
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
|
||||
FTP_HOST: ${{ secrets.FTP_HOST }}
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
lftp -d -u "$FTP_USERNAME","$FTP_PASSWORD" -e "set ssl:verify-certificate no; set ftp:ssl-allow yes; set ftp:ssl-force true; set ftp:ssl-protect-data true; set ftp:passive-mode true; mirror -R dist/ ./; quit" $FTP_HOST
|
||||
apt-get update && apt-get install -y awscli
|
||||
|
||||
- name: Deploy to S3
|
||||
run: aws s3 sync dist/ s3://partnerproductionsite --delete
|
||||
|
||||
- name: Invalidate CloudFront
|
||||
run: aws cloudfront create-invalidation --distribution-id EBBGP8E0MLQNJ --paths "/*"
|
||||
|
|
|
|||
13
.gitignore
vendored
|
|
@ -1,11 +1,14 @@
|
|||
# Logs
|
||||
logs
|
||||
html
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
test-results
|
||||
playwright-report
|
||||
|
||||
node_modules
|
||||
dist
|
||||
|
|
@ -22,3 +25,13 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
.env/
|
||||
*.env
|
||||
|
||||
# CDK
|
||||
cdk.out/
|
||||
|
|
|
|||
BIN
HLD_optimized.docx
Normal file
BIN
HLD_optimized.pptx
Normal file
BIN
business_overview.pptx
Normal file
6
cdk.context.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"hosted-zone:account=764709663363:domainName=tipro.in:region=ap-south-2": {
|
||||
"Id": "/hostedzone/Z04761903HH8T88QWE7JX",
|
||||
"Name": "tipro.in."
|
||||
}
|
||||
}
|
||||
68
cdk.json
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"app": "npx tsx cdk/app.ts",
|
||||
"watch": {
|
||||
"include": [
|
||||
"cdk/**"
|
||||
],
|
||||
"exclude": [
|
||||
"README.md",
|
||||
"cdk*.json",
|
||||
"**/*.d.ts",
|
||||
"**/*.js",
|
||||
"tsconfig.json",
|
||||
"package*.json",
|
||||
"yarn.lock",
|
||||
"node_modules",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"context": {
|
||||
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
||||
"@aws-cdk/core:checkSecretUsage": true,
|
||||
"@aws-cdk/core:target-partitions": [
|
||||
"aws",
|
||||
"aws-cn"
|
||||
],
|
||||
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
|
||||
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
|
||||
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
|
||||
"@aws-cdk/aws-iam:minimizePolicies": true,
|
||||
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
|
||||
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasReadAccess": true,
|
||||
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
|
||||
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
|
||||
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
|
||||
"@aws-cdk/core:enablePartitionLiterals": true,
|
||||
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
|
||||
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
|
||||
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
|
||||
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
|
||||
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
|
||||
"@aws-cdk/aws-route53-patters:useCertificate": true,
|
||||
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
|
||||
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
|
||||
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
|
||||
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
|
||||
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
|
||||
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachment": true,
|
||||
"@aws-cdk/aws-redshift:columnId": true,
|
||||
"@aws-cdk/aws-stepfunctions-tasks:invokeLambdaOutputIncludesPayload": true,
|
||||
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
|
||||
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
|
||||
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
|
||||
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
|
||||
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
|
||||
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
|
||||
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
|
||||
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
|
||||
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
|
||||
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
|
||||
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
|
||||
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
|
||||
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
|
||||
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
|
||||
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
|
||||
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
|
||||
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true
|
||||
}
|
||||
}
|
||||
21
cdk/app.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env node
|
||||
import 'source-map-support/register.js';
|
||||
import * as cdk from 'aws-cdk-lib';
|
||||
import { BetaSupplierSiteStack } from './betasupplier-site-stack.js';
|
||||
|
||||
const app = new cdk.App();
|
||||
new BetaSupplierSiteStack(app, 'BetaSupplierSiteStack', {
|
||||
/* If you don't specify 'env', this stack will be environment-agnostic.
|
||||
* Account/Region-dependent features and context lookups will not work,
|
||||
* but a single synthesized template can be deployed anywhere. */
|
||||
|
||||
/* Uncomment the next line to specialize this stack for the AWS Account
|
||||
* and Region that are implied by the current CLI configuration. */
|
||||
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
|
||||
|
||||
/* Uncomment the next line if you know exactly what Account and Region you
|
||||
* want to deploy the stack to. */
|
||||
// env: { account: '123456789012', region: 'us-east-1' },
|
||||
|
||||
/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
|
||||
});
|
||||
105
cdk/betasupplier-site-stack.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import { Construct } from 'constructs';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
||||
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
||||
import * as route53 from 'aws-cdk-lib/aws-route53';
|
||||
import * as targets from 'aws-cdk-lib/aws-route53-targets';
|
||||
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
|
||||
|
||||
export class BetaSupplierSiteStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// 1. Create S3 Bucket for Website hosting
|
||||
const websiteBucket = new s3.Bucket(this, 'BetaSupplierSiteBucket', {
|
||||
bucketName: `betasuppliersite-${this.account}-${this.region}`,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
autoDeleteObjects: true,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // Secure by default, OAI will be used
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
});
|
||||
|
||||
const ec2Origin = new origins.HttpOrigin('api.tipro.in', {
|
||||
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
|
||||
httpPort: 8080,
|
||||
});
|
||||
|
||||
// Look up the tipro.in hosted zone
|
||||
const zone = route53.HostedZone.fromLookup(this, 'TiproZone', {
|
||||
domainName: 'tipro.in',
|
||||
});
|
||||
|
||||
// Create Route53 A Record for the EC2 API server
|
||||
// This allows us to use an IP address indirectly, by mapping api.tipro.in to it.
|
||||
new route53.ARecord(this, 'ApiRecord', {
|
||||
recordName: 'api.tipro.in',
|
||||
target: route53.RecordTarget.fromIpAddresses('16.113.57.127'),
|
||||
zone
|
||||
});
|
||||
|
||||
// Create a certificate in us-east-1 for CloudFront
|
||||
const certificate = new acm.DnsValidatedCertificate(this, 'SiteCertificate', {
|
||||
domainName: 'partners.tipro.in',
|
||||
hostedZone: zone,
|
||||
region: 'us-east-1', // CloudFront requires certificates to be in us-east-1
|
||||
});
|
||||
|
||||
// 2. Create CloudFront Distribution
|
||||
// origins.S3BucketOrigin.withOriginAccessControl will automatically create an Origin Access Control (OAC)
|
||||
// and update the S3 bucket policy to allow access only from this CloudFront distribution.
|
||||
const distribution = new cloudfront.Distribution(this, 'BetaSupplierSiteDistribution', {
|
||||
domainNames: ['partners.tipro.in'],
|
||||
certificate: certificate,
|
||||
defaultRootObject: 'index.html',
|
||||
defaultBehavior: {
|
||||
origin: origins.S3BucketOrigin.withOriginAccessControl(websiteBucket),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
||||
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
||||
},
|
||||
additionalBehaviors: {
|
||||
'/api/*': {
|
||||
origin: ec2Origin,
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
||||
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
|
||||
},
|
||||
},
|
||||
errorResponses: [
|
||||
{
|
||||
httpStatus: 404,
|
||||
responseHttpStatus: 200, // Return 200 for SPAs
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
},
|
||||
{
|
||||
httpStatus: 403,
|
||||
responseHttpStatus: 200, // Return 200 for SPAs
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'BetaSupplierSiteBucketName', {
|
||||
value: websiteBucket.bucketName,
|
||||
description: 'The name of the S3 bucket for the betasupplier site',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'BetaSupplierSiteCloudFrontDomainName', {
|
||||
value: distribution.distributionDomainName,
|
||||
description: 'The domain name of the CloudFront distribution for betasupplier site',
|
||||
});
|
||||
|
||||
// 3. Create Route53 A Record to point to the CloudFront Distribution
|
||||
new route53.ARecord(this, 'SiteAliasRecord', {
|
||||
recordName: 'partners.tipro.in',
|
||||
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
|
||||
zone
|
||||
});
|
||||
}
|
||||
}
|
||||
59
cdk/betasupplier-site-stack.ts.orig
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import * as cdk from 'aws-cdk-lib';
|
||||
import { Construct } from 'constructs';
|
||||
import * as s3 from 'aws-cdk-lib/aws-s3';
|
||||
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
|
||||
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
|
||||
|
||||
export class BetaSupplierSiteStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
// 1. Create S3 Bucket for Website hosting
|
||||
const websiteBucket = new s3.Bucket(this, 'BetaSupplierSiteBucket', {
|
||||
bucketName: `betasuppliersite-${this.account}-${this.region}`,
|
||||
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||
autoDeleteObjects: true,
|
||||
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, // Secure by default, OAI will be used
|
||||
encryption: s3.BucketEncryption.S3_MANAGED,
|
||||
});
|
||||
|
||||
// 2. Create CloudFront Distribution
|
||||
// origins.S3Origin will automatically create an Origin Access Identity (OAI)
|
||||
// and update the S3 bucket policy to allow access only from this CloudFront distribution.
|
||||
const distribution = new cloudfront.Distribution(this, 'BetaSupplierSiteDistribution', {
|
||||
defaultRootObject: 'index.html',
|
||||
defaultBehavior: {
|
||||
origin: new origins.S3Origin(websiteBucket),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
||||
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
||||
},
|
||||
errorResponses: [
|
||||
{
|
||||
httpStatus: 404,
|
||||
responseHttpStatus: 200, // Return 200 for SPAs
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
},
|
||||
{
|
||||
httpStatus: 403,
|
||||
responseHttpStatus: 200, // Return 200 for SPAs
|
||||
responsePagePath: '/index.html',
|
||||
ttl: cdk.Duration.seconds(0),
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Outputs
|
||||
new cdk.CfnOutput(this, 'BetaSupplierSiteBucketName', {
|
||||
value: websiteBucket.bucketName,
|
||||
description: 'The name of the S3 bucket for the betasupplier site',
|
||||
});
|
||||
|
||||
new cdk.CfnOutput(this, 'BetaSupplierSiteCloudFrontDomainName', {
|
||||
value: distribution.distributionDomainName,
|
||||
description: 'The domain name of the CloudFront distribution for betasupplier site',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +1,60 @@
|
|||
describe('Seller Central Django Backend API E2E Tests', () => {
|
||||
const backendUrl = 'http://localhost:8000/api';
|
||||
const backendUrl = 'http://16.113.57.127:8080/api';
|
||||
|
||||
it('verifies product list', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `${backendUrl}/products/`
|
||||
}).then((response) => {
|
||||
cy.request(`${backendUrl}/products/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.be.an('array');
|
||||
});
|
||||
});
|
||||
|
||||
it('creates and deletes a product', () => {
|
||||
const testSku = `CY-SKU-${Date.now()}`;
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${backendUrl}/products/`,
|
||||
body: {
|
||||
title: 'Cypress Test Pot',
|
||||
category: 'Home Decor',
|
||||
price: '45.00',
|
||||
stock: 5,
|
||||
sku: testSku
|
||||
}
|
||||
const uniqueSku = `SKU-CY-${Date.now()}`;
|
||||
cy.request('POST', `${backendUrl}/products/`, {
|
||||
title: 'Cypress Test Silk Scarf',
|
||||
category: 'Apparel',
|
||||
price: '55.00',
|
||||
stock: 20,
|
||||
sku: uniqueSku
|
||||
}).then((response) => {
|
||||
expect(response.status).to.eq(201);
|
||||
expect(response.body.sku).to.eq(testSku);
|
||||
const productId = response.body.id;
|
||||
expect(response.body.sku).to.eq(uniqueSku);
|
||||
const prodId = response.body.id;
|
||||
|
||||
// Delete the product
|
||||
cy.request({
|
||||
method: 'DELETE',
|
||||
url: `${backendUrl}/products/${productId}/`
|
||||
}).then((delResponse) => {
|
||||
expect(delResponse.status).to.eq(204);
|
||||
// Delete the created product
|
||||
cy.request('DELETE', `${backendUrl}/products/${prodId}/`).then((delRes) => {
|
||||
expect(delRes.status).to.eq(204); // django rest framework delete returns 204 No Content
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies order list and acceptance', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `${backendUrl}/orders/`
|
||||
}).then((response) => {
|
||||
cy.request(`${backendUrl}/orders/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.be.an('array');
|
||||
const order = response.body.find((o: any) => o.status === 'Pending Acceptance');
|
||||
if (order) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${backendUrl}/orders/${order.id}/accept/`
|
||||
}).then((acceptResponse) => {
|
||||
expect(acceptResponse.status).to.eq(200);
|
||||
expect(acceptResponse.body.status).to.eq('Ready to Ship');
|
||||
expect(response.body.length).to.be.greaterThan(0);
|
||||
|
||||
const pendingOrder = response.body.find((o: any) => o.status === 'Pending Acceptance');
|
||||
if (pendingOrder) {
|
||||
cy.request('POST', `${backendUrl}/orders/${pendingOrder.id}/accept/`).then((acceptRes) => {
|
||||
expect(acceptRes.status).to.eq(200);
|
||||
expect(acceptRes.body.status).to.eq('Ready to Ship');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies wallet summary and payout withdrawal', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `${backendUrl}/wallet/`
|
||||
}).then((response) => {
|
||||
cy.request(`${backendUrl}/wallet/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
const initialOutstanding = parseFloat(response.body.outstanding);
|
||||
if (initialOutstanding > 10) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${backendUrl}/wallet/withdraw/`,
|
||||
body: { amount: '10.00' }
|
||||
}).then((withdrawResponse) => {
|
||||
expect(withdrawResponse.status).to.eq(200);
|
||||
expect(parseFloat(withdrawResponse.body.outstanding)).to.eq(initialOutstanding - 10);
|
||||
expect(response.body).to.have.property('outstanding');
|
||||
expect(response.body).to.have.property('withdrawn');
|
||||
|
||||
const currentOutstanding = parseFloat(response.body.outstanding);
|
||||
if (currentOutstanding > 10) {
|
||||
cy.request('POST', `${backendUrl}/wallet/withdraw/`, { amount: '10.00' }).then((withdrawRes) => {
|
||||
expect(withdrawRes.status).to.eq(200);
|
||||
expect(parseFloat(withdrawRes.body.outstanding)).to.eq(currentOutstanding - 10);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
|
||||
// 2. Registration Page
|
||||
cy.contains('Get Started Today').click();
|
||||
cy.get('input#reg-email').type('test_seller@example.com');
|
||||
const randUser = `test_seller_${Date.now()}@example.com`;
|
||||
cy.get('input#reg-email').type(randUser);
|
||||
cy.get('input#reg-phone').type('9876543210');
|
||||
cy.get('input#reg-pass').type('super_secure_pass_123');
|
||||
cy.get('input#reg-confirm-pass').type('super_secure_pass_123');
|
||||
cy.get('input#reg-policy').check();
|
||||
cy.get('button').contains('Register Business').click();
|
||||
|
||||
// 3. Step 1: Contact Verification
|
||||
|
|
@ -27,8 +30,8 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
// 4. Step 2: Tax & Identity Verification
|
||||
cy.contains('Step 2 of 3: Tax & Identity Verification').should('be.visible');
|
||||
cy.get('input#verify-gst').clear().type('29AAAAA1111A1Z1');
|
||||
cy.contains('Verify GSTIN').click();
|
||||
cy.contains('GSTIN successfully verified with government registry.').should('be.visible');
|
||||
cy.contains('Submit GSTIN').click();
|
||||
cy.contains('verification will be completed within next 24 hrs').should('be.visible');
|
||||
|
||||
// Upload mock Aadhaar & PAN files
|
||||
cy.get('input#aadhar-upload').selectFile({
|
||||
|
|
@ -63,8 +66,8 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('Submit Supplier Profile').click();
|
||||
|
||||
// 6. Setup Welcome Tour Stepper
|
||||
cy.contains('Welcome to Global Artisans Hub! 🎉').should('be.visible');
|
||||
cy.contains('Start Tour').click();
|
||||
cy.contains('Supplier Account Under Review ⏳').should('be.visible');
|
||||
cy.contains('Start Guided Tour 🎬').click();
|
||||
cy.contains('📦 Products & Inventory Management').should('be.visible');
|
||||
cy.contains('Next: Order Management').click();
|
||||
cy.contains('🚚 Order Acceptance Control').should('be.visible');
|
||||
|
|
@ -72,7 +75,7 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('🏷️ Barcode Identification & Tracking').should('be.visible');
|
||||
cy.contains('Next: Earnings & Wallet').click();
|
||||
cy.contains('💼 Wallet & Payout Withdrawals').should('be.visible');
|
||||
cy.contains('Launch Dashboard 🚀').click();
|
||||
cy.contains('Explore Dashboard 🚀').click();
|
||||
|
||||
// 7. Check Active Dashboard Tab
|
||||
cy.contains('Performance Analytics').should('be.visible');
|
||||
|
|
@ -98,4 +101,54 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('Print Label').should('be.visible');
|
||||
cy.contains('Download SVG').should('be.visible');
|
||||
});
|
||||
|
||||
it('enforces security constraints and validation rules (negative test cases)', () => {
|
||||
// 1. Visit Home and verify dashboard components are not in DOM (route guarding)
|
||||
cy.visit('/');
|
||||
cy.get('.dashboard-container').should('not.exist');
|
||||
cy.get('.sidebar-menu').should('not.exist');
|
||||
|
||||
// 2. Go to Registration
|
||||
cy.contains('Get Started Today').click();
|
||||
const randSecurityUser = `security_test_${Date.now()}@example.com`;
|
||||
cy.get('input#reg-email').type(randSecurityUser);
|
||||
cy.get('input#reg-phone').type('9000000000');
|
||||
cy.get('input#reg-pass').type('password123');
|
||||
cy.get('input#reg-confirm-pass').type('password123');
|
||||
cy.get('input#reg-policy').check();
|
||||
cy.get('button').contains('Register Business').click();
|
||||
|
||||
// 3. Step 1: Negative OTP validations
|
||||
cy.contains('Step 1 of 3: Contact Verification').should('be.visible');
|
||||
|
||||
// Proceed button must be disabled initially
|
||||
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
|
||||
|
||||
// Submit invalid WhatsApp OTP
|
||||
cy.contains('Send WhatsApp OTP').click();
|
||||
cy.get('input[placeholder="Enter 123456"]').first().type('000000');
|
||||
|
||||
// Capture alert for incorrect OTP
|
||||
const alertStub = cy.stub();
|
||||
cy.on('window:alert', alertStub);
|
||||
|
||||
cy.contains('Verify Code').first().click().then(() => {
|
||||
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
|
||||
});
|
||||
cy.contains('✓ Mobile & WhatsApp Verified').should('not.exist');
|
||||
|
||||
// Correct the code to enable proceed
|
||||
cy.get('input[placeholder="Enter 123456"]').first().clear().type('123456');
|
||||
cy.contains('Verify Code').first().click();
|
||||
cy.contains('✓ Mobile & WhatsApp Verified').should('be.visible');
|
||||
|
||||
// Submit invalid Email OTP
|
||||
cy.contains('Send Email OTP').click();
|
||||
cy.get('input[placeholder="Enter 123456"]').last().type('999999');
|
||||
cy.contains('Verify Code').last().click().then(() => {
|
||||
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
|
||||
});
|
||||
cy.contains('✓ Email Verified').should('not.exist');
|
||||
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
5
fix.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
with open('/home/vignesh/github/Tiproemail/aws_cdk/lib/betasupplier-site-stack.ts', 'r') as f:
|
||||
content = f.read()
|
||||
content = content.replace(' }\n errorResponses: [', ' },\n errorResponses: [')
|
||||
with open('/home/vignesh/github/Tiproemail/aws_cdk/lib/betasupplier-site-stack.ts', 'w') as f:
|
||||
f.write(content)
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>sellercentral</title>
|
||||
</head>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
|
|
|||
2012
package-lock.json
generated
15
package.json
|
|
@ -10,13 +10,18 @@
|
|||
"test": "vitest run",
|
||||
"preview": "vite preview",
|
||||
"cypress:run": "cypress run --headed",
|
||||
"cypress:open": "cypress open"
|
||||
"cypress:open": "cypress open",
|
||||
"test:playwright": "playwright test --project=chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"bulma": "^1.0.4",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
"react-dom": "^19.2.8",
|
||||
"recharts": "^3.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
|
|
@ -24,9 +29,15 @@
|
|||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"aws-cdk": "^2.1140.0",
|
||||
"aws-cdk-lib": "^2.268.0",
|
||||
"constructs": "^10.8.1",
|
||||
"cypress": "^15.20.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"oxlint": "^1.75.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tsx": "^4.23.13",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.2.0",
|
||||
"vitest": "^4.1.10"
|
||||
|
|
|
|||
30
patch.diff
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
--- /home/vignesh/github/Tiproemail/aws_cdk/lib/betasupplier-site-stack.ts
|
||||
+++ /home/vignesh/github/Tiproemail/aws_cdk/lib/betasupplier-site-stack.ts
|
||||
@@ -19,13 +19,25 @@
|
||||
|
||||
+ const ec2Origin = new origins.HttpOrigin('ec2-16-113-57-127.ap-south-2.compute.amazonaws.com', {
|
||||
+ protocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
|
||||
+ });
|
||||
+
|
||||
// 2. Create CloudFront Distribution
|
||||
// origins.S3Origin will automatically create an Origin Access Identity (OAI)
|
||||
// and update the S3 bucket policy to allow access only from this CloudFront distribution.
|
||||
const distribution = new cloudfront.Distribution(this, 'BetaSupplierSiteDistribution', {
|
||||
defaultRootObject: 'index.html',
|
||||
defaultBehavior: {
|
||||
origin: new origins.S3Origin(websiteBucket),
|
||||
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
|
||||
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
|
||||
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
|
||||
},
|
||||
+ additionalBehaviors: {
|
||||
+ '/api/*': {
|
||||
+ origin: ec2Origin,
|
||||
+ viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
||||
+ allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
|
||||
+ cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
|
||||
+ originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER,
|
||||
+ }
|
||||
+ },
|
||||
errorResponses: [
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 110 KiB |
49
playwright-report/index.html
Normal file
32
playwright-report/trace/assets/codeMirrorModule-rXmQmLUY.js
Normal file
181
playwright-report/trace/assets/defaultSettingsView-B-dXF5JN.js
Normal file
1
playwright-report/trace/assets/urlMatch-L3liM589.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e,t=`'`){let n=JSON.stringify(e),r=n.substring(1,n.length-1).replace(/\\"/g,`"`);if(t===`'`)return t+r.replace(/[']/g,`\\'`)+t;if(t===`"`)return t+r.replace(/["]/g,`\\"`)+t;if(t==="`")return t+r.replace(/[`]/g,"\\`")+t;throw Error(`Invalid escape char`)}function t(e){return e.charAt(0).toUpperCase()+e.substring(1)}function n(e){return e.replace(/([a-z0-9])([A-Z])/g,`$1_$2`).replace(/([A-Z])([A-Z][a-z])/g,`$1_$2`).toLowerCase()}function r(e){return`"${e.replace(/["\\]/g,e=>`\\`+e)}"`}var i;function a(){i=new Map}function o(e){let t=i?.get(e);return t===void 0&&(t=e.replace(/[\u200b\u00ad]/g,``).trim().replace(/\s+/g,` `),i?.set(e,t)),t}function s(e){return e.replace(/(^|[^\\])(\\\\)*\\(['"`])/g,`$1$2$3`)}function c(e){return e.unicode||e.unicodeSets?String(e):String(e).replace(/(^|[^\\])(\\\\)*(["'`])/g,`$1$2\\$3`).replace(/>>/g,`\\>\\>`)}function l(e,t){return typeof e==`string`?`${JSON.stringify(e)}${t?`s`:`i`}`:c(e)}function u(e,t){return typeof e==`string`?`"${e.replace(/\\/g,`\\\\`).replace(/["]/g,`\\"`)}"${t?`s`:`i`}`:c(e)}function d(e,t,n=``){if(e.length<=t)return e;let r=[...e];return r.length>t?r.slice(0,t-n.length).join(``)+n:r.join(``)}function f(e,t){return d(e,t,`…`)}function p(e){if(!e.startsWith(`data:`))return e;let t=e.indexOf(`,`);return t===-1?e:e.slice(0,t+1)+`…`}function m(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function h(e,t){let n=e.length,r=t.length,i=0,a=0,o=Array(n+1).fill(null).map(()=>Array(r+1).fill(0));for(let s=1;s<=n;s++)for(let n=1;n<=r;n++)e[s-1]===t[n-1]&&(o[s][n]=o[s-1][n-1]+1,o[s][n]>i&&(i=o[s][n],a=s));return e.slice(a-i,a)}function g(e,t){try{return[`http:`,`https:`].includes(new URL(e,t).protocol)}catch{return!1}}export{m as a,s as c,n as d,t as f,l as i,o as l,p as m,a as n,e as o,f as p,u as r,h as s,g as t,r as u};
|
||||
1
playwright-report/trace/codeMirrorModule.-QdMvsKi.css
Normal file
BIN
playwright-report/trace/codicon.DCmgc-ay.ttf
Normal file
1
playwright-report/trace/defaultSettingsView.BLFoOugd.css
Normal file
1
playwright-report/trace/index.B_TqY17P.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.drop-target{background-color:var(--vscode-editor-background);z-index:100;flex-direction:column;flex:auto;justify-content:center;align-items:center;line-height:24px;display:flex;position:absolute;inset:0}body .drop-target{background:#fffc}:root.dark-mode .drop-target{background:#000c}.drop-target .title{margin-bottom:30px;font-size:24px;font-weight:700}.drop-target .info{text-align:center;max-width:400px}.drop-target .processing-error{color:#e74c3c;text-align:center;white-space:pre-line;margin:30px;font-size:24px;font-weight:700}.drop-target input{margin-top:50px}.drop-target button{color:#fff;cursor:pointer;background-color:#007acc;border:none;margin:30px 0;padding:8px 12px}.drop-target .version{color:var(--vscode-disabledForeground);margin-top:8px}.progress-dialog{background-color:var(--vscode-sideBar-background);border:none;outline:none;width:400px;inset:0}.progress-dialog::backdrop{background-color:#0006}.progress-content{padding:16px}.progress-content .title{background-color:unset;padding:0;font-size:18px;font-weight:700}.progress-wrapper{background-color:var(--vscode-commandCenter-activeBackground);width:100%;margin-top:16px;margin-bottom:8px}.inner-progress{background-color:var(--vscode-progressBar-background);height:4px}.workbench-loader-header{background-color:var(--vscode-sideBar-background);box-shadow:var(--vscode-scrollbar-shadow) 0 6px 6px -6px;flex:0 0 48px;align-items:center;font-size:16px;line-height:48px;display:flex}.workbench-loader{contain:size}.workbench-loader .workbench-loader-header{flex-basis:32px;font-size:13px;line-height:32px}.workbench-loader .workbench-loader-header .toolbar-button{margin:4px}.workbench-loader .logo{align-items:center;margin-left:16px;display:flex}.workbench-loader .logo img{pointer-events:none;flex:none;width:32px;height:32px}.workbench-loader .product{flex:none;margin-left:16px;font-weight:600}.workbench-loader .workbench-loader-header .title{text-overflow:ellipsis;text-wrap:nowrap;margin-left:16px;overflow:hidden}html,body{min-width:550px;min-height:450px;overflow:auto}
|
||||
1
playwright-report/trace/index.KZ4wOW1K.js
Normal file
44
playwright-report/trace/index.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
|
||||
<link rel="manifest" href="./manifest.webmanifest">
|
||||
<title>Playwright Trace Viewer</title>
|
||||
<script type="module" crossorigin src="./index.KZ4wOW1K.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-B-dXF5JN.js">
|
||||
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BLFoOugd.css">
|
||||
<link rel="stylesheet" crossorigin href="./index.B_TqY17P.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<dialog id="fallback-error">
|
||||
<p>The Playwright Trace Viewer must be loaded over the <code>http://</code> or <code>https://</code> protocols.</p>
|
||||
<p>For more information, please see the <a href="https://aka.ms/playwright/trace-viewer-file-protocol">docs</a>.</p>
|
||||
</dialog>
|
||||
<script>
|
||||
if (!/^https?:/.test(window.location.protocol)) {
|
||||
const fallbackErrorDialog = document.getElementById('fallback-error');
|
||||
const isTraceViewerInsidePlaywrightReport = window.location.protocol === 'file:' && window.location.pathname.endsWith('/trace/index.html');
|
||||
// Best-effort to show the report path in the dialog.
|
||||
if (isTraceViewerInsidePlaywrightReport) {
|
||||
const reportPath = (() => {
|
||||
const base = decodeURIComponent(window.location.pathname).replace(/\/trace\/index\.html$/, '');
|
||||
if (navigator.platform === 'Win32')
|
||||
return base.replace(/^\//, '').replace(/\//g, '\\\\');
|
||||
return base;
|
||||
})();
|
||||
const reportLink = document.createElement('div');
|
||||
const command = `npx playwright show-report "${reportPath}"`;
|
||||
reportLink.innerHTML = `You can open the report via <code>${command}</code> from your Playwright project. <button type="button">Copy Command</button>`;
|
||||
fallbackErrorDialog.insertBefore(reportLink, fallbackErrorDialog.children[1]);
|
||||
reportLink.querySelector('button').addEventListener('click', () => navigator.clipboard.writeText(command));
|
||||
}
|
||||
fallbackErrorDialog.show();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
16
playwright-report/trace/manifest.webmanifest
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"theme_color": "#000",
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
"start_url": "index.html",
|
||||
"name": "Playwright Trace Viewer",
|
||||
"short_name": "Trace Viewer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "playwright-logo.svg",
|
||||
"sizes": "48x48 72x72 96x96 128x128 150x150 256x256 512x512 1024x1024",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
playwright-report/trace/playwright-logo.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
|
||||
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
|
||||
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
|
||||
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
|
||||
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
|
||||
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
|
||||
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
1
playwright-report/trace/snapshot.B_Jk1wbt.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{t as e}from"./assets/urlMatch-L3liM589.js";(async()=>{if(!navigator.serviceWorker)throw Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);navigator.serviceWorker.register(`sw.bundle.js`),navigator.serviceWorker.controller||await new Promise(e=>navigator.serviceWorker.oncontrollerchange=e);let t=new URL(location.href).searchParams.get(`trace`),n=new URLSearchParams;t&&n.set(`trace`,t),await fetch(`contexts?`+n.toString());let r=new URLSearchParams(location.search).get(`r`);if(!r||!e(r,location.href))return;let i=document.querySelector(`iframe`);i&&(i.src=r)})();
|
||||
10
playwright-report/trace/snapshot.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<script type="module" crossorigin src="./snapshot.B_Jk1wbt.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
|
||||
<body>
|
||||
<iframe src="about:blank" sandbox="allow-same-origin allow-scripts" style="position:absolute;top:0;left:0;right:0;bottom:0;width:100%;height:100%;border:none;"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
4
playwright-report/trace/sw.bundle.js
Normal file
1
playwright-report/trace/uiMode.C7UW1sC9.css
Normal file
5
playwright-report/trace/uiMode.Dzuouizj.js
Normal file
18
playwright-report/trace/uiMode.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Test</title>
|
||||
<script type="module" crossorigin src="./uiMode.Dzuouizj.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/urlMatch-L3liM589.js">
|
||||
<link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-B-dXF5JN.js">
|
||||
<link rel="stylesheet" crossorigin href="./defaultSettingsView.BLFoOugd.css">
|
||||
<link rel="stylesheet" crossorigin href="./uiMode.C7UW1sC9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
playwright-report/trace/xtermModule.kHJ-D0s7.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}
|
||||
30
playwright.config.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './playwright/e2e',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'https://betasuppliers.tipro.in/',
|
||||
trace: 'on',
|
||||
screenshot: 'on',
|
||||
video: 'on',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
178
playwright/e2e/onboarding.spec.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
||||
test('completes the registration, contact verification, document upload, location pinning, welcome tour, order acceptance, and barcode generation', async ({ page }) => {
|
||||
// 1. Visit Landing Page
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Sell Globally.')).toBeVisible();
|
||||
|
||||
// 2. Registration Page
|
||||
await page.locator('button, a').filter({ hasText: 'Get Started Today' }).first().click();
|
||||
const randUser = `test_seller_${Date.now()}_${Math.floor(Math.random() * 1000000)}@example.com`;
|
||||
const randPhone = `9${Math.floor(100000000 + Math.random() * 900000000)}`;
|
||||
await page.locator('input#reg-email').fill(randUser);
|
||||
await page.locator('input#reg-phone').fill(randPhone);
|
||||
await page.locator('input#reg-pass').fill('Super_secure_pass_123!');
|
||||
await page.locator('input#reg-confirm-pass').fill('Super_secure_pass_123!');
|
||||
await page.locator('input#reg-policy').check();
|
||||
await page.locator('button').filter({ hasText: 'Register Business' }).click();
|
||||
|
||||
// 3. Step 1: Contact Verification
|
||||
await expect(page.getByText('Step 1 of 3: Contact Verification')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Send WhatsApp OTP' }).click();
|
||||
await page.locator('input[placeholder="Enter 123456"]').first().fill('123456');
|
||||
await page.locator('button').filter({ hasText: 'Verify Code' }).first().click();
|
||||
await expect(page.getByText('✓ Mobile & WhatsApp Verified')).toBeVisible();
|
||||
|
||||
await page.locator('button').filter({ hasText: 'Send Email OTP' }).click();
|
||||
await page.locator('input[placeholder="Enter 123456"]').last().fill('123456');
|
||||
await page.locator('button').filter({ hasText: 'Verify Code' }).last().click();
|
||||
await expect(page.getByText('✓ Email Verified')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Next Step: Identity Verification' }).click();
|
||||
|
||||
// 4. Step 2: Tax & Identity Verification
|
||||
await expect(page.getByText('Step 2 of 3: Tax & Identity Verification')).toBeVisible();
|
||||
await page.locator('input#verify-gst').clear();
|
||||
await page.locator('input#verify-gst').fill('29AAAAA1111A1Z1');
|
||||
await page.locator('button').filter({ hasText: 'Submit GSTIN' }).click();
|
||||
await expect(page.getByText('verification will be completed within next 24 hrs')).toBeVisible();
|
||||
|
||||
// Upload mock Aadhaar & PAN files
|
||||
await page.setInputFiles('input#aadhar-upload', {
|
||||
name: 'aadhar_card.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: Buffer.from('mock aadhaar pdf'),
|
||||
});
|
||||
await expect(page.getByText('Selected: aadhar_card.pdf')).toBeVisible();
|
||||
|
||||
await page.setInputFiles('input#pan-upload', {
|
||||
name: 'pan_card.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
buffer: Buffer.from('mock pan pdf'),
|
||||
});
|
||||
await expect(page.getByText('Selected: pan_card.pdf')).toBeVisible();
|
||||
|
||||
await page.locator('button').filter({ hasText: 'Next Step: Store & Location' }).click();
|
||||
|
||||
// 5. Step 3: Store & Pickup Location Setup
|
||||
await expect(page.getByText('Step 3 of 3: Store & Pickup Location')).toBeVisible();
|
||||
await page.locator('input#store-name').clear();
|
||||
await page.locator('input#store-name').fill('Teak Wood Craft Store');
|
||||
await page.locator('textarea#store-bio').clear();
|
||||
await page.locator('textarea#store-bio').fill('Teak wood hand-carved home decor elements.');
|
||||
|
||||
// Test interactive map coordinate pinning
|
||||
await expect(page.getByText('Store Location on Map *')).toBeVisible();
|
||||
const mapGridParent = page.locator('span').filter({ hasText: 'Click anywhere on the map grid to pin location' }).locator('..');
|
||||
await mapGridParent.click({ position: { x: 100, y: 80 } });
|
||||
await expect(page.getByText('Coordinates: Lat')).toBeVisible();
|
||||
|
||||
await expect(page.locator('input#location-text')).not.toHaveValue('');
|
||||
await page.locator('input#loc-city').clear();
|
||||
await page.locator('input#loc-city').fill('Bangalore');
|
||||
await page.locator('input#loc-pincode').clear();
|
||||
await page.locator('input#loc-pincode').fill('560001');
|
||||
await page.locator('button').filter({ hasText: 'Submit Supplier Profile' }).click();
|
||||
|
||||
// 6. Setup Welcome Tour Stepper
|
||||
await expect(page.getByText('Supplier Account Under Review ⏳')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Start Guided Tour 🎬' }).click();
|
||||
await expect(page.getByText('📦 Products & Inventory Management')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Next: Order Management' }).click();
|
||||
await expect(page.getByText('🚚 Order Acceptance Control')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Next: Barcode Generation' }).click();
|
||||
await expect(page.getByText('🏷️ Barcode Identification & Tracking')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Next: Earnings & Wallet' }).click();
|
||||
await expect(page.getByText('💼 Wallet & Payout Withdrawals')).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Explore Dashboard 🚀' }).click();
|
||||
|
||||
// 7. Check Active Dashboard Tab
|
||||
await expect(page.getByText('Performance Analytics')).toBeVisible();
|
||||
|
||||
// 8. Manage Products tab & Bulk template download
|
||||
await page.locator('button, a').filter({ hasText: 'Manage Products' }).click();
|
||||
await expect(page.getByText('Active Inventory')).toBeVisible();
|
||||
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.locator('button, a').filter({ hasText: 'Download Sample Excel Template' }).click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toBeDefined();
|
||||
|
||||
// 9. Orders Acceptance Flow
|
||||
await page.locator('button, a').filter({ hasText: 'Orders & Transit' }).click();
|
||||
await expect(page.getByText('Active Orders & Payout status')).toBeVisible();
|
||||
await expect(page.getByText('Pending Acceptance').first()).toBeVisible();
|
||||
await page.locator('button').filter({ hasText: 'Accept' }).first().click();
|
||||
await expect(page.getByText('Ready to Ship')).toBeVisible();
|
||||
|
||||
// 10. Customer Address Printer
|
||||
await page.locator('button, a').filter({ hasText: 'Customer Address Printer' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Customer Address Printer' })).toBeVisible();
|
||||
await page.locator('select#print-order').selectOption({ index: 1 });
|
||||
await expect(page.getByText('Shipping Label Preview')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Print Address Label' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('enforces security constraints and validation rules (negative test cases)', async ({ page }) => {
|
||||
// 1. Visit Home and verify dashboard components are not in DOM (route guarding)
|
||||
await page.goto('/');
|
||||
await expect(page.locator('.dashboard-container')).toBeHidden();
|
||||
await expect(page.locator('.sidebar-menu')).toBeHidden();
|
||||
|
||||
// 2. Go to Registration
|
||||
await page.locator('button, a').filter({ hasText: 'Get Started Today' }).first().click();
|
||||
const randSecurityUser = `security_test_${Date.now()}_${Math.floor(Math.random() * 1000000)}@example.com`;
|
||||
const randSecurityPhone = `9${Math.floor(100000000 + Math.random() * 900000000)}`;
|
||||
await page.locator('input#reg-email').fill(randSecurityUser);
|
||||
await page.locator('input#reg-phone').fill(randSecurityPhone);
|
||||
await page.locator('input#reg-pass').fill('Password123!');
|
||||
await page.locator('input#reg-confirm-pass').fill('Password123!');
|
||||
await page.locator('input#reg-policy').check();
|
||||
await page.locator('button').filter({ hasText: 'Register Business' }).click();
|
||||
|
||||
// 3. Step 1: Negative OTP validations
|
||||
await expect(page.getByText('Step 1 of 3: Contact Verification')).toBeVisible();
|
||||
|
||||
// Proceed button must be disabled initially
|
||||
await expect(page.locator('button').filter({ hasText: 'Next Step: Identity Verification' })).toBeDisabled();
|
||||
|
||||
// Submit invalid WhatsApp OTP
|
||||
await page.locator('button').filter({ hasText: 'Send WhatsApp OTP' }).click();
|
||||
await page.locator('input[placeholder="Enter 123456"]').first().fill('000000');
|
||||
|
||||
// Capture dialog
|
||||
let alertMessage = '';
|
||||
page.once('dialog', async dialog => {
|
||||
alertMessage = dialog.message();
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.locator('button').filter({ hasText: 'Verify Code' }).first().click();
|
||||
|
||||
// Give brief time/event loop ticks to ensure dialog handler fires
|
||||
await page.waitForTimeout(500);
|
||||
expect(alertMessage).toBe('Incorrect code. Enter 123456');
|
||||
await expect(page.getByText('✓ Mobile & WhatsApp Verified')).toBeHidden();
|
||||
|
||||
// Correct the code to enable proceed
|
||||
await page.locator('input[placeholder="Enter 123456"]').first().clear();
|
||||
await page.locator('input[placeholder="Enter 123456"]').first().fill('123456');
|
||||
await page.locator('button').filter({ hasText: 'Verify Code' }).first().click();
|
||||
await expect(page.getByText('✓ Mobile & WhatsApp Verified')).toBeVisible();
|
||||
|
||||
// Submit invalid Email OTP
|
||||
await page.locator('button').filter({ hasText: 'Send Email OTP' }).click();
|
||||
await page.locator('input[placeholder="Enter 123456"]').last().fill('999999');
|
||||
|
||||
page.once('dialog', async dialog => {
|
||||
alertMessage = dialog.message();
|
||||
await dialog.accept();
|
||||
});
|
||||
|
||||
await page.locator('button').filter({ hasText: 'Verify Code' }).last().click();
|
||||
await page.waitForTimeout(500);
|
||||
expect(alertMessage).toBe('Incorrect code. Enter 123456');
|
||||
await expect(page.getByText('✓ Email Verified')).toBeHidden();
|
||||
await expect(page.locator('button').filter({ hasText: 'Next Step: Identity Verification' })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
5
prompt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
django server code : /home/vignesh/github/seller_central_backend
|
||||
Don't push code without my command
|
||||
beta site : https://betasuppliers.tipro.in/
|
||||
in betasite only you need test with playwright
|
||||
Read this repo and understand this project , once you ready let me know i will provide you instruction
|
||||
2
public/.htaccess
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
RewriteEngine On
|
||||
RewriteRule ^api/(.*) http://16.113.57.127:8080/api/$1 [P,L]
|
||||
BIN
public/artisan_banner.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/artisan_weaving.jpg
Normal file
|
After Width: | Height: | Size: 982 KiB |
BIN
public/brass_figurine.jpg
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
public/kanchipuram_silk.jpg
Normal file
|
After Width: | Height: | Size: 871 KiB |
BIN
public/terracotta_vases.jpg
Normal file
|
After Width: | Height: | Size: 644 KiB |
BIN
public/wooden_carving.jpg
Normal file
|
After Width: | Height: | Size: 768 KiB |
302
seller_settings/code.html
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Tradhox - Settings</title>
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com" rel="preconnect"/>
|
||||
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Libre+Caslon+Text:ital,wght@0,400;0,700;1,400&family=Work+Sans:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<!-- Tailwind Config -->
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"on-error": "#ffffff",
|
||||
"on-tertiary-fixed": "#241a0e",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed-dim": "#c8c6c2",
|
||||
"background": "#fbf9f8",
|
||||
"surface-variant": "#e4e2e2",
|
||||
"surface-container": "#efeded",
|
||||
"on-tertiary-container": "#b19f8d",
|
||||
"on-surface-variant": "#554244",
|
||||
"primary-container": "#6b1a2c",
|
||||
"on-secondary-fixed-variant": "#474744",
|
||||
"surface-bright": "#fbf9f8",
|
||||
"surface-container-highest": "#e4e2e2",
|
||||
"tertiary-container": "#433628",
|
||||
"primary-fixed": "#ffd9dc",
|
||||
"on-primary-fixed-variant": "#7e2839",
|
||||
"on-background": "#1b1c1c",
|
||||
"outline": "#887274",
|
||||
"error-container": "#ffdad6",
|
||||
"tertiary-fixed": "#f4dfcb",
|
||||
"on-tertiary-fixed-variant": "#524436",
|
||||
"secondary": "#5f5e5b",
|
||||
"secondary-fixed": "#e5e2de",
|
||||
"surface-tint": "#9c4050",
|
||||
"on-secondary-fixed": "#1c1c1a",
|
||||
"surface": "#fbf9f8",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary": "#ffffff",
|
||||
"surface-container-low": "#f5f3f3",
|
||||
"inverse-primary": "#ffb2bb",
|
||||
"on-surface": "#1b1c1c",
|
||||
"inverse-on-surface": "#f2f0f0",
|
||||
"primary": "#4d0218",
|
||||
"tertiary-fixed-dim": "#d7c3b0",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-container": "#e2dfdb",
|
||||
"on-primary-fixed": "#400011",
|
||||
"on-secondary-container": "#636260",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-primary-container": "#ef8191",
|
||||
"inverse-surface": "#303030",
|
||||
"surface-container-high": "#eae8e7",
|
||||
"error": "#ba1a1a",
|
||||
"primary-fixed-dim": "#ffb2bb",
|
||||
"outline-variant": "#dac0c2",
|
||||
"surface-dim": "#dbd9d9",
|
||||
"tertiary": "#2c2114"
|
||||
},
|
||||
borderRadius: {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
spacing: {
|
||||
"gutter": "24px",
|
||||
"stack-sm": "8px",
|
||||
"container-max": "1120px",
|
||||
"stack-lg": "32px",
|
||||
"section-gap": "64px",
|
||||
"margin-edge": "32px",
|
||||
"stack-md": "16px"
|
||||
},
|
||||
fontFamily: {
|
||||
"display-lg": ["Libre Caslon Text"],
|
||||
"headline-lg": ["Libre Caslon Text"],
|
||||
"headline-lg-mobile": ["Libre Caslon Text"],
|
||||
"body-md": ["Work Sans"],
|
||||
"label-caps": ["Work Sans"],
|
||||
"body-lg": ["Work Sans"],
|
||||
"button": ["Work Sans"],
|
||||
"headline-md": ["Libre Caslon Text"]
|
||||
},
|
||||
fontSize: {
|
||||
"display-lg": ["40px", { lineHeight: "48px", letterSpacing: "-0.02em", fontWeight: "700" }],
|
||||
"headline-lg": ["32px", { lineHeight: "40px", fontWeight: "700" }],
|
||||
"headline-lg-mobile": ["26px", { lineHeight: "32px", fontWeight: "700" }],
|
||||
"body-md": ["16px", { lineHeight: "24px", fontWeight: "400" }],
|
||||
"label-caps": ["12px", { lineHeight: "16px", letterSpacing: "0.1em", fontWeight: "600" }],
|
||||
"body-lg": ["18px", { lineHeight: "28px", fontWeight: "400" }],
|
||||
"button": ["14px", { lineHeight: "20px", fontWeight: "600" }],
|
||||
"headline-md": ["24px", { lineHeight: "32px", fontWeight: "600" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.icon-fill {
|
||||
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-md antialiased overflow-x-hidden">
|
||||
<!-- SideNavBar (Shared Component) -->
|
||||
<nav class="h-screen w-64 fixed left-0 top-0 bg-surface dark:bg-surface-dim border-r border-outline-variant dark:border-outline z-50 flex flex-col py-stack-lg">
|
||||
<!-- Header / Brand Area -->
|
||||
<div class="px-margin-edge mb-section-gap flex flex-col items-start gap-stack-sm">
|
||||
<div class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed mb-2">Tradhox</div>
|
||||
<div class="flex items-center gap-3 w-full p-2 rounded-lg hover:bg-surface-container-high transition-colors duration-200 cursor-pointer">
|
||||
<img alt="Store Owner Profile" class="w-10 h-10 rounded-full object-cover border border-outline-variant" data-alt="A close-up studio portrait of a traditional Indian artisan, softly lit to highlight the textures of their woven garments. The artisan looks slightly off-camera with a serene, proud expression. The background is a muted, warm studio grey, emphasizing the subject's craftsmanship and cultural heritage." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCm99RI_WStIkfNUvQCwPmOMiIRksrAeImH1mePNgRRqFjNP9JCBi1L6r08WO7CnD1FahA_9N4hxQ8gjov6defwS78KAdtgV9Roz6f-hIgCBJ4XaK4nTrdsh3cYoOQGBHG7QTT4pSt8vWax1Pai6WNQ16KTx_sMrSR43n8Fl9azSyL_3q85YRaT9m--8gCuoaEcKJgILQE-i-eFPiyWAEi_KKcOPw25-8mrQFjICgvi2yhGsaosJKI_ZA"/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-button text-button text-on-surface">Tradhox Seller</span>
|
||||
<span class="font-label-caps text-label-caps text-secondary uppercase">Artisan Partner</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mt-2 w-full text-center py-2 px-4 border border-primary text-primary font-button text-button rounded hover:bg-primary/5 transition-colors duration-200">
|
||||
View Store
|
||||
</button>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<div class="flex flex-col flex-grow">
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 text-secondary dark:text-secondary-fixed-dim hover:bg-secondary-container/10 transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">home</span>
|
||||
<span class="font-button text-button">Home</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 text-secondary dark:text-secondary-fixed-dim hover:bg-secondary-container/10 transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">shopping_cart</span>
|
||||
<span class="font-button text-button">Orders</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 text-secondary dark:text-secondary-fixed-dim hover:bg-secondary-container/10 transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">inventory_2</span>
|
||||
<span class="font-button text-button">Products</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 text-secondary dark:text-secondary-fixed-dim hover:bg-secondary-container/10 transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">payments</span>
|
||||
<span class="font-button text-button">Payments</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 text-secondary dark:text-secondary-fixed-dim hover:bg-secondary-container/10 transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined text-[20px]">leaderboard</span>
|
||||
<span class="font-button text-button">Analytics</span>
|
||||
</a>
|
||||
<!-- Active Tab: Settings -->
|
||||
<a class="flex items-center gap-3 px-margin-edge py-3 bg-primary-container/5 text-primary font-bold border-r-4 border-primary transition-all opacity-80" href="#">
|
||||
<span class="material-symbols-outlined text-[20px] icon-fill">settings</span>
|
||||
<span class="font-button text-button">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- TopNavBar (Shared Component) -->
|
||||
<header class="fixed top-0 right-0 w-[calc(100%-16rem)] z-40 bg-surface/80 dark:bg-surface-dim/80 backdrop-blur-md border-b border-outline-variant dark:border-outline flex justify-between items-center h-16 px-margin-edge ml-64">
|
||||
<!-- Left Area (Empty or contextual, Product Name usually handled in sidebar for this layout, but following JSON search logic) -->
|
||||
<div class="flex items-center flex-1">
|
||||
<div class="relative w-64">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-secondary text-[20px]">search</span>
|
||||
<input class="w-full bg-surface-container-low border border-outline-variant rounded pl-10 pr-4 py-1.5 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" placeholder="Search..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Area: Actions & Profile -->
|
||||
<div class="flex items-center gap-stack-md">
|
||||
<button class="text-secondary dark:text-secondary-fixed-dim hover:text-primary dark:hover:text-primary-fixed transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
</button>
|
||||
<button class="text-secondary dark:text-secondary-fixed-dim hover:text-primary dark:hover:text-primary-fixed transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined">help_outline</span>
|
||||
</button>
|
||||
<div class="h-8 w-px bg-outline-variant mx-2"></div>
|
||||
<img alt="Seller Avatar" class="w-8 h-8 rounded-full border border-outline-variant cursor-pointer object-cover" data-alt="A small, circular avatar image showing a macro shot of intricately woven natural fibers. The texture is prominent, with warm earthy tones of beige and brown, representing an artisanal craft context in a clean, minimal style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDfP_P9njAGlKmQ5gSN9l0v9c6xGaUblWqTNEppcMtOKilo4W9lFZlEFfRQrZndgLsHerwuJCC14tUi3XVT46JenxA-GPordrhVI-sVklJOJawGnL0BhQGpXCMBGH1UQb5kztXU3vCWFfX8GVObem2XcNpZUnCsv3ITFLDAfSCzxGotu8FNdiZdkFRO_XKBQ0jHIPa-PTauYhkPzh8KnpMUgmbZqJ6c4UmFv3yTxAbEnafltbluo340aw"/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="ml-64 pt-24 min-h-screen px-margin-edge pb-section-gap">
|
||||
<div class="max-w-container-max mx-auto">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-section-gap">
|
||||
<h1 class="font-headline-lg text-headline-lg text-on-background mb-stack-sm">Settings</h1>
|
||||
<p class="font-body-lg text-body-lg text-secondary">Manage your artisan store profile, account details, and preferences.</p>
|
||||
</div>
|
||||
<!-- Settings Navigation (In-page Tabs) -->
|
||||
<div class="flex border-b border-outline-variant mb-section-gap gap-stack-lg overflow-x-auto relative">
|
||||
<!-- Active Tab -->
|
||||
<button class="font-button text-button text-primary pb-stack-sm border-b-2 border-primary relative top-[1px]">
|
||||
Store Profile
|
||||
</button>
|
||||
<button class="font-button text-button text-secondary hover:text-primary transition-colors pb-stack-sm border-b-2 border-transparent relative top-[1px]">
|
||||
Account Settings
|
||||
</button>
|
||||
<button class="font-button text-button text-secondary hover:text-primary transition-colors pb-stack-sm border-b-2 border-transparent relative top-[1px]">
|
||||
Shipping Preferences
|
||||
</button>
|
||||
<button class="font-button text-button text-secondary hover:text-primary transition-colors pb-stack-sm border-b-2 border-transparent relative top-[1px]">
|
||||
Notifications
|
||||
</button>
|
||||
</div>
|
||||
<!-- Tab Content: Store Profile -->
|
||||
<div class="flex flex-col gap-section-gap">
|
||||
<!-- Section 1: Basic Information (Asymmetric Layout) -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-gutter">
|
||||
<!-- Left Column: Context -->
|
||||
<div class="md:col-span-4 flex flex-col">
|
||||
<h2 class="font-headline-md text-headline-md text-on-background mb-stack-sm">Public Profile</h2>
|
||||
<p class="font-body-md text-body-md text-secondary">
|
||||
This information will be displayed publicly on your artisan store page. Make sure your brand reflects the quality of your craft.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Right Column: Form Fields in a Glass/Clean Card -->
|
||||
<div class="md:col-span-8 bg-surface-container-lowest border border-outline-variant rounded-lg p-margin-edge shadow-[0_4px_20px_rgba(107,26,44,0.02)]">
|
||||
<!-- Logo Upload -->
|
||||
<div class="flex items-center gap-stack-lg mb-stack-lg pb-stack-lg border-b border-surface-container-high">
|
||||
<div class="relative group">
|
||||
<img class="w-20 h-20 rounded-full object-cover border border-outline-variant" data-alt="A clean, minimalist logo placeholder image. The design features a stylized, continuous line-art drawing of a loom or weaving tool in deep maroon on an off-white background. The image has a subtle paper texture, conveying an authentic, handcrafted brand identity." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAILFVWTQhDaG_Vnsodmdq-gvnv7cLSh7Cebas19cjBXnaoRSzC2fK8sKdYgujI37D8ofBs35zZhLfMqXXdHttB6L0dyVH2GC_PEAl-u9XDxmkb4CRYAsXVfQfD8y6nNR40XzDPhz4WoZh-fyIdgEX4dk54o_GhsnE0u7cZYna-QzgjrAUl1-Lp1z2PNjVnMMk3eQYQ79VdQwdGb4wZPmxqYHDqtjc4847zh0LIyylLdHDBlQH_eC29Cw"/>
|
||||
<div class="absolute inset-0 bg-primary/20 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
|
||||
<span class="material-symbols-outlined text-surface-container-lowest">edit</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-stack-sm">
|
||||
<button class="self-start px-4 py-2 border border-primary text-primary font-button text-button rounded hover:bg-primary-container/5 transition-colors">
|
||||
Change Logo
|
||||
</button>
|
||||
<span class="font-label-caps text-label-caps text-secondary uppercase">JPG, GIF or PNG. Max size of 800K</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Inputs -->
|
||||
<div class="flex flex-col gap-stack-md">
|
||||
<!-- Store Name -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-label-caps text-label-caps text-secondary uppercase tracking-wider">Store Name</label>
|
||||
<input class="bg-surface-container-lowest border border-outline-variant rounded px-4 py-3 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" type="text" value="Heritage Weaves & Co."/>
|
||||
</div>
|
||||
<!-- Store URL -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-label-caps text-label-caps text-secondary uppercase tracking-wider">Store URL</label>
|
||||
<div class="flex">
|
||||
<span class="inline-flex items-center px-4 rounded-l border border-r-0 border-outline-variant bg-surface-container-low text-secondary font-body-md text-body-md">
|
||||
tradhox.com/store/
|
||||
</span>
|
||||
<input class="flex-1 bg-surface-container-lowest border border-outline-variant rounded-r px-4 py-3 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" type="text" value="heritage-weaves"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Description -->
|
||||
<div class="flex flex-col gap-2 mt-stack-sm">
|
||||
<label class="font-label-caps text-label-caps text-secondary uppercase tracking-wider">About Your Craft</label>
|
||||
<textarea class="bg-surface-container-lowest border border-outline-variant rounded px-4 py-3 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all resize-none" rows="4">Preserving centuries-old handloom techniques, our family has been creating pure silk and cotton textiles for three generations. Every piece is a testament to the dedication of our master weavers in Varanasi.</textarea>
|
||||
<span class="font-label-caps text-label-caps text-secondary uppercase self-end mt-1">214 / 500 characters</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Divider -->
|
||||
<hr class="border-outline-variant"/>
|
||||
<!-- Section 2: Contact Information -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-gutter pb-section-gap">
|
||||
<!-- Left Column -->
|
||||
<div class="md:col-span-4 flex flex-col">
|
||||
<h2 class="font-headline-md text-headline-md text-on-background mb-stack-sm">Support Contact</h2>
|
||||
<p class="font-body-md text-body-md text-secondary">
|
||||
How buyers can reach you for inquiries regarding custom orders or product details.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Right Column -->
|
||||
<div class="md:col-span-8 bg-surface-container-lowest border border-outline-variant rounded-lg p-margin-edge shadow-[0_4px_20px_rgba(107,26,44,0.02)]">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-stack-md">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-label-caps text-label-caps text-secondary uppercase tracking-wider">Support Email</label>
|
||||
<input class="bg-surface-container-lowest border border-outline-variant rounded px-4 py-3 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" type="email" value="hello@heritageweaves.in"/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="font-label-caps text-label-caps text-secondary uppercase tracking-wider">Phone Number (Optional)</label>
|
||||
<input class="bg-surface-container-lowest border border-outline-variant rounded px-4 py-3 font-body-md text-body-md text-on-surface focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" type="tel" value="+91 98765 43210"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Action Bar (Sticky or floating feel) -->
|
||||
<div class="flex justify-end gap-stack-md border-t border-outline-variant pt-margin-edge mt-auto">
|
||||
<button class="px-6 py-3 border border-outline-variant text-on-surface font-button text-button rounded hover:bg-surface-container-high transition-colors">
|
||||
Discard Changes
|
||||
</button>
|
||||
<button class="px-6 py-3 bg-primary text-on-primary font-button text-button rounded shadow-sm hover:bg-primary/90 transition-colors flex items-center gap-2">
|
||||
Save Profile
|
||||
<span class="material-symbols-outlined text-[18px]">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
BIN
seller_settings/screen.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
196
src/App.test.tsx
|
|
@ -2,9 +2,49 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
|||
import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'
|
||||
import App from './App'
|
||||
|
||||
// Mock window.scrollTo since jsdom does not implement it
|
||||
// Mock window.scrollTo and fetch since jsdom does not implement them
|
||||
beforeAll(() => {
|
||||
window.scrollTo = vi.fn()
|
||||
globalThis.fetch = vi.fn((url) => {
|
||||
let responseData: any = {};
|
||||
if (url.includes('/api/auth/register') || url.includes('/api/auth/login')) {
|
||||
responseData = {
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'test_seller',
|
||||
email: 'test@example.com',
|
||||
profile: { onboarding_step: 4, phone_verified: true, email_verified: true, is_gstin_verified: true, aadhar_s3_key: 'mock_aadhar', pan_s3_key: 'mock_pan', store_name: 'Crafty Store', logo_s3_key: 'mock_logo', street: '123 Loom Lane', status: 'pending_approval' }
|
||||
}
|
||||
};
|
||||
} else if (url.includes('/api/auth/verify-otp')) {
|
||||
responseData = { verified: true, next_step: 'complete_profile' };
|
||||
} else if (url.includes('/api/profile/submit-gstin')) {
|
||||
responseData = { verified: true, business_name: 'TEST BUSINESS', address: 'Test address', gstin_status: 'Active' };
|
||||
} else if (url.includes('/api/profile/presigned-url')) {
|
||||
responseData = { presigned_url: 'https://s3.mock/upload', s3_key: 'suppliers/1/mock_key.jpg' };
|
||||
} else if (url.includes('/api/profile/')) {
|
||||
responseData = {
|
||||
id: 1,
|
||||
username: 'test_seller',
|
||||
email: 'test@example.com',
|
||||
profile: { onboarding_step: 4, phone_verified: true, email_verified: true, is_gstin_verified: true, aadhar_s3_key: 'mock_aadhar', pan_s3_key: 'mock_pan', store_name: 'Crafty Store', logo_s3_key: 'mock_logo', street: '123 Loom Lane', status: 'pending_approval' }
|
||||
};
|
||||
} else if (url.includes('/api/products')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/orders')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/returns')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/wallet')) {
|
||||
responseData = { outstanding: 850.00, withdrawn: 1250.00, transactions: [] };
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(responseData)
|
||||
} as Response);
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -15,21 +55,21 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
it('renders landing page with correct primary titles', () => {
|
||||
render(<App />)
|
||||
|
||||
expect(screen.getByText(/Sell Globally\./i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Celebrate Craft\./i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/The Winter Weaves/i)).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByRole('heading', { name: /^Expand Your Reach$/i })).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByRole('heading', { name: /^Explore by Craft$/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('navigates to Support page and handles contact form submission', () => {
|
||||
render(<App />)
|
||||
|
||||
const supportBtn = screen.getByText('Support')
|
||||
const supportBtn = screen.getByText('Contact Us')
|
||||
fireEvent.click(supportBtn)
|
||||
|
||||
const nameInput = screen.getByLabelText(/Full Name \*/i)
|
||||
const emailInput = screen.getByLabelText(/Email Address \*/i)
|
||||
const msgInput = screen.getByLabelText(/Message \*/i)
|
||||
const nameInput = screen.getByPlaceholderText(/e.g. Ananya Sharma/i)
|
||||
const emailInput = screen.getByPlaceholderText(/e.g. ananya@example.com/i)
|
||||
const msgInput = screen.getByPlaceholderText(/How can we assist you today\?/i)
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } })
|
||||
fireEvent.change(emailInput, { target: { value: 'john@example.com' } })
|
||||
|
|
@ -37,7 +77,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
|
||||
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
|
||||
const submitBtn = screen.getByText('Submit Inquiry')
|
||||
const submitBtn = screen.getByText('Send Message')
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
expect(alertMock).toHaveBeenCalled()
|
||||
|
|
@ -47,153 +87,71 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
it('signs up successfully and completes profile to active dashboard', async () => {
|
||||
render(<App />)
|
||||
|
||||
const getStartedBtn = screen.getByRole('button', { name: /^Get Started$/i })
|
||||
const getStartedBtn = screen.getByText(/^Get started$/i)
|
||||
fireEvent.click(getStartedBtn)
|
||||
// fireEvent.click(screen.getByText(/Create an account/i))
|
||||
|
||||
const emailInput = screen.getByLabelText(/Business Email \*/i)
|
||||
const phoneInput = screen.getByLabelText(/Mobile Number \*/i)
|
||||
const passInput = screen.getByLabelText(/Create Password \*/i)
|
||||
const emailInput = screen.getByPlaceholderText(/Enter your email/i)
|
||||
const phoneInput = screen.getByPlaceholderText(/9876543210/i)
|
||||
const passInput = screen.getByPlaceholderText(/Create a password/i)
|
||||
const confirmPassInput = screen.getByPlaceholderText(/Repeat your password/i)
|
||||
const policyCheckbox = screen.getByLabelText(/I agree to the/i)
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'seller@example.com' } })
|
||||
fireEvent.change(phoneInput, { target: { value: '9876543210' } })
|
||||
fireEvent.change(passInput, { target: { value: 'password123' } })
|
||||
fireEvent.change(confirmPassInput, { target: { value: 'password123' } })
|
||||
fireEvent.click(policyCheckbox)
|
||||
|
||||
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
|
||||
const registerBtn = screen.getByRole('button', { name: /Register Business/i })
|
||||
const registerBtn = screen.getByRole('button', { name: /Create Account/i })
|
||||
fireEvent.click(registerBtn)
|
||||
|
||||
// Step 1: Contact Verification
|
||||
expect(screen.getByText(/Step 1 of 3: Contact Verification/i)).toBeInTheDocument()
|
||||
|
||||
const sendWhatsappBtn = screen.getByText(/Send WhatsApp OTP/i)
|
||||
fireEvent.click(sendWhatsappBtn)
|
||||
|
||||
const whatsappCodeInput = screen.getByPlaceholderText(/Enter 123456/i)
|
||||
fireEvent.change(whatsappCodeInput, { target: { value: '123456' } })
|
||||
const verifyWhatsappBtn = screen.getByRole('button', { name: /Verify Code/i })
|
||||
fireEvent.click(verifyWhatsappBtn)
|
||||
|
||||
const sendEmailBtn = screen.getByText(/Send Email OTP/i)
|
||||
fireEvent.click(sendEmailBtn)
|
||||
|
||||
// There are now multiple placeholder elements with "Enter 123456". Let's fetch the visible inputs.
|
||||
const codeInputs = screen.getAllByPlaceholderText(/Enter 123456/i)
|
||||
fireEvent.change(codeInputs[codeInputs.length - 1], { target: { value: '123456' } })
|
||||
const verifyButtons = screen.getAllByRole('button', { name: /Verify Code/i })
|
||||
fireEvent.click(verifyButtons[verifyButtons.length - 1])
|
||||
|
||||
const nextStep1Btn = screen.getByRole('button', { name: /Next Step: Identity Verification/i })
|
||||
fireEvent.click(nextStep1Btn)
|
||||
|
||||
// Step 2: Tax & Identity Verification
|
||||
expect(screen.getByText(/Step 2 of 3: Tax & Identity Verification/i)).toBeInTheDocument()
|
||||
|
||||
const taxInput = screen.getByLabelText(/GSTIN Number \*/i)
|
||||
fireEvent.change(taxInput, { target: { value: '29AAAAA1111A1Z1' } })
|
||||
const verifyGstinBtn = screen.getByRole('button', { name: /Verify GSTIN/i })
|
||||
fireEvent.click(verifyGstinBtn)
|
||||
|
||||
// Upload files
|
||||
const aadharInput = screen.getByLabelText(/Aadhaar Card Upload \*/i)
|
||||
const panInput = screen.getByLabelText(/PAN Card Upload \*/i)
|
||||
|
||||
// Trigger file changes
|
||||
fireEvent.change(aadharInput, { target: { files: [{ name: 'aadhar_card.pdf' }] } })
|
||||
fireEvent.change(panInput, { target: { files: [{ name: 'pan_card.pdf' }] } })
|
||||
|
||||
const nextStep2Btn = screen.getByRole('button', { name: /Next Step: Store & Location/i })
|
||||
fireEvent.click(nextStep2Btn)
|
||||
|
||||
// Step 3: Store Details & Location Map
|
||||
expect(screen.getByText(/Step 3 of 3: Store & Pickup Location/i)).toBeInTheDocument()
|
||||
|
||||
const storeNameInput = screen.getByLabelText(/Store Display Name \*/i)
|
||||
fireEvent.change(storeNameInput, { target: { value: 'Crafty Store' } })
|
||||
|
||||
const bioText = screen.getByLabelText(/About Your Craft \/ Business \*/i)
|
||||
fireEvent.change(bioText, { target: { value: 'Handmade pottery.' } })
|
||||
|
||||
const streetInput = screen.getByLabelText(/Location in Text \(Address\) \*/i)
|
||||
const cityInput = screen.getByLabelText(/City \*/i)
|
||||
const pincodeInput = screen.getByLabelText(/Pincode \*/i)
|
||||
|
||||
fireEvent.change(streetInput, { target: { value: '123 Loom Lane' } })
|
||||
fireEvent.change(cityInput, { target: { value: 'Handloom City' } })
|
||||
fireEvent.change(pincodeInput, { target: { value: '560001' } })
|
||||
|
||||
const submitBtn = screen.getByRole('button', { name: /Submit Supplier Profile/i })
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
// Welcome Page & Setup Tour
|
||||
expect(screen.getByText(/Welcome to Global Artisans Hub!/i)).toBeInTheDocument()
|
||||
|
||||
// Complete tour steps
|
||||
const startTourBtn = screen.getByRole('button', { name: /Start Tour/i })
|
||||
fireEvent.click(startTourBtn)
|
||||
|
||||
const next1 = screen.getByRole('button', { name: /Next: Order Management/i })
|
||||
fireEvent.click(next1)
|
||||
|
||||
const next2 = screen.getByRole('button', { name: /Next: Barcode Generation/i })
|
||||
fireEvent.click(next2)
|
||||
|
||||
const next3 = screen.getByRole('button', { name: /Next: Earnings & Wallet/i })
|
||||
fireEvent.click(next3)
|
||||
|
||||
const finishBtn = screen.getByRole('button', { name: /Launch Dashboard 🚀/i })
|
||||
fireEvent.click(finishBtn)
|
||||
|
||||
expect(await screen.findByText(/Welcome to Tradhox/i)).toBeInTheDocument()
|
||||
// Redirects to Dashboard Page
|
||||
expect(screen.getByText('Performance Analytics')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Total Sales Volume/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Welcome to Tradhox/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Welcome to Tradhox/i)).toBeInTheDocument()
|
||||
|
||||
alertMock.mockRestore()
|
||||
})
|
||||
|
||||
it('logs in successfully and navigates dashboard tools', () => {
|
||||
it('logs in successfully and navigates dashboard tools', async () => {
|
||||
render(<App />)
|
||||
|
||||
const loginBtn = screen.getByRole('button', { name: /^Login$/i })
|
||||
const loginBtn = screen.getByText(/^Login$/i)
|
||||
fireEvent.click(loginBtn)
|
||||
|
||||
const emailInput = screen.getByLabelText(/Business Email/i)
|
||||
const passInput = screen.getByLabelText(/Password/i)
|
||||
const emailInput = screen.getByPlaceholderText(/you@example.com/i)
|
||||
const passInput = screen.getByPlaceholderText(/••••••••/)
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'seller@example.com' } })
|
||||
fireEvent.change(passInput, { target: { value: 'password123' } })
|
||||
|
||||
const submitBtn = screen.getByRole('button', { name: /Secure Login/i })
|
||||
const submitBtn = screen.getByRole('button', { name: /Sign In/i })
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
// Check dashboard rendering
|
||||
expect(screen.getByText('Performance Analytics')).toBeInTheDocument()
|
||||
expect(await screen.findByText(/Total Sales/i)).toBeInTheDocument()
|
||||
|
||||
// Check Sidebar Tab click: Manage Products
|
||||
const productsTabBtn = screen.getByText(/Manage Products/i)
|
||||
const productsTabBtn = screen.getAllByText(/^Products$/i)[0]
|
||||
fireEvent.click(productsTabBtn)
|
||||
|
||||
expect(screen.getByText(/Active Inventory/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Upload New Product/i)).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/Products/i).length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText(/Add Product/i).length).toBeGreaterThan(0)
|
||||
|
||||
// Check Sidebar Tab click: Orders & Transit
|
||||
const ordersTabBtn = screen.getByText(/Orders & Transit/i)
|
||||
const ordersTabBtn = screen.getByText(/Orders/i)
|
||||
fireEvent.click(ordersTabBtn)
|
||||
|
||||
expect(screen.getByText(/Active Orders & Payout status/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/All Orders/i)).toBeInTheDocument()
|
||||
|
||||
// Check Sidebar Tab click: Returns Management
|
||||
const returnsTabBtn = screen.getByText(/Returns Management/i)
|
||||
fireEvent.click(returnsTabBtn)
|
||||
|
||||
expect(screen.getByText(/Returns & Quality Assurance Center/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Items Requested for Return/i)).toBeInTheDocument()
|
||||
|
||||
// Check Sidebar Tab click: Wallet & Payouts
|
||||
const walletTabBtn = screen.getByText(/Wallet & Payouts/i)
|
||||
// Check Sidebar Tab click: Payments
|
||||
const walletTabBtn = screen.getByText(/Payments & Earnings/i)
|
||||
fireEvent.click(walletTabBtn)
|
||||
|
||||
expect(screen.getByText(/Wallet & Payout Portal/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Outstanding Ready Balance/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Available for Payout/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Transaction History/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
2189
src/App.tsx
42
src/components/dashboard/DashboardLayout.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
import Sidebar from '../layout/Sidebar';
|
||||
import DashboardHeader from '../layout/DashboardHeader';
|
||||
import OverviewTab from './tabs/OverviewTab';
|
||||
import OrdersTab from './tabs/OrdersTab';
|
||||
import ProductsTab from './tabs/ProductsTab';
|
||||
import AddProductTab from './tabs/AddProductTab';
|
||||
import PaymentsTab from './tabs/PaymentsTab';
|
||||
import SettingsTab from './tabs/SettingsTab';
|
||||
|
||||
export default function DashboardLayout() {
|
||||
const { dashTab } = useSeller();
|
||||
|
||||
const renderContent = () => {
|
||||
switch (dashTab) {
|
||||
case 'overview': return <OverviewTab />;
|
||||
case 'orders': return <OrdersTab />;
|
||||
case 'products': return <ProductsTab />;
|
||||
case 'add-product': return <AddProductTab />;
|
||||
case 'payments': return <PaymentsTab />;
|
||||
case 'settings': return <SettingsTab />;
|
||||
default: return <OverviewTab />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="has-background-white-ter" style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<DashboardHeader />
|
||||
<div className="columns is-gapless is-flex-grow-1 is-marginless">
|
||||
<div className="column is-2 is-hidden-mobile has-background-white" style={{ borderRight: '1px solid #e2dfdb' }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="column is-10 is-12-mobile">
|
||||
<main className="p-6" style={{ height: '100%' }}>
|
||||
{renderContent()}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
170
src/components/dashboard/tabs/AddProductTab.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
export default function AddProductTab() {
|
||||
const { setDashTab, productForm, setProductForm, handleSaveProduct, isEditingProduct } = useSeller();
|
||||
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setImagePreview(result);
|
||||
setProductForm({...productForm, image: result});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
// Call the actual API save logic
|
||||
handleSaveProduct(e);
|
||||
|
||||
// Simulate animation and redirect
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setDashTab('products');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-flex is-align-items-center mb-5">
|
||||
<button
|
||||
className="button is-ghost has-text-grey pl-0 mr-3"
|
||||
onClick={() => setDashTab('products')}
|
||||
>
|
||||
<span className="icon"><span className="material-symbols-outlined">arrow_back</span></span>
|
||||
<span>Back</span>
|
||||
</button>
|
||||
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||
{isEditingProduct ? 'Edit Product' : 'Add New Product'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="columns is-multiline">
|
||||
|
||||
{/* Left Column: Basic Info */}
|
||||
<div className="column is-8">
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Product Title</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="e.g. Handwoven Silk Scarf" required value={productForm.title} onChange={e => setProductForm({...productForm, title: e.target.value})} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Description</label>
|
||||
<div className="control">
|
||||
<textarea className="textarea" placeholder="Describe the artisan craft, material, and story..." rows={4} required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className="column is-6">
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Price (INR)</label>
|
||||
<div className="control has-icons-left">
|
||||
<input className="input" type="number" placeholder="0.00" step="0.01" required value={productForm.price || ''} onChange={e => setProductForm({...productForm, price: Number(e.target.value)})} />
|
||||
<span className="icon is-small is-left">
|
||||
<span className="material-symbols-outlined">currency_rupee</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-6">
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Stock Quantity</label>
|
||||
<div className="control has-icons-left">
|
||||
<input className="input" type="number" placeholder="10" min="0" required value={productForm.stock || ''} onChange={e => setProductForm({...productForm, stock: Number(e.target.value)})} />
|
||||
<span className="icon is-small is-left">
|
||||
<span className="material-symbols-outlined">inventory_2</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Imagery & Category */}
|
||||
<div className="column is-4">
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Product Image</label>
|
||||
<div className="control">
|
||||
<div className="file is-boxed is-fullwidth">
|
||||
<label className="file-label is-flex-direction-column is-align-items-center" style={{ width: '100%' }}>
|
||||
<input className="file-input" type="file" accept="image/*" onChange={handleImageChange} required={!imagePreview} />
|
||||
<span
|
||||
className="file-cta is-flex is-flex-direction-column is-align-items-center py-5 has-background-white"
|
||||
style={{ border: '1px solid var(--bulma-primary)', width: '100%', overflow: 'hidden' }}
|
||||
>
|
||||
{imagePreview ? (
|
||||
<img src={imagePreview} alt="Preview" style={{ maxHeight: '150px', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<>
|
||||
<span className="file-icon mb-2 has-text-primary">
|
||||
<span className="material-symbols-outlined is-size-3">upload</span>
|
||||
</span>
|
||||
<span className="file-label has-text-primary">
|
||||
Choose an image…
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
{imagePreview && (
|
||||
<p className="help has-text-centered mt-2 is-size-7">
|
||||
<a onClick={() => setImagePreview(null)} className="has-text-danger">Remove image</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field mb-4">
|
||||
<label className="label has-text-grey-dark">Category</label>
|
||||
<div className="control">
|
||||
<div className="select is-fullwidth">
|
||||
<select required value={productForm.category} onChange={e => setProductForm({...productForm, category: e.target.value})}>
|
||||
<option value="">Select a category</option>
|
||||
<option value="Apparel">Apparel & Textiles</option>
|
||||
<option value="jewelry">Jewelry</option>
|
||||
<option value="pottery">Pottery</option>
|
||||
<option value="woodwork">Woodwork</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="is-flex is-justify-content-flex-end mt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="button is-light mr-3"
|
||||
onClick={() => setDashTab('products')}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={`button is-primary is-outlined ${isSubmitting ? 'is-loading' : ''}`}
|
||||
>
|
||||
{isEditingProduct ? 'Save Changes' : 'Publish Product'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
src/components/dashboard/tabs/OrdersTab.tsx
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
type TabFilter = 'All Orders' | 'Pending' | 'Shipped' | 'Completed';
|
||||
|
||||
const TAB_STATUSES: Record<TabFilter, string[]> = {
|
||||
'All Orders': [],
|
||||
'Pending': ['Pending Acceptance'],
|
||||
'Shipped': ['Ready to Ship', 'Shipped'],
|
||||
'Completed': ['Delivered', 'Cancelled', 'Rejected'],
|
||||
};
|
||||
|
||||
const STATUS_TAG: Record<string, string> = {
|
||||
'Pending Acceptance': 'is-warning is-light',
|
||||
'Ready to Ship': 'is-primary is-light',
|
||||
'Shipped': 'is-info is-light',
|
||||
'Delivered': 'is-success is-light',
|
||||
'Cancelled': 'is-danger is-light',
|
||||
'Rejected': 'is-danger is-light',
|
||||
};
|
||||
|
||||
export default function OrdersTab() {
|
||||
const { orders, handleAcceptOrder, handleRejectOrder, setSelectedOrderDetail } = useSeller();
|
||||
const [activeTab, setActiveTab] = useState<TabFilter>('All Orders');
|
||||
|
||||
const filteredOrders = TAB_STATUSES[activeTab].length === 0
|
||||
? orders
|
||||
: orders.filter((o: any) => TAB_STATUSES[activeTab].includes(o.status));
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const handleExportCsv = () => {
|
||||
const header = 'Order ID,Date,Customer,Item,Qty,Status,Total\n';
|
||||
const rows = orders.map((o: any) =>
|
||||
`${o.id},${o.date},${o.customer},${o.item},${o.quantity},${o.status},${o.total}`
|
||||
).join('\n');
|
||||
const blob = new Blob([header + rows], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'orders_export.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Orders</h1>
|
||||
<button className="button is-primary is-outlined" onClick={handleExportCsv}>
|
||||
<span className="icon"><span className="material-symbols-outlined">download</span></span>
|
||||
<span>Export</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="box">
|
||||
<div className="tabs">
|
||||
<ul>
|
||||
{(Object.keys(TAB_STATUSES) as TabFilter[]).map(tab => (
|
||||
<li key={tab} className={activeTab === tab ? 'is-active' : ''}>
|
||||
<a onClick={() => setActiveTab(tab)}>{tab}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{filteredOrders.length === 0 ? (
|
||||
<div className="has-text-centered py-6 has-text-grey">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '3rem' }}>inventory_2</span>
|
||||
<p className="mt-2">No orders found in this category.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="table is-fullwidth is-hoverable is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Date</th>
|
||||
<th>Customer</th>
|
||||
<th>Item</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredOrders.map((order: any) => (
|
||||
<tr key={order.id}>
|
||||
<td><strong>#{order.id}</strong></td>
|
||||
<td>{formatDate(order.date)}</td>
|
||||
<td>{order.customer || '—'}</td>
|
||||
<td>{order.item || '—'}</td>
|
||||
<td>
|
||||
<span className={`tag ${STATUS_TAG[order.status] || 'is-light'}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
</td>
|
||||
<td>₹{Number(order.total).toFixed(2)}</td>
|
||||
<td>
|
||||
<div className="buttons are-small">
|
||||
<button
|
||||
className="button is-light"
|
||||
onClick={() => setSelectedOrderDetail(order)}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
{order.status === 'Pending Acceptance' && (
|
||||
<>
|
||||
<button
|
||||
className="button is-success is-light"
|
||||
onClick={() => handleAcceptOrder(order.id)}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-light"
|
||||
onClick={() => handleRejectOrder(order.id)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
src/components/dashboard/tabs/OverviewTab.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
export default function OverviewTab() {
|
||||
const { orders, products, computeRealtimeMetrics } = useSeller();
|
||||
const metrics = computeRealtimeMetrics();
|
||||
|
||||
// Group orders by month for chart
|
||||
const chartData = useMemo(() => {
|
||||
const monthlySales: Record<number, number> = {};
|
||||
orders
|
||||
.filter((o: any) => o.status !== 'Cancelled' && o.status !== 'Rejected')
|
||||
.forEach((o: any) => {
|
||||
const date = new Date(o.date);
|
||||
if (!isNaN(date.getTime())) {
|
||||
const month = date.getMonth();
|
||||
monthlySales[month] = (monthlySales[month] || 0) + Number(o.total);
|
||||
}
|
||||
});
|
||||
|
||||
// Show last 6 months
|
||||
const currentMonth = new Date().getMonth();
|
||||
const months = [];
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const idx = (currentMonth - i + 12) % 12;
|
||||
months.push({ name: MONTH_NAMES[idx], sales: monthlySales[idx] || 0 });
|
||||
}
|
||||
return months;
|
||||
}, [orders]);
|
||||
|
||||
const activeOrderCount = orders.filter((o: any) =>
|
||||
!['Cancelled', 'Rejected', 'Delivered'].includes(o.status)
|
||||
).length;
|
||||
|
||||
const recentActivity = useMemo(() => {
|
||||
return [...orders]
|
||||
.sort((a: any, b: any) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.slice(0, 5);
|
||||
}, [orders]);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const statusTagClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Delivered': return 'is-success is-light';
|
||||
case 'Shipped': return 'is-info is-light';
|
||||
case 'Ready to Ship': return 'is-primary is-light';
|
||||
case 'Pending Acceptance': return 'is-warning is-light';
|
||||
case 'Rejected':
|
||||
case 'Cancelled': return 'is-danger is-light';
|
||||
default: return 'is-light';
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadCsv = () => {
|
||||
const header = 'Month,Sales\n';
|
||||
const rows = chartData.map(row => `${row.name},${row.sales}`).join('\n');
|
||||
const blob = new Blob([header + rows], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'sales_report.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Overview</h1>
|
||||
<button className="button is-primary is-outlined" onClick={handleDownloadCsv}>
|
||||
<span className="icon"><span className="material-symbols-outlined">download</span></span>
|
||||
<span>Download Report</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="columns is-multiline">
|
||||
<div className="column is-4">
|
||||
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||
<p className="heading has-text-grey-dark">Total Sales</p>
|
||||
<p className="title is-2 has-text-dark">
|
||||
{orders.length === 0 ? '—' : `₹${metrics.totalSales.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||
<p className="heading has-text-grey-dark">Active Orders</p>
|
||||
<p className="title is-2 has-text-dark">
|
||||
{orders.length === 0 ? '—' : activeOrderCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-4">
|
||||
<div className="box" style={{ borderTop: '4px solid var(--bulma-primary)' }}>
|
||||
<p className="heading has-text-grey-dark">Total Products</p>
|
||||
<p className="title is-2 has-text-dark">
|
||||
{products.length === 0 ? '—' : products.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sales Chart */}
|
||||
<div className="box mt-4">
|
||||
<h2 className="title is-5 has-text-dark mb-4">Sales Performance (Last 6 Months)</h2>
|
||||
{orders.length === 0 ? (
|
||||
<div className="has-text-centered py-6 has-text-grey">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '3rem' }}>bar_chart</span>
|
||||
<p className="mt-2">No order data yet. Sales will appear here once orders are placed.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: 300 }}>
|
||||
<ResponsiveContainer>
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorSales" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="var(--bulma-primary)" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="var(--bulma-primary)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis dataKey="name" tick={{ fill: '#5f5e5b' }} axisLine={false} tickLine={false} />
|
||||
<YAxis tick={{ fill: '#5f5e5b' }} axisLine={false} tickLine={false} tickFormatter={(v) => `₹${v}`} />
|
||||
<Tooltip formatter={(value) => [`₹${Number(value).toFixed(2)}`, 'Sales']} />
|
||||
<Area type="monotone" dataKey="sales" stroke="var(--bulma-primary)" fillOpacity={1} fill="url(#colorSales)" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Orders */}
|
||||
<div className="box mt-5">
|
||||
<h2 className="title is-5 has-text-dark">Recent Orders</h2>
|
||||
{recentActivity.length === 0 ? (
|
||||
<div className="has-text-centered py-5 has-text-grey">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '3rem' }}>inbox</span>
|
||||
<p className="mt-2">No orders yet. Your recent orders will appear here.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Customer</th>
|
||||
<th>Item</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentActivity.map((order: any) => (
|
||||
<tr key={order.id}>
|
||||
<td>{formatDate(order.date)}</td>
|
||||
<td>{order.customer || '—'}</td>
|
||||
<td>{order.item || '—'}</td>
|
||||
<td>₹{Number(order.total).toFixed(2)}</td>
|
||||
<td><span className={`tag ${statusTagClass(order.status)}`}>{order.status}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
147
src/components/dashboard/tabs/PaymentsTab.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
export default function PaymentsTab() {
|
||||
const { wallet, withdrawAmount, setWithdrawAmount, handleWithdrawRequest } = useSeller();
|
||||
|
||||
const outstanding = Number(wallet.outstanding || 0);
|
||||
const withdrawn = Number(wallet.withdrawn || 0);
|
||||
const transactions = wallet.transactions || wallet.history || [];
|
||||
|
||||
// Pending = orders delivered but not yet in wallet (approximated as 0 unless backend provides it)
|
||||
const pendingClearance = Number(wallet.pending_clearance || 0);
|
||||
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return dateStr;
|
||||
return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
};
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
await handleWithdrawRequest();
|
||||
setShowWithdrawModal(false);
|
||||
setWithdrawAmount('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-3 has-text-dark mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||
Payments & Earnings
|
||||
</h1>
|
||||
|
||||
<div className="columns is-multiline mb-5">
|
||||
<div className="column is-6">
|
||||
<div className="box has-background-primary has-text-white">
|
||||
<p className="heading has-text-white-ter mb-1">Available for Payout</p>
|
||||
<p className="title is-1 has-text-white mb-4">
|
||||
₹{outstanding.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
<button
|
||||
className="button is-white is-outlined is-fullwidth"
|
||||
onClick={() => setShowWithdrawModal(true)}
|
||||
disabled={outstanding <= 0}
|
||||
>
|
||||
Withdraw Funds
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-6">
|
||||
<div className="box" style={{ height: '100%' }}>
|
||||
<p className="heading has-text-grey-dark mb-1">Total Withdrawn</p>
|
||||
<p className="title is-3 has-text-dark mb-3">
|
||||
₹{withdrawn.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
{pendingClearance > 0 && (
|
||||
<>
|
||||
<p className="heading has-text-grey-dark mb-1 mt-3">Pending Clearance</p>
|
||||
<p className="title is-5 has-text-dark mb-2">
|
||||
₹{pendingClearance.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p className="is-size-7 has-text-grey">Funds will be available in 3-5 business days after order delivery.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Withdraw Modal */}
|
||||
{showWithdrawModal && (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" onClick={() => setShowWithdrawModal(false)} />
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">Withdraw Funds</p>
|
||||
<button className="delete" onClick={() => setShowWithdrawModal(false)} />
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<p className="mb-3 has-text-grey">
|
||||
Available: ₹{outstanding.toLocaleString('en-IN', { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
<div className="field">
|
||||
<label className="label">Amount (₹)</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
min="1"
|
||||
max={outstanding}
|
||||
placeholder="Enter amount"
|
||||
value={withdrawAmount}
|
||||
onChange={e => setWithdrawAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer className="modal-card-foot">
|
||||
<button className="button is-primary" onClick={handleWithdraw}>Confirm Withdrawal</button>
|
||||
<button className="button" onClick={() => setShowWithdrawModal(false)}>Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="box">
|
||||
<h2 className="title is-5 has-text-dark">Transaction History</h2>
|
||||
{transactions.length === 0 ? (
|
||||
<div className="has-text-centered py-5 has-text-grey">
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '3rem' }}>receipt_long</span>
|
||||
<p className="mt-2">No transactions yet.</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{transactions.map((tx: any) => {
|
||||
const amount = Number(tx.amount || 0);
|
||||
const isDebit = tx.type === 'withdrawal' || amount < 0;
|
||||
return (
|
||||
<tr key={tx.id}>
|
||||
<td>{formatDate(tx.date)}</td>
|
||||
<td>{tx.description || (isDebit ? 'Payout Withdrawal' : 'Order Revenue')}</td>
|
||||
<td className={isDebit ? 'has-text-danger' : 'has-text-success'}>
|
||||
{isDebit ? '-' : '+'}₹{Math.abs(amount).toFixed(2)}
|
||||
</td>
|
||||
<td>
|
||||
<span className={`tag ${tx.status === 'Transferred' ? 'is-success is-light' : 'is-warning is-light'}`}>
|
||||
{tx.status || 'Pending'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
src/components/dashboard/tabs/ProductsTab.tsx
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
export default function ProductsTab() {
|
||||
const {
|
||||
setDashTab,
|
||||
bulkCsvFile, setBulkCsvFile,
|
||||
bulkZipFile, setBulkZipFile,
|
||||
handleBulkUploadSubmit,
|
||||
isParsingBulk,
|
||||
bulkLog,
|
||||
uploadDocument,
|
||||
products,
|
||||
handleEditClick,
|
||||
handleDeleteProduct
|
||||
} = useSeller();
|
||||
|
||||
const [showBulkUploadModal, setShowBulkUploadModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-flex is-justify-content-space-between is-align-items-center mb-5">
|
||||
<h1 className="title is-3 has-text-dark mb-0" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Products</h1>
|
||||
<div className="buttons">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setShowBulkUploadModal(true)}
|
||||
style={{ backgroundColor: 'transparent', borderColor: '#4d0218', color: '#4d0218' }}
|
||||
>
|
||||
<span className="icon"><span className="material-symbols-outlined">upload_file</span></span>
|
||||
<span>Bulk Upload</span>
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setDashTab('add-product')}
|
||||
style={{ backgroundColor: 'transparent', borderColor: '#4d0218', color: '#4d0218' }}
|
||||
>
|
||||
<span className="icon"><span className="material-symbols-outlined">add</span></span>
|
||||
<span>Add Product</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="columns is-multiline">
|
||||
{products.map((item: any) => (
|
||||
<div className="column is-3" key={item.id}>
|
||||
<div className="card" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div className="card-image">
|
||||
<figure className="image is-4by3 has-background-light">
|
||||
{item.image ? (
|
||||
<img src={item.image} alt={item.title} style={{ objectFit: 'cover', height: '100%', width: '100%' }} />
|
||||
) : (
|
||||
<div className="is-flex is-align-items-center is-justify-content-center" style={{ height: '100%' }}>
|
||||
<span className="material-symbols-outlined has-text-grey-light is-size-1">image</span>
|
||||
</div>
|
||||
)}
|
||||
</figure>
|
||||
</div>
|
||||
<div className="card-content is-flex-grow-1">
|
||||
<p className="title is-6 mb-2">{item.title}</p>
|
||||
<p className="subtitle is-6 has-text-primary has-text-weight-bold">₹{Number(item.price).toFixed(2)}</p>
|
||||
<div className="is-flex is-align-items-center is-size-7 has-text-grey">
|
||||
<span className="icon is-small mr-1"><span className="material-symbols-outlined is-size-7">inventory</span></span>
|
||||
<span>{item.stock} in stock</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="card-footer">
|
||||
<a href="#" className="card-footer-item has-text-grey" onClick={(e) => { e.preventDefault(); handleEditClick(item); setDashTab('add-product'); }}>Edit</a>
|
||||
<a href="#" className="card-footer-item has-text-danger" onClick={(e) => { e.preventDefault(); handleDeleteProduct(item.id); }}>Delete</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{products.length === 0 && (
|
||||
<div className="column is-12">
|
||||
<div className="notification is-light has-text-centered py-6">
|
||||
<p>No products available yet. Click "Add Product" or "Bulk Upload" to get started.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`modal ${showBulkUploadModal ? 'is-active' : ''}`}>
|
||||
<div className="modal-background" onClick={() => setShowBulkUploadModal(false)}></div>
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">Bulk Upload Products</p>
|
||||
<button className="delete" aria-label="close" onClick={() => setShowBulkUploadModal(false)}></button>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<form id="bulk-upload-form" onSubmit={handleBulkUploadSubmit}>
|
||||
<div className="field">
|
||||
<label className="label">Product Catalog (CSV)</label>
|
||||
<div className="control">
|
||||
<input className="input" type="file" accept=".csv" onChange={(e) => setBulkCsvFile(e.target.files?.[0] || null)} required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Images (ZIP) - Optional</label>
|
||||
<div className="control">
|
||||
<input className="input" type="file" accept=".zip" onChange={(e) => setBulkZipFile(e.target.files?.[0] || null)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Other Document (Optional)</label>
|
||||
<div className="control">
|
||||
<input className="input" type="file" onChange={(e) => {
|
||||
if (e.target.files?.[0]) {
|
||||
uploadDocument(e.target.files[0], 'document').then(() => {
|
||||
alert(`Document uploaded successfully`);
|
||||
}).catch(() => {
|
||||
alert(`Failed to upload document`);
|
||||
});
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
{bulkLog.length > 0 && (
|
||||
<div className="notification is-info is-light mt-4">
|
||||
<ul>
|
||||
{bulkLog.map((log: string, i: number) => <li key={i}>{log}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
<footer className="modal-card-foot">
|
||||
<button
|
||||
type="submit"
|
||||
form="bulk-upload-form"
|
||||
className={`button ${isParsingBulk ? 'is-loading' : ''}`}
|
||||
style={{ backgroundColor: 'transparent', borderColor: '#4d0218', color: '#4d0218' }}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
<button className="button" onClick={() => setShowBulkUploadModal(false)}>Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
458
src/components/dashboard/tabs/SettingsTab.tsx
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../../context/SellerContext';
|
||||
|
||||
export default function SettingsTab() {
|
||||
const {
|
||||
storeName, setStoreName,
|
||||
storeSlug,
|
||||
businessBio, setBusinessBio,
|
||||
supportEmail, setSupportEmail,
|
||||
supportPhone, setSupportPhone,
|
||||
saveProfileBackend,
|
||||
uploadDocument,
|
||||
// Verification
|
||||
email, phone,
|
||||
emailVerified, phoneVerified, setEmailVerified, setPhoneVerified,
|
||||
// Business Details
|
||||
gstin, setGstin, isGstinVerified, handleVerifyGstin,
|
||||
aadharFile, aadharS3Key, aadharUrl, setAadharS3Key, setAadharFile,
|
||||
panFile, panS3Key, panUrl, setPanS3Key, setPanFile,
|
||||
// Location
|
||||
address, setAddress
|
||||
} = useSeller();
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||
const [saveError, setSaveError] = useState('');
|
||||
|
||||
const [localAadhar, setLocalAadhar] = useState<File | null>(null);
|
||||
const [localAadharPreview, setLocalAadharPreview] = useState<string | null>(null);
|
||||
const [isUploadingAadhar, setIsUploadingAadhar] = useState(false);
|
||||
|
||||
const [localPan, setLocalPan] = useState<File | null>(null);
|
||||
const [localPanPreview, setLocalPanPreview] = useState<string | null>(null);
|
||||
const [isUploadingPan, setIsUploadingPan] = useState(false);
|
||||
|
||||
// OTP Verification local state
|
||||
const [emailOtpSentLocal, setEmailOtpSentLocal] = useState(false);
|
||||
const [phoneOtpSentLocal, setPhoneOtpSentLocal] = useState(false);
|
||||
const [enteredEmailOtpLocal, setEnteredEmailOtpLocal] = useState('');
|
||||
const [enteredPhoneOtpLocal, setEnteredPhoneOtpLocal] = useState('');
|
||||
|
||||
const handleSendEmailOtp = () => setEmailOtpSentLocal(true);
|
||||
const handleVerifyEmailOtp = () => {
|
||||
if (enteredEmailOtpLocal) {
|
||||
setEmailVerified(true);
|
||||
setEmailOtpSentLocal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendPhoneOtp = () => setPhoneOtpSentLocal(true);
|
||||
const handleVerifyPhoneOtp = () => {
|
||||
if (enteredPhoneOtpLocal) {
|
||||
setPhoneVerified(true);
|
||||
setPhoneOtpSentLocal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
setSaveSuccess(false);
|
||||
setSaveError('');
|
||||
try {
|
||||
await saveProfileBackend(true);
|
||||
setSaveSuccess(true);
|
||||
setTimeout(() => setSaveSuccess(false), 3000);
|
||||
} catch (err: any) {
|
||||
setSaveError(err.message || 'Failed to save settings.');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="columns is-centered">
|
||||
<div className="column is-10">
|
||||
<h1 className="title is-3 has-text-dark mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Settings</h1>
|
||||
|
||||
{saveSuccess && (
|
||||
<div className="notification is-success is-light mb-4">
|
||||
<button className="delete" onClick={() => setSaveSuccess(false)} />
|
||||
Settings saved successfully!
|
||||
</div>
|
||||
)}
|
||||
{saveError && (
|
||||
<div className="notification is-danger is-light mb-4">
|
||||
<button className="delete" onClick={() => setSaveError('')} />
|
||||
{saveError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSave}>
|
||||
{/* Verification Section */}
|
||||
<div className="box mb-5">
|
||||
<h2 className="title is-5 has-text-primary pb-2" style={{ borderBottom: '1px solid #eee' }}>Verification</h2>
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Registered Email</label>
|
||||
<div className="field has-addons">
|
||||
<div className="control is-expanded has-icons-right">
|
||||
<input className="input" type="email" value={email} readOnly />
|
||||
{emailVerified && (
|
||||
<span className="icon is-small is-right has-text-success">
|
||||
<span className="material-symbols-outlined">check_circle</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!emailVerified && !emailOtpSentLocal && (
|
||||
<div className="control">
|
||||
<button type="button" className="button is-info" onClick={handleSendEmailOtp}>
|
||||
Verify
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!emailVerified && !emailOtpSentLocal && <p className="help has-text-danger">Email not verified</p>}
|
||||
</div>
|
||||
{emailOtpSentLocal && (
|
||||
<div className="field has-addons mt-2">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Enter Email OTP"
|
||||
value={enteredEmailOtpLocal}
|
||||
onChange={(e) => setEnteredEmailOtpLocal(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button type="button" className="button is-success" onClick={handleVerifyEmailOtp}>
|
||||
Submit OTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Registered Phone</label>
|
||||
<div className="field has-addons">
|
||||
<div className="control is-expanded has-icons-right">
|
||||
<input className="input" type="tel" value={phone} readOnly />
|
||||
{phoneVerified && (
|
||||
<span className="icon is-small is-right has-text-success">
|
||||
<span className="material-symbols-outlined">check_circle</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!phoneVerified && !phoneOtpSentLocal && (
|
||||
<div className="control">
|
||||
<button type="button" className="button is-info" onClick={handleSendPhoneOtp}>
|
||||
Verify
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!phoneVerified && !phoneOtpSentLocal && <p className="help has-text-danger">Phone not verified</p>}
|
||||
</div>
|
||||
{phoneOtpSentLocal && (
|
||||
<div className="field has-addons mt-2">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="Enter Phone OTP"
|
||||
value={enteredPhoneOtpLocal}
|
||||
onChange={(e) => setEnteredPhoneOtpLocal(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button type="button" className="button is-success" onClick={handleVerifyPhoneOtp}>
|
||||
Submit OTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Store Profile Section */}
|
||||
<div className="box mb-5">
|
||||
<h2 className="title is-5 has-text-primary pb-2" style={{ borderBottom: '1px solid #eee' }}>Store Profile & Location</h2>
|
||||
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Store Name</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={storeName}
|
||||
onChange={e => setStoreName(e.target.value)}
|
||||
placeholder="Your store name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Store URL Slug</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" value={storeSlug} readOnly />
|
||||
</div>
|
||||
<p className="help">Contact support to change your store URL.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Store Description</label>
|
||||
<div className="control">
|
||||
<textarea
|
||||
className="textarea"
|
||||
placeholder="Tell buyers about your craft..."
|
||||
value={businessBio}
|
||||
onChange={e => setBusinessBio(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="columns mt-2">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Street Address</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={address.street}
|
||||
onChange={e => setAddress({...address, street: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">City</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={address.city}
|
||||
onChange={e => setAddress({...address, city: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">State</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={address.state}
|
||||
onChange={e => setAddress({...address, state: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Pincode</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={address.pincode}
|
||||
onChange={e => setAddress({...address, pincode: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Details / Legal */}
|
||||
<div className="box mb-5">
|
||||
<h2 className="title is-5 has-text-primary pb-2" style={{ borderBottom: '1px solid #eee' }}>Business Documents (Legal)</h2>
|
||||
<p className="has-text-grey-dark mb-4">Required for marketplace verification.</p>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">GSTIN</label>
|
||||
<div className="field has-addons">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={gstin}
|
||||
onChange={e => setGstin(e.target.value)}
|
||||
placeholder="Enter 15-digit GSTIN"
|
||||
/>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button type="button" className="button is-info" onClick={handleVerifyGstin}>
|
||||
Verify
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isGstinVerified && <p className="help has-text-success">GSTIN Verified ✓</p>}
|
||||
</div>
|
||||
|
||||
<div className="columns mt-4">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">Aadhar Document</label>
|
||||
<div className="control">
|
||||
<div className="file has-name is-fullwidth">
|
||||
<label className="file-label">
|
||||
<input className="file-input" type="file" onChange={e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setLocalAadhar(file);
|
||||
setLocalAadharPreview(URL.createObjectURL(file));
|
||||
}
|
||||
}} />
|
||||
<span className="file-cta">
|
||||
<span className="file-icon"><span className="material-symbols-outlined">upload</span></span>
|
||||
<span className="file-label">Choose Aadhar...</span>
|
||||
</span>
|
||||
<span className="file-name">{localAadhar ? localAadhar.name : (aadharFile || aadharS3Key || 'No file uploaded')}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/* Add Preview Link Below */}
|
||||
{localAadhar ? (
|
||||
<div className="mt-2 is-flex is-align-items-center" style={{ gap: '10px' }}>
|
||||
<a href={localAadharPreview!} target="_blank" rel="noopener noreferrer" className="button is-small is-info is-light">
|
||||
<span className="icon is-small"><i className="material-symbols-outlined" style={{ fontSize: '14px' }}>visibility</i></span>
|
||||
<span>Preview Selected</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className={`button is-small is-primary ${isUploadingAadhar ? 'is-loading' : ''}`}
|
||||
onClick={async () => {
|
||||
setIsUploadingAadhar(true);
|
||||
const s3Key = await uploadDocument(localAadhar, 'aadhar');
|
||||
setAadharS3Key(s3Key);
|
||||
setAadharFile(localAadhar.name);
|
||||
setLocalAadhar(null);
|
||||
setLocalAadharPreview(null);
|
||||
setIsUploadingAadhar(false);
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button is-small is-danger is-light"
|
||||
onClick={() => {
|
||||
setLocalAadhar(null);
|
||||
setLocalAadharPreview(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
aadharUrl && (
|
||||
<p className="help mt-2">
|
||||
<a href={aadharUrl} target="_blank" rel="noopener noreferrer">
|
||||
<span className="icon is-small"><i className="material-symbols-outlined" style={{ fontSize: '14px' }}>visibility</i></span>
|
||||
Preview Uploaded Aadhar
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label">PAN Document</label>
|
||||
<div className="control">
|
||||
<div className="file has-name is-fullwidth">
|
||||
<label className="file-label">
|
||||
<input className="file-input" type="file" onChange={e => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setLocalPan(file);
|
||||
setLocalPanPreview(URL.createObjectURL(file));
|
||||
}
|
||||
}} />
|
||||
<span className="file-cta">
|
||||
<span className="file-icon"><span className="material-symbols-outlined">upload</span></span>
|
||||
<span className="file-label">Choose PAN...</span>
|
||||
</span>
|
||||
<span className="file-name">{localPan ? localPan.name : (panFile || panS3Key || 'No file uploaded')}</span>
|
||||
</label>
|
||||
</div>
|
||||
{/* Add Preview Link Below */}
|
||||
{localPan ? (
|
||||
<div className="mt-2 is-flex is-align-items-center" style={{ gap: '10px' }}>
|
||||
<a href={localPanPreview!} target="_blank" rel="noopener noreferrer" className="button is-small is-info is-light">
|
||||
<span className="icon is-small"><i className="material-symbols-outlined" style={{ fontSize: '14px' }}>visibility</i></span>
|
||||
<span>Preview Selected</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className={`button is-small is-primary ${isUploadingPan ? 'is-loading' : ''}`}
|
||||
onClick={async () => {
|
||||
setIsUploadingPan(true);
|
||||
const s3Key = await uploadDocument(localPan, 'pan');
|
||||
setPanS3Key(s3Key);
|
||||
setPanFile(localPan.name);
|
||||
setLocalPan(null);
|
||||
setLocalPanPreview(null);
|
||||
setIsUploadingPan(false);
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button is-small is-danger is-light"
|
||||
onClick={() => {
|
||||
setLocalPan(null);
|
||||
setLocalPanPreview(null);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
panUrl && (
|
||||
<p className="help mt-2">
|
||||
<a href={panUrl} target="_blank" rel="noopener noreferrer">
|
||||
<span className="icon is-small"><i className="material-symbols-outlined" style={{ fontSize: '14px' }}>visibility</i></span>
|
||||
Preview Uploaded PAN
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="is-flex is-justify-content-flex-end">
|
||||
<button
|
||||
type="submit"
|
||||
className={`button is-primary is-outlined is-medium ${isSaving ? 'is-loading' : ''}`}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Save Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
src/components/layout/DashboardHeader.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
export default function DashboardHeader() {
|
||||
const { handleLogout, storeSlug } = useSeller();
|
||||
|
||||
return (
|
||||
<nav className="navbar has-background-white" style={{ borderBottom: '1px solid #e2dfdb', position: 'sticky', top: 0, zIndex: 30 }}>
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#">
|
||||
<h1 className="title is-4 has-text-primary" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||
Tradhox
|
||||
</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end pr-5">
|
||||
<div className="navbar-item has-dropdown is-hoverable">
|
||||
<a className="navbar-link is-arrowless is-flex is-align-items-center">
|
||||
<span className="icon has-text-grey mr-3">
|
||||
<span className="material-symbols-outlined">notifications</span>
|
||||
</span>
|
||||
<div className="is-flex is-align-items-center">
|
||||
<div style={{ width: '32px', height: '32px', borderRadius: '50%', backgroundColor: 'var(--bulma-primary)', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold' }}>
|
||||
{storeSlug ? storeSlug.charAt(0).toUpperCase() : 'S'}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div className="navbar-dropdown is-right">
|
||||
<a className="navbar-item">
|
||||
<span className="icon mr-2"><span className="material-symbols-outlined">account_circle</span></span>
|
||||
Profile
|
||||
</a>
|
||||
<hr className="navbar-divider" />
|
||||
<a className="navbar-item has-text-danger" onClick={handleLogout}>
|
||||
<span className="icon mr-2"><span className="material-symbols-outlined">logout</span></span>
|
||||
Log Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
38
src/components/layout/Footer.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer has-background-white-bis" style={{ borderTop: '1px solid #dac0c2' }}>
|
||||
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||
<div className="columns">
|
||||
{/* Brand & Copyright */}
|
||||
<div className="column is-4">
|
||||
<h2 className="title is-4 has-text-primary mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Tradhox</h2>
|
||||
<p className="has-text-grey-dark">
|
||||
© 2026 Tradhox. Honoring Human Craftsmanship.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links 1 */}
|
||||
<div className="column is-4">
|
||||
<ul style={{ listStyle: 'none', marginLeft: 0 }}>
|
||||
<li className="mb-2"><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Privacy Policy</a></li>
|
||||
<li><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Terms of Service</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Links 2 */}
|
||||
<div className="column is-4">
|
||||
<ul style={{ listStyle: 'none', marginLeft: 0 }}>
|
||||
<li className="mb-2"><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Sustainability Report</a></li>
|
||||
<li><a href="#" className="has-text-grey-dark hover-text-primary" style={{ transition: 'color 0.2s' }}>Shipping Info</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
.hover-text-primary:hover { color: var(--bulma-primary) !important; }
|
||||
`}</style>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
88
src/components/layout/Header.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
export default function Header() {
|
||||
const { currentPage, setCurrentPage, isMobileMenuOpen, setIsMobileMenuOpen } = useSeller();
|
||||
|
||||
return (
|
||||
<nav className="navbar has-background-white-bis" style={{ borderBottom: '1px solid #dac0c2' }} role="navigation" aria-label="main navigation">
|
||||
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item is-size-4 has-text-primary" style={{ fontFamily: 'Libre Caslon Text, serif', letterSpacing: '-0.02em', fontWeight: 700 }} onClick={() => setCurrentPage('home')}>
|
||||
Tradhox
|
||||
</a>
|
||||
|
||||
<a role="button" className={`navbar-burger ${isMobileMenuOpen ? 'is-active' : ''}`} aria-label="menu" aria-expanded="false" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} style={{ color: 'var(--bulma-primary)' }}>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className={`navbar-menu ${isMobileMenuOpen ? 'is-active' : ''}`} style={{ backgroundColor: 'transparent' }}>
|
||||
<div className="navbar-start" style={{ margin: '0 auto', gap: '2rem' }}>
|
||||
<a
|
||||
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||
onClick={() => setCurrentPage('home')}
|
||||
style={{ color: currentPage === 'home' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||
>
|
||||
Shop
|
||||
</a>
|
||||
|
||||
<a
|
||||
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||
onClick={() => setCurrentPage('about')}
|
||||
style={{ color: currentPage === 'about' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||
>
|
||||
Our Story
|
||||
</a>
|
||||
<a
|
||||
className="navbar-item has-text-weight-medium custom-nav-link"
|
||||
onClick={() => setCurrentPage('contact')}
|
||||
style={{ color: currentPage === 'contact' ? 'var(--bulma-primary)' : '#5f5e5b' }}
|
||||
>
|
||||
Contact Us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="buttons">
|
||||
<a className="button is-ghost has-text-weight-semibold" style={{ color: '#5f5e5b' }} onClick={() => setCurrentPage('login')}>
|
||||
Login
|
||||
</a>
|
||||
<a className="button is-primary is-outlined has-text-weight-semibold" onClick={() => setCurrentPage('signup')}>
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.custom-nav-link {
|
||||
position: relative;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.custom-nav-link:hover {
|
||||
color: var(--bulma-primary) !important;
|
||||
}
|
||||
.custom-nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background-color: var(--bulma-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.custom-nav-link:hover::after {
|
||||
width: 80%;
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
38
src/components/layout/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
export default function Sidebar() {
|
||||
const { dashTab, setDashTab } = useSeller();
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'overview', icon: 'grid_view', label: 'Overview' },
|
||||
{ id: 'orders', icon: 'local_shipping', label: 'Orders' },
|
||||
{ id: 'products', icon: 'inventory_2', label: 'Products' },
|
||||
{ id: 'payments', icon: 'account_balance_wallet', label: 'Payments & Earnings' },
|
||||
{ id: 'settings', icon: 'settings', label: 'Settings' }
|
||||
];
|
||||
|
||||
return (
|
||||
<aside className="menu p-5" style={{ position: 'sticky', top: '4rem' }}>
|
||||
<p className="menu-label has-text-weight-bold tracking-wide" style={{ color: 'var(--bulma-grey)', letterSpacing: '0.1em' }}>
|
||||
Seller Central
|
||||
</p>
|
||||
<ul className="menu-list">
|
||||
{menuItems.map(item => (
|
||||
<li key={item.id} className="mb-2">
|
||||
<a
|
||||
className={`is-flex is-align-items-center py-3 px-4 ${dashTab === item.id ? 'has-background-primary-light has-text-primary has-text-weight-bold' : 'has-text-grey-dark'}`}
|
||||
style={{ borderRadius: '8px', transition: 'all 0.2s ease' }}
|
||||
onClick={() => setDashTab(item.id)}
|
||||
>
|
||||
<span className="icon mr-3">
|
||||
<span className="material-symbols-outlined is-size-5">{item.icon}</span>
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
98
src/components/pages/AboutPage.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
export default function AboutPage() {
|
||||
const { setCurrentPage } = useSeller();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Hero Section */}
|
||||
<section className="hero is-large" style={{ position: 'relative', overflow: 'hidden', minHeight: '800px' }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
||||
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuA5mjm6T9CY7j2AtbFE_jEFFkhOzCmc_eKwvgj0hHVKV6AoVKeAf8ZfBnnz4sSYkdzX5gyzlk-pGufTtwpJN5MhYgDAIezV1yaDN8ANw66nZ5kk40-GGDiVWp3eGiPJZ4mIuL_aJp6O1TqJsQihFvPZyfj5VgjH4IQivXEltdrAAUfKSMdbWgNHLi3SP2X5uLF6Xc_gDyYs-tOepA8IuU6_1Q9wcaSebXAQOQhAQAPW7wVcUFGY7crerw')",
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
zIndex: 0
|
||||
}}>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to right, rgba(251, 249, 248, 0.9), rgba(251, 249, 248, 0.4))' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="hero-body" style={{ position: 'relative', zIndex: 10 }}>
|
||||
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||
<div className="columns is-vcentered">
|
||||
<div className="column is-6">
|
||||
<p className="subtitle is-6 has-text-primary is-uppercase has-text-weight-bold tracking-widest mb-4" style={{ letterSpacing: '0.2em' }}>
|
||||
Honoring Human Craftsmanship
|
||||
</p>
|
||||
<h1 className="title has-text-dark mb-5" style={{ fontSize: '3.5rem', fontFamily: 'Libre Caslon Text, serif' }}>
|
||||
Preserving Heritage, Inspiring the Modern World.
|
||||
</h1>
|
||||
<p className="subtitle is-5 has-text-grey-dark mb-6" style={{ lineHeight: '1.6' }}>
|
||||
Discover authentic, handcrafted masterworks from generations of skilled artisans. Every piece tells a story of culture, resilience, and unparalleled skill.
|
||||
</p>
|
||||
<button className="button is-primary is-medium px-6 is-outlined" onClick={() => setCurrentPage('home')}>
|
||||
<span className="has-text-weight-bold">Explore the Marketplace</span>
|
||||
<span className="icon ml-2"><span className="material-symbols-outlined">arrow_forward</span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Tradhox Section */}
|
||||
<section className="section py-6 has-background-white-bis">
|
||||
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||
<div className="has-text-centered mb-6" style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h2 className="title is-2 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Why Choose Tradhox</h2>
|
||||
<p className="subtitle is-5 has-text-grey-dark">
|
||||
We bridge the gap between discerning collectors and master artisans, ensuring fairness, authenticity, and enduring quality.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="columns is-multiline mt-5">
|
||||
{/* Feature 1 */}
|
||||
<div className="column is-4-tablet">
|
||||
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>verified</span>
|
||||
</span>
|
||||
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Authenticity Guaranteed</h3>
|
||||
<p className="has-text-grey-dark">Direct sourcing from verified artisanal clusters ensures every piece is genuine and ethically procured.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature 2 */}
|
||||
<div className="column is-4-tablet">
|
||||
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>handshake</span>
|
||||
</span>
|
||||
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Fair Trade Promise</h3>
|
||||
<p className="has-text-grey-dark">We empower makers by providing direct market access, ensuring they receive equitable compensation for their craft.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature 3 */}
|
||||
<div className="column is-4-tablet">
|
||||
<div className="box has-text-centered h-100 feature-card" style={{ padding: '2rem', height: '100%', border: '1px solid #dac0c2', backgroundColor: '#ffffff', boxShadow: 'none', transition: 'box-shadow 0.3s' }}>
|
||||
<span className="icon is-large has-text-primary mb-4" style={{ backgroundColor: '#f5f3f3', borderRadius: '50%', width: '64px', height: '64px' }}>
|
||||
<span className="material-symbols-outlined is-size-3" style={{ fontVariationSettings: "'FILL' 1" }}>eco</span>
|
||||
</span>
|
||||
<h3 className="title is-4 mb-3" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Sustainable Heritage</h3>
|
||||
<p className="has-text-grey-dark">Supporting traditional techniques that inherently respect natural materials and sustainable production cycles.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>{`
|
||||
.feature-card:hover {
|
||||
box-shadow: 0 4px 20px rgba(107, 26, 44, 0.05) !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
240
src/components/pages/AuthForms.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
const ARTISAN_IMAGE = "https://lh3.googleusercontent.com/aida-public/AB6AXuAAFsGHQxH4paIeyYZq8omfEOk4T6ZEJqZYRodaOcQHD0LtVHFHgCIbibIROjn374aCnXLV8lMu_a4PAiB2hSlSCweYGsHwS5Yo_qSy1nNAZPLlYCBJAm4rGsYoeyu_UDpTNR8eQjl_nUnYEjlNe_bS3LYk8VhaJnlE2uy_LsQmUuW-JWNYEqV9vbFeeG1VBgLjqUD7HixjvwxkWx9prT2WTwoTN0fOnHqxndcVNs1jPMAkFLc2y9082g";
|
||||
const SIGNUP_IMAGE = "https://lh3.googleusercontent.com/aida-public/AB6AXuCltIMM8YbcYMs4QPFOFVNm_ieJNxla5TaKcrFw3or4efQU-wz4mZtq2sWHFvLVv-DN1XfSlcJQUW2BCVrNiTUFnNtUoxVqPqGYiuWoaGFd_GvpHLctklxC9UMMOZOEpSQqEymQUBvpKbz6TNLgcR12JSCy4JZc3VOPKEW4xFquW6T1dSTrTJWcdo7ONH70XsdK4FT6FVZJIkAlD_f4b5iJufeGHaYGTqm4Y7OgBEorWRTGrciNeKxxDA";
|
||||
|
||||
const P = '#6b1a2c';
|
||||
const P_DARK = '#4d0218';
|
||||
const BORDER = '#D9C5B2';
|
||||
const BG = '#fbf9f8';
|
||||
const MUTED = '#554244';
|
||||
|
||||
export default function AuthForms() {
|
||||
const {
|
||||
currentPage, navigateTo,
|
||||
activeTab, setActiveTab,
|
||||
email, setEmail,
|
||||
password, setPassword,
|
||||
confirmPassword, setConfirmPassword,
|
||||
phone, setPhone,
|
||||
policyAccepted, setPolicyAccepted,
|
||||
handleLoginSubmit, handleRegisterSubmit,
|
||||
showLoginPass, setShowLoginPass,
|
||||
showSignupPass, setShowSignupPass,
|
||||
} = useSeller();
|
||||
|
||||
const isLogin = currentPage === 'login';
|
||||
const [focused, setFocused] = useState('');
|
||||
|
||||
const inp = (field: string): React.CSSProperties => ({
|
||||
width: '100%', background: '#fff', border: `1px solid ${focused === field ? P : BORDER}`,
|
||||
boxShadow: focused === field ? `0 0 0 1px ${P}` : 'none',
|
||||
borderRadius: '4px', padding: '12px 16px',
|
||||
fontFamily: 'Work Sans, sans-serif', fontSize: '16px', color: '#1b1c1c', outline: 'none',
|
||||
transition: 'border-color 0.2s',
|
||||
});
|
||||
|
||||
const lbl: React.CSSProperties = {
|
||||
display: 'block', fontFamily: 'Work Sans, sans-serif', fontSize: '12px',
|
||||
fontWeight: 600, letterSpacing: '0.1em', textTransform: 'uppercase', color: MUTED, marginBottom: '6px',
|
||||
};
|
||||
|
||||
const btn: React.CSSProperties = {
|
||||
width: '100%', background: P, color: '#fff', border: 'none', borderRadius: '4px',
|
||||
padding: '12px 16px', fontFamily: 'Work Sans, sans-serif', fontSize: '14px', fontWeight: 600,
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
gap: '8px', marginTop: '8px',
|
||||
};
|
||||
|
||||
const switchPage = (tab: 'login' | 'register') => {
|
||||
setActiveTab(tab);
|
||||
navigateTo(tab === 'login' ? 'login' : 'signup');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: BG, display: 'flex', flexDirection: 'column' }}>
|
||||
<header style={{ padding: '20px 32px', borderBottom: '1px solid #e4e2e2', display: 'flex', justifyContent: 'center' }}>
|
||||
<a href="#" onClick={e => { e.preventDefault(); navigateTo('home'); }}
|
||||
style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '26px', fontWeight: 700, color: P_DARK, textDecoration: 'none' }}>
|
||||
Tradhox
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main style={{ flexGrow: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '48px 24px' }}>
|
||||
<div style={{
|
||||
width: '100%', maxWidth: '900px', display: 'grid', gridTemplateColumns: '1fr 1fr',
|
||||
background: '#fff', borderRadius: '12px', border: `1px solid ${BORDER}`,
|
||||
boxShadow: '0 4px 20px rgba(107,26,44,0.07)', overflow: 'hidden',
|
||||
}}>
|
||||
|
||||
{/* LEFT: FORM */}
|
||||
<div style={{ padding: '48px 44px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||
{isLogin ? (
|
||||
<>
|
||||
<h1 style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '26px', fontWeight: 600, color: '#1b1c1c', marginBottom: '6px' }}>Sign In</h1>
|
||||
<p style={{ fontFamily: 'Work Sans, sans-serif', fontSize: '15px', color: MUTED, marginBottom: '28px' }}>
|
||||
Access your artisan marketplace account.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleLoginSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={lbl} htmlFor="l-email">Email or Phone</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span className="material-symbols-outlined" style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: MUTED, fontSize: '20px', pointerEvents: 'none' }}>mail</span>
|
||||
<input id="l-email" type="text" placeholder="you@example.com" value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
onFocus={() => setFocused('l-email')} onBlur={() => setFocused('')}
|
||||
style={{ ...inp('l-email'), paddingLeft: '42px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '6px' }}>
|
||||
<label style={{ ...lbl, marginBottom: 0 }} htmlFor="l-pass">Password</label>
|
||||
<a href="#" style={{ fontSize: '13px', color: P, fontFamily: 'Work Sans, sans-serif', textDecoration: 'none' }}>Forgot Password?</a>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<span className="material-symbols-outlined" style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: MUTED, fontSize: '20px', pointerEvents: 'none' }}>lock</span>
|
||||
<input id="l-pass" type={showLoginPass ? 'text' : 'password'} placeholder="••••••••" value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onFocus={() => setFocused('l-pass')} onBlur={() => setFocused('')}
|
||||
style={{ ...inp('l-pass'), paddingLeft: '42px', paddingRight: '42px' }} />
|
||||
<button type="button" onClick={() => setShowLoginPass(!showLoginPass)}
|
||||
style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: MUTED, display: 'flex' }}>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>{showLoginPass ? 'visibility' : 'visibility_off'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="login-submit" style={btn}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = P_DARK)}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = P)}>
|
||||
Sign In <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>arrow_forward</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', margin: '20px 0' }}>
|
||||
<div style={{ flexGrow: 1, borderTop: `1px solid ${BORDER}` }} />
|
||||
<span style={{ padding: '0 12px', color: MUTED, fontSize: '13px', fontFamily: 'Work Sans, sans-serif' }}>or</span>
|
||||
<div style={{ flexGrow: 1, borderTop: `1px solid ${BORDER}` }} />
|
||||
</div>
|
||||
|
||||
<p style={{ textAlign: 'center', fontFamily: 'Work Sans, sans-serif', fontSize: '15px', color: MUTED }}>
|
||||
New to Tradhox?{' '}
|
||||
<a href="#" onClick={e => { e.preventDefault(); switchPage('register'); }}
|
||||
style={{ color: P, fontWeight: 600, textDecoration: 'none' }}>Create an account</a>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<a href="#" onClick={e => { e.preventDefault(); navigateTo('home'); }}
|
||||
style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '20px', fontWeight: 700, color: P_DARK, textDecoration: 'none', display: 'inline-block', marginBottom: '10px' }}>
|
||||
Tradhox
|
||||
</a>
|
||||
<h1 style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '28px', fontWeight: 700, color: '#1b1c1c', marginBottom: '6px' }}>Create an Account</h1>
|
||||
<p style={{ fontFamily: 'Work Sans, sans-serif', fontSize: '15px', color: MUTED, marginBottom: '24px' }}>
|
||||
Join our community to preserve and support cultural craftsmanship.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleRegisterSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<div>
|
||||
<label style={lbl} htmlFor="r-email">Email Address</label>
|
||||
<input id="r-email" type="email" placeholder="Enter your email" value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
onFocus={() => setFocused('r-email')} onBlur={() => setFocused('')}
|
||||
style={inp('r-email')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={lbl} htmlFor="r-phone">Phone Number</label>
|
||||
<input id="r-phone" type="tel" placeholder="e.g. 9876543210" value={phone}
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
onFocus={() => setFocused('r-phone')} onBlur={() => setFocused('')}
|
||||
style={inp('r-phone')} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={lbl} htmlFor="r-pass">Password</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input id="r-pass" type={showSignupPass ? 'text' : 'password'} placeholder="Create a password" value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onFocus={() => setFocused('r-pass')} onBlur={() => setFocused('')}
|
||||
style={{ ...inp('r-pass'), paddingRight: '42px' }} />
|
||||
<button type="button" onClick={() => setShowSignupPass(!showSignupPass)}
|
||||
style={{ position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', background: 'none', border: 'none', cursor: 'pointer', color: MUTED, display: 'flex' }}>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '20px' }}>{showSignupPass ? 'visibility' : 'visibility_off'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={lbl} htmlFor="r-confirm">Confirm Password</label>
|
||||
<input id="r-confirm" type="password" placeholder="Repeat your password" value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
onFocus={() => setFocused('r-confirm')} onBlur={() => setFocused('')}
|
||||
style={inp('r-confirm')} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px' }}>
|
||||
<input type="checkbox" id="terms" checked={policyAccepted} onChange={e => setPolicyAccepted(e.target.checked)}
|
||||
style={{ marginTop: '3px', accentColor: P, cursor: 'pointer' }} />
|
||||
<label htmlFor="terms" style={{ fontFamily: 'Work Sans, sans-serif', fontSize: '13px', color: MUTED, cursor: 'pointer' }}>
|
||||
I agree to the{' '}<a href="#" style={{ color: P, textDecoration: 'none' }}>Terms of Service</a>{' '}and{' '}<a href="#" style={{ color: P, textDecoration: 'none' }}>Privacy Policy</a>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="signup-submit" style={btn}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = P_DARK)}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = P)}>
|
||||
Create Account <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>arrow_forward</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p style={{ marginTop: '20px', textAlign: 'center', fontFamily: 'Work Sans, sans-serif', fontSize: '15px', color: MUTED }}>
|
||||
Already have an account?{' '}
|
||||
<a href="#" onClick={e => { e.preventDefault(); switchPage('login'); }}
|
||||
style={{ color: P, fontWeight: 600, textDecoration: 'none' }}>Sign In</a>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* RIGHT: IMAGE */}
|
||||
<div style={{ position: 'relative', minHeight: '500px', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: `url('${isLogin ? ARTISAN_IMAGE : SIGNUP_IMAGE}')`,
|
||||
backgroundSize: 'cover', backgroundPosition: 'center',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'linear-gradient(to top, rgba(77,2,24,0.88) 0%, rgba(77,2,24,0.25) 55%, transparent 100%)',
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', padding: '40px',
|
||||
}}>
|
||||
{isLogin ? (
|
||||
<>
|
||||
<h2 style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '26px', fontWeight: 700, color: '#fff', marginBottom: '8px' }}>
|
||||
Welcome to Heritage
|
||||
</h2>
|
||||
<p style={{ fontFamily: 'Work Sans, sans-serif', fontSize: '15px', color: 'rgba(255,178,187,0.9)' }}>
|
||||
Preserving cultural craftsmanship, one artisan at a time.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ background: 'rgba(255,255,255,0.1)', backdropFilter: 'blur(8px)', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', padding: '24px' }}>
|
||||
<span className="material-symbols-outlined" style={{ fontSize: '36px', color: '#fff', marginBottom: '12px', display: 'block', fontVariationSettings: "'FILL' 1" }}>eco</span>
|
||||
<blockquote style={{ fontFamily: 'Libre Caslon Text, serif', fontSize: '17px', fontWeight: 600, color: '#fff', marginBottom: '10px', fontStyle: 'italic', lineHeight: 1.5 }}>
|
||||
"Preserving our cultural heritage, one stitch at a time. Tradhox connects true artisans with a world that values authenticity."
|
||||
</blockquote>
|
||||
<p style={{ fontFamily: 'Work Sans, sans-serif', fontSize: '14px', color: 'rgba(255,255,255,0.7)' }}>— The Makers Story</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
src/components/pages/ContactPage.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<div className="container" style={{ maxWidth: '1120px', padding: '4rem 1.5rem' }}>
|
||||
{/* Header Section */}
|
||||
<div className="mb-6" style={{ maxWidth: '800px' }}>
|
||||
<h1 className="title is-1 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Get in Touch</h1>
|
||||
<p className="subtitle is-4 has-text-grey-dark" style={{ lineHeight: '1.6' }}>
|
||||
Whether you have a question about an artisanal piece, a partnership inquiry, or need support with your order, we are here to help. Reach out to the Tradhox team.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<div className="columns is-multiline mb-6">
|
||||
{/* Left Side: Contact Details */}
|
||||
<div className="column is-4">
|
||||
<div className="box mb-5" style={{ border: '1px solid #dac0c2', boxShadow: 'none' }}>
|
||||
<h2 className="title is-4 has-text-primary mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Contact Information</h2>
|
||||
|
||||
<div className="is-flex mb-4">
|
||||
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">location_on</span></span>
|
||||
<div>
|
||||
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Headquarters</h3>
|
||||
<p className="has-text-dark">12 Craft Heritage Lane,<br/>Dashashwamedh Ghat Area,<br/>Varanasi, UP 221001, India</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="is-flex mb-4">
|
||||
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">mail</span></span>
|
||||
<div>
|
||||
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Email Us</h3>
|
||||
<a href="mailto:namaste@tradhox.com" className="has-text-primary has-text-weight-semibold">namaste@tradhox.com</a>
|
||||
<p className="is-size-7 has-text-grey mt-1">We aim to reply within 24 hours.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="is-flex">
|
||||
<span className="icon has-text-primary mr-3 mt-1"><span className="material-symbols-outlined">call</span></span>
|
||||
<div>
|
||||
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-1" style={{ letterSpacing: '0.1em' }}>Call Us</h3>
|
||||
<a href="tel:+918005550199" className="has-text-dark has-text-weight-semibold hover-text-primary">+91 800 555 0199</a>
|
||||
<p className="is-size-7 has-text-grey mt-1">Mon-Fri, 9am - 6pm IST</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links Card */}
|
||||
<div className="box" style={{ border: '1px solid #dac0c2', boxShadow: 'none' }}>
|
||||
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-3" style={{ letterSpacing: '0.1em' }}>Connect With Us</h3>
|
||||
<div className="is-flex gap-2">
|
||||
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border mr-2">
|
||||
<span className="icon"><span className="material-symbols-outlined">public</span></span>
|
||||
</a>
|
||||
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border mr-2">
|
||||
<span className="icon"><span className="material-symbols-outlined">share</span></span>
|
||||
</a>
|
||||
<a href="#" className="button is-rounded is-outlined border-grey has-text-dark hover-primary-border">
|
||||
<span className="icon"><span className="material-symbols-outlined">groups</span></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Inquiry Form */}
|
||||
<div className="column is-8">
|
||||
<div className="box h-100" style={{ border: '1px solid #dac0c2', boxShadow: 'none', padding: '2.5rem' }}>
|
||||
<h2 className="title is-4 has-text-primary mb-5" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Send an Inquiry</h2>
|
||||
|
||||
<form onSubmit={(e) => { e.preventDefault(); alert("Message sent successfully"); }}>
|
||||
<div className="columns is-multiline">
|
||||
<div className="column is-6 pb-0">
|
||||
<div className="field">
|
||||
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Full Name</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text" placeholder="e.g. Ananya Sharma" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column is-6 pb-0">
|
||||
<div className="field">
|
||||
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Email Address</label>
|
||||
<div className="control">
|
||||
<input className="input" type="email" placeholder="e.g. ananya@example.com" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column is-12 pb-0">
|
||||
<div className="field">
|
||||
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Subject of Inquiry</label>
|
||||
<div className="control">
|
||||
<div className="select is-fullwidth">
|
||||
<select style={{ borderColor: '#dac0c2', boxShadow: 'none' }}>
|
||||
<option value="customer_support">Customer Support (Order Issues, Returns)</option>
|
||||
<option value="seller_inquiry">Seller/Artisan Inquiry (Join Tradhox)</option>
|
||||
<option value="partnership">Business Partnership / B2B Bulk Orders</option>
|
||||
<option value="general">General Question</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column is-12 pb-0">
|
||||
<div className="field">
|
||||
<label className="label is-size-7 has-text-grey has-text-weight-bold is-uppercase" style={{ letterSpacing: '0.1em' }}>Message</label>
|
||||
<div className="control">
|
||||
<textarea className="textarea has-fixed-size" rows={5} placeholder="How can we assist you today?" required style={{ borderColor: '#dac0c2', boxShadow: 'none' }}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column is-12 mt-3">
|
||||
<button type="submit" className="button is-primary is-medium px-6 is-outlined">
|
||||
<span className="has-text-weight-bold">Send Message</span>
|
||||
<span className="icon ml-2"><span className="material-symbols-outlined is-size-5">arrow_forward</span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visit Our Studio Section */}
|
||||
<div className="box p-0 overflow-hidden" style={{ border: '1px solid #dac0c2', boxShadow: 'none', borderRadius: '12px' }}>
|
||||
<div className="columns is-gapless mb-0">
|
||||
<div className="column is-6 is-flex is-flex-direction-column is-justify-content-center p-6 has-background-white-bis">
|
||||
<h2 className="title is-3 has-text-primary mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Visit Our Studio</h2>
|
||||
<p className="has-text-grey-dark mb-5" style={{ lineHeight: '1.6' }}>
|
||||
Experience the craftsmanship firsthand. Our Varanasi studio is open to visitors who wish to learn about our sourcing process and see exclusive GI-tagged products before they launch online.
|
||||
</p>
|
||||
|
||||
<div className="box" style={{ border: '1px solid #dac0c2', boxShadow: 'none', alignSelf: 'flex-start' }}>
|
||||
<h3 className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mb-3" style={{ letterSpacing: '0.1em' }}>Studio Hours</h3>
|
||||
<ul className="has-text-dark">
|
||||
<li className="is-flex is-justify-content-space-between mb-2" style={{ width: '220px' }}><span>Monday - Friday:</span> <span className="has-text-weight-semibold">10:00 - 18:00</span></li>
|
||||
<li className="is-flex is-justify-content-space-between mb-2" style={{ width: '220px' }}><span>Saturday:</span> <span className="has-text-weight-semibold">10:00 - 14:00</span></li>
|
||||
<li className="is-flex is-justify-content-space-between" style={{ width: '220px' }}><span className="has-text-grey">Sunday:</span> <span className="has-text-grey has-text-weight-semibold">Closed</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column is-6 is-flex is-align-items-center is-justify-content-center" style={{ backgroundColor: '#e4e2e2', borderLeft: '1px solid #dac0c2', minHeight: '350px', position: 'relative' }}>
|
||||
<div className="has-text-centered p-5" style={{ zIndex: 1 }}>
|
||||
<span className="icon is-large has-text-grey mb-2"><span className="material-symbols-outlined" style={{ fontSize: '48px' }}>map</span></span>
|
||||
<p className="is-size-7 has-text-grey has-text-weight-bold is-uppercase mt-3" style={{ letterSpacing: '0.1em' }}>Interactive Map view of Varanasi Studio</p>
|
||||
</div>
|
||||
{/* Dot Pattern Overlay */}
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.1, backgroundImage: 'radial-gradient(#887274 1.5px, transparent 1.5px)', backgroundSize: '24px 24px' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.hover-text-primary:hover { color: var(--bulma-primary) !important; }
|
||||
.hover-primary-border:hover { border-color: var(--bulma-primary) !important; color: var(--bulma-primary) !important; }
|
||||
.border-grey { border-color: #dac0c2; }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
113
src/components/pages/LandingPage.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
export default function LandingPage() {
|
||||
const { setCurrentPage } = useSeller();
|
||||
|
||||
const handleSellerLogin = () => {
|
||||
setCurrentPage('login');
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Hero Section */}
|
||||
<section className="hero is-large has-background-dark" style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: '600px'
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, right: 0, bottom: 0,
|
||||
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuCWM3QRK9NLvhVDgb1yr3ZPMxI4YJ2zEvrECk8pbXQ8kTCi9EQFYsJchNz2sfzWd8Gb5vdmZPyg83MZtWRG3ZzVG-xVBVALPGm_LAPbgxH4YJTprM-xhBDoG_dlzo7T9v5nOXnRF_MibxuJ9ct3JBgbQNzN9Fa57uY0gfjGHEk_qSZLWBiZQxxTgtjEZtGqnCXCZ1kno2Gn9gnYQHgmfY3_PVnjWnUKk-F2ZLmIIl7F3AlCv3B7U6lXLg')",
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
zIndex: 0
|
||||
}}>
|
||||
<div style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.4)' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="hero-body is-flex is-align-items-center is-justify-content-center" style={{ position: 'relative', zIndex: 10 }}>
|
||||
<div className="container has-text-centered" style={{ maxWidth: '800px' }}>
|
||||
<p className="subtitle is-6 has-text-grey-light is-uppercase has-text-weight-bold tracking-widest mb-4" style={{ letterSpacing: '0.2em' }}>
|
||||
Seasonal Collection
|
||||
</p>
|
||||
<h1 className="title has-text-white mb-5" style={{ fontSize: '3.5rem', fontFamily: 'Libre Caslon Text, serif', textShadow: '0 2px 4px rgba(0,0,0,0.3)' }}>
|
||||
The Winter Weaves
|
||||
</h1>
|
||||
<p className="subtitle is-5 has-text-white-ter mb-6" style={{ lineHeight: '1.6', textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}>
|
||||
Discover the warmth of tradition. Hand-loomed pashminas and thick cotton weaves, crafted by generational artisans from the highlands.
|
||||
</p>
|
||||
<button className="button is-primary is-medium is-rounded px-6 is-outlined has-text-white" style={{ borderColor: 'white' }} onClick={handleSellerLogin}>
|
||||
<span className="has-text-weight-bold">Explore Collection</span>
|
||||
<span className="icon ml-2"><span className="material-symbols-outlined">arrow_forward</span></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Categories Bento Grid */}
|
||||
<section className="section py-6">
|
||||
<div className="container" style={{ maxWidth: '1120px' }}>
|
||||
<div className="has-text-centered mb-6">
|
||||
<h2 className="title is-2 has-text-dark" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Explore by Craft</h2>
|
||||
<div style={{ width: '64px', height: '4px', backgroundColor: 'var(--bulma-primary)', margin: '16px auto', borderRadius: '4px' }}></div>
|
||||
</div>
|
||||
|
||||
<div className="columns is-multiline">
|
||||
{/* Handloom (Large span - 8 columns) */}
|
||||
<div className="column is-8-tablet">
|
||||
<a className="box p-0 is-clickable is-clipped" style={{ position: 'relative', height: '400px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuAS9r3NI9-FM8tgHGT2hWRg-M8knvFHZx5atRJ4rhhNLbZMRDwLJMhzK2ON7vpXvZoecKox8x5DPSYrmVTTYT62g_Es8tHv4R9YNHG8GdacoUglwQOOx2CWtc_y244pgcUKTskwMF2so3MkqXUH9puqF9Un0FGplE_BERtUsY3V_lhdLgt72RRhTKc3XvooH19hMuCYN2ljDzPrUWY4917V8qekx0cKfBYQsrPI1GiJP1o4li2_j2VHjg')",
|
||||
backgroundSize: 'cover', backgroundPosition: 'center',
|
||||
transition: 'transform 0.5s ease'
|
||||
}} className="hover-zoom"></div>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.8), transparent 60%)' }}></div>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '2rem' }}>
|
||||
<span className="tag is-primary is-medium mb-3 has-text-weight-bold" style={{ backgroundColor: 'rgba(77, 2, 24, 0.9)' }}>Apparel & Textiles</span>
|
||||
<h3 className="title is-3 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Handloom Heritage</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Right side items (4 columns) */}
|
||||
<div className="column is-4-tablet is-flex is-flex-direction-column">
|
||||
{/* Pottery */}
|
||||
<a className="box p-0 is-clickable is-clipped mb-4 is-flex-grow-1" style={{ position: 'relative', height: '192px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuBQ4-Xs5qziux8_GlEJ0-ufRlJrIRx8C1oQCnRqE4DszCnYl_I30-ckRS3gFJChcgsaRXqmlQKB6rd1yajOCgURWi61y_-KiW5TtK_UqMjT3r4UrZHwyHoD091oxgN0ehJb-nKbkD5H2IyKr5MigS_W0M-n2ouvGlbmBELRwvaDfbEEUGlF488_X5DsuZE2Sgtu42Qqz3oQdxWBUNstUHgTgov_mzEv06FuJxLdPA5c7hVGFHWV7rQp5Q')",
|
||||
backgroundSize: 'cover', backgroundPosition: 'center'
|
||||
}} className="hover-zoom"></div>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)' }}></div>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '1.25rem' }}>
|
||||
<h3 className="title is-4 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Studio Pottery</h3>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Woodwork */}
|
||||
<a className="box p-0 is-clickable is-clipped is-flex-grow-1" style={{ position: 'relative', height: '192px', display: 'block', border: '1px solid #dac0c2' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: "url('https://lh3.googleusercontent.com/aida-public/AB6AXuDFzLzVAg22uWncCePb-VxR7tx9BnFu8jMLulD2SFXg5Gt2qjgpJoMQmGMcv2m99GA1oAoUG2dLPFTuWPhjxk7TqyUbNlBA9uCVSwPScN6wa_T8BKnLv88zT7glVF81p00Qe2YUNAsH1lzFn_jxGADhHvMv_o9Wior2Uk373C1m2F3z_TwXMfRNDPmJc7WOuHC67Pt0K036WLz5lFSKaf3MwxwtKSE1HO1vmm2U5rnoyrES8Z_iEnglNA')",
|
||||
backgroundSize: 'cover', backgroundPosition: 'center'
|
||||
}} className="hover-zoom"></div>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.7), transparent)' }}></div>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, padding: '1.25rem' }}>
|
||||
<h3 className="title is-4 has-text-white" style={{ fontFamily: 'Libre Caslon Text, serif' }}>Carved Woodwork</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Add hover zoom style */}
|
||||
<style>{`
|
||||
.hover-zoom:hover { transform: scale(1.05); }
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
166
src/components/pages/OnboardingWizard.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import React from 'react';
|
||||
import { useSeller } from '../../context/SellerContext';
|
||||
|
||||
const CRAFT_CATEGORIES = [
|
||||
'Apparel',
|
||||
'Home Decor',
|
||||
'Jewelry',
|
||||
'Textiles',
|
||||
'Pottery & Ceramics',
|
||||
'Woodwork',
|
||||
'Art & Collectibles',
|
||||
'Bath & Beauty',
|
||||
];
|
||||
|
||||
export default function OnboardingWizard() {
|
||||
const {
|
||||
handleProfileSubmit,
|
||||
businessType, setBusinessType,
|
||||
storeSlug, setStoreSlug,
|
||||
supportEmail, setSupportEmail,
|
||||
supportPhone, setSupportPhone,
|
||||
selectedCategories, setSelectedCategories
|
||||
} = useSeller();
|
||||
|
||||
const handleCategoryToggle = (category: string) => {
|
||||
if (selectedCategories.includes(category)) {
|
||||
setSelectedCategories(selectedCategories.filter((c: string) => c !== category));
|
||||
} else {
|
||||
setSelectedCategories([...selectedCategories, category]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="has-background-white" style={{ minHeight: 'calc(100vh - 4rem)' }}>
|
||||
<div className="container is-max-desktop py-6">
|
||||
<h1 className="title is-2 has-text-dark mb-4" style={{ fontFamily: 'Libre Caslon Text, serif' }}>
|
||||
Welcome to Tradhox
|
||||
</h1>
|
||||
<p className="subtitle is-5 has-text-grey-dark mb-6">
|
||||
Let's get to know your business. Tell us about what you do so we can help you reach the right audience.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleProfileSubmit} className="box p-5" style={{ border: '1px solid #d7c3b0', boxShadow: '0 4px 20px rgba(107,26,44,0.05)' }}>
|
||||
|
||||
<div className="field mb-5">
|
||||
<label className="label has-text-dark">Business Type</label>
|
||||
<div className="control">
|
||||
<label className="radio mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="businessType"
|
||||
value="registered_company"
|
||||
checked={businessType === 'registered_company'}
|
||||
onChange={(e) => setBusinessType(e.target.value as any)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Registered Company
|
||||
</label>
|
||||
<label className="radio mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="businessType"
|
||||
value="self_help_group"
|
||||
checked={businessType === 'self_help_group'}
|
||||
onChange={(e) => setBusinessType(e.target.value as any)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Self Help Group
|
||||
</label>
|
||||
<label className="radio">
|
||||
<input
|
||||
type="radio"
|
||||
name="businessType"
|
||||
value="individual_maker"
|
||||
checked={businessType === 'individual_maker'}
|
||||
onChange={(e) => setBusinessType(e.target.value as any)}
|
||||
className="mr-2"
|
||||
/>
|
||||
Individual Maker
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field mb-5">
|
||||
<label className="label has-text-dark">Store URL Slug</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="e.g. my-artisan-store"
|
||||
value={storeSlug}
|
||||
onChange={(e) => setStoreSlug(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<p className="help">This will be your unique Tradhox web address (e.g., tradhox.com/store/your-slug).</p>
|
||||
</div>
|
||||
|
||||
<div className="columns mb-5">
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label has-text-dark">Support Email</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="email"
|
||||
placeholder="support@example.com"
|
||||
value={supportEmail}
|
||||
onChange={(e) => setSupportEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<div className="field">
|
||||
<label className="label has-text-dark">Support Phone</label>
|
||||
<div className="control">
|
||||
<input
|
||||
className="input"
|
||||
type="tel"
|
||||
placeholder="+91 XXXXX XXXXX"
|
||||
value={supportPhone}
|
||||
onChange={(e) => setSupportPhone(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field mb-6">
|
||||
<label className="label has-text-dark">Categories</label>
|
||||
<p className="help mb-3">Select the categories that best describe your crafts.</p>
|
||||
<div className="columns is-multiline">
|
||||
{CRAFT_CATEGORIES.map(category => (
|
||||
<div key={category} className="column is-4 pb-2 pt-2">
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={selectedCategories.includes(category)}
|
||||
onChange={() => handleCategoryToggle(category)}
|
||||
/>
|
||||
{category}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field mt-5">
|
||||
<div className="control has-text-right">
|
||||
<button type="submit" className="button is-primary is-medium is-rounded">
|
||||
<span>Complete Profile</span>
|
||||
<span className="icon is-small ml-2">
|
||||
<span className="material-symbols-outlined">arrow_forward</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
const getApiBaseUrl = () => {
|
||||
if (typeof window !== 'undefined' && window.location.hostname === 'betasuppliers.tipro.in') {
|
||||
return 'http://16.113.57.127:8000';
|
||||
if (import.meta.env.DEV) {
|
||||
return 'http://127.0.0.1:8000'; // Or your local backend port
|
||||
}
|
||||
return 'http://localhost:8000';
|
||||
// Use relative path in production.
|
||||
// CloudFront will proxy /api/* requests to the EC2 backend automatically!
|
||||
return '';
|
||||
};
|
||||
|
||||
export const CONFIG = {
|
||||
apiBaseUrl: getApiBaseUrl(),
|
||||
companyName: 'Global Artisans Hub',
|
||||
companyName: 'Tradhox',
|
||||
logoLetter: 'A',
|
||||
supportEmail: 'support@globalartisanshub.com',
|
||||
supportEmail: 'support@tradhox.com',
|
||||
supportPhone: '+1 (800) 555-0199',
|
||||
address: '1775 Artisan Ridge Rd, Craftsville, CA 90210',
|
||||
|
||||
|
|
@ -122,3 +124,77 @@ export const CONFIG = {
|
|||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let isRefreshing = false;
|
||||
let refreshSubscribers: ((token: string) => void)[] = [];
|
||||
|
||||
function onTokenRefreshed(token: string) {
|
||||
refreshSubscribers.forEach(cb => cb(token));
|
||||
refreshSubscribers = [];
|
||||
}
|
||||
|
||||
async function refreshAccessToken(): Promise<string | null> {
|
||||
const refreshToken = localStorage.getItem('refresh_token');
|
||||
if (!refreshToken) return null;
|
||||
try {
|
||||
const res = await fetch(`${CONFIG.apiBaseUrl}/api/auth/token/refresh/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh: refreshToken }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const newToken = data.access;
|
||||
if (newToken) {
|
||||
localStorage.setItem('access_token', newToken);
|
||||
return newToken;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const makeRequest = (authToken: string | null) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
...(authToken ? { 'Authorization': `Bearer ${authToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
const response = await makeRequest(token);
|
||||
|
||||
// Auto-refresh on 401
|
||||
if (response.status === 401) {
|
||||
if (!isRefreshing) {
|
||||
isRefreshing = true;
|
||||
const newToken = await refreshAccessToken();
|
||||
isRefreshing = false;
|
||||
if (newToken) {
|
||||
onTokenRefreshed(newToken);
|
||||
return makeRequest(newToken);
|
||||
} else {
|
||||
// Refresh failed — clear tokens
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
refreshSubscribers = [];
|
||||
}
|
||||
} else {
|
||||
// Queue the request until refresh completes
|
||||
return new Promise(resolve => {
|
||||
refreshSubscribers.push((newToken: string) => {
|
||||
resolve(makeRequest(newToken));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
|
|||
835
src/context/SellerContext.tsx
Normal file
|
|
@ -0,0 +1,835 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { CONFIG, apiFetch } from '../config';
|
||||
|
||||
type Page = 'home' | 'about' | 'contact' | 'login' | 'signup' | 'profile-completion' | 'confirmation' | 'dashboard' | 'forgot-password' | 'login-otp' | 'welcome-tour';
|
||||
type DashboardTab = 'overview' | 'products' | 'orders' | 'returns' | 'wallet' | 'settings' | 'barcode-generator' | 'analytics';
|
||||
type DateFilter = 'year' | 'week' | 'day' | 'custom';
|
||||
|
||||
interface Product {
|
||||
id: string;
|
||||
title: string;
|
||||
category: string;
|
||||
price: number;
|
||||
stock: number;
|
||||
sku: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
date: string;
|
||||
item: string;
|
||||
quantity: number;
|
||||
customer: string;
|
||||
total: number;
|
||||
status: 'Ready to Ship test' | 'Ready to Ship' | 'Shipped' | 'Delivered' | 'Cancelled' | 'Pending Acceptance' | 'Rejected';
|
||||
carrier: string;
|
||||
tracking: string;
|
||||
eta: string;
|
||||
}
|
||||
|
||||
interface ReturnRequest {
|
||||
id: string;
|
||||
orderId: string;
|
||||
customer: string;
|
||||
item: string;
|
||||
reason: string;
|
||||
status: 'Pending Approval' | 'Approved' | 'Rejected' | 'In Transit';
|
||||
image: string;
|
||||
returningTracking: string;
|
||||
}
|
||||
|
||||
export const SellerContext = createContext<any>(null);
|
||||
|
||||
export const useSeller = () => useContext(SellerContext);
|
||||
|
||||
export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
|
||||
const [currentPage, setCurrentPage] = useState<Page>('home')
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
|
||||
|
||||
// Onboarding Status
|
||||
const [isProfileComplete, setIsProfileComplete] = useState(false)
|
||||
|
||||
// Registration / Onboarding Form States
|
||||
const [email, setEmail] = useState('')
|
||||
const [phone, setPhone] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
// Step 1: Verification
|
||||
const [phoneVerified, setPhoneVerified] = useState(false)
|
||||
const [emailVerified, setEmailVerified] = useState(false)
|
||||
const [phoneOtpSent, setPhoneOtpSent] = useState(false)
|
||||
const [emailOtpSent, setEmailOtpSent] = useState(false)
|
||||
const [enteredPhoneOtp, setEnteredPhoneOtp] = useState('')
|
||||
const [enteredEmailOtp, setEnteredEmailOtp] = useState('')
|
||||
|
||||
// Step 2: Business details (moved GSTIN here)
|
||||
const [gstin, setGstin] = useState('29AAAAA1111A1Z1')
|
||||
const [isGstinVerified, setIsGstinVerified] = useState(false)
|
||||
const [aadharFile, setAadharFile] = useState<string | null>(null)
|
||||
const [panFile, setPanFile] = useState<string | null>(null)
|
||||
const [aadharS3Key, setAadharS3Key] = useState<string | null>(null)
|
||||
const [aadharUrl, setAadharUrl] = useState<string | null>(null) // New
|
||||
const [panS3Key, setPanS3Key] = useState<string | null>(null)
|
||||
const [panUrl, setPanUrl] = useState<string | null>(null) // New
|
||||
|
||||
// New onboarding customization states
|
||||
const [businessType, setBusinessType] = useState<'registered_company' | 'self_help_group' | 'individual_maker'>('registered_company')
|
||||
const [storeSlug, setStoreSlug] = useState('')
|
||||
const [supportEmail, setSupportEmail] = useState('')
|
||||
const [supportPhone, setSupportPhone] = useState('')
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
||||
|
||||
// Step 3: Store and Location details
|
||||
const [storeName, setStoreName] = useState('My Artisan Handloom')
|
||||
const [storeLogo, setStoreLogo] = useState<string | null>(null)
|
||||
const [logoS3Key, setLogoS3Key] = useState<string | null>(null)
|
||||
const [businessBio, setBusinessBio] = useState('Traditional weaving and local sustainable designs.')
|
||||
const [address, setAddress] = useState({
|
||||
street: '123 Handloom Lane',
|
||||
city: 'Textile Town',
|
||||
state: 'Karnataka',
|
||||
pincode: '560001'
|
||||
})
|
||||
const [mapCoordinates, setMapCoordinates] = useState({ lat: 12.9716, lng: 77.5946 })
|
||||
|
||||
// --- Password visibility, confirm password and policy states ---
|
||||
const [showLoginPass, setShowLoginPass] = useState(false)
|
||||
const [showSignupPass, setShowSignupPass] = useState(false)
|
||||
const [showSignupConfirmPass, setShowSignupConfirmPass] = useState(false)
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [policyAccepted, setPolicyAccepted] = useState(false)
|
||||
|
||||
// --- Password Reset Page States ---
|
||||
const [resetEmail, setResetEmail] = useState('')
|
||||
const [resetOtpSent, setResetOtpSent] = useState(false)
|
||||
const [resetOtp, setResetOtp] = useState('')
|
||||
const [resetPassword, setResetPassword] = useState('')
|
||||
|
||||
// --- OTP Login States ---
|
||||
const [otpLoginPhone, setOtpLoginPhone] = useState('')
|
||||
const [otpLoginSent, setOtpLoginSent] = useState(false)
|
||||
const [otpLoginCode, setOtpLoginCode] = useState('')
|
||||
|
||||
// --- Barcode Generator States ---
|
||||
const [barcodeProductSku, setBarcodeProductSku] = useState('')
|
||||
const [barcodeGenerated, setBarcodeGenerated] = useState(false)
|
||||
|
||||
// --- Active Dashboard States ---
|
||||
const [dashTab, setDashTab] = useState<DashboardTab>('overview')
|
||||
const [dateFilter, setDateFilter] = useState<DateFilter>('year')
|
||||
const [customDates, setCustomDates] = useState({ start: '2026-06-01', end: '2026-06-15' })
|
||||
|
||||
// Lists
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [returns, setReturns] = useState<ReturnRequest[]>([])
|
||||
|
||||
// Forms & Editing
|
||||
const [productForm, setProductForm] = useState({
|
||||
id: '',
|
||||
title: '',
|
||||
category: 'Apparel',
|
||||
price: 0,
|
||||
stock: 0,
|
||||
sku: '',
|
||||
image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100'
|
||||
})
|
||||
const [isEditingProduct, setIsEditingProduct] = useState(false)
|
||||
|
||||
// New Heritage UI wizard states
|
||||
const [productWizardStep, setProductWizardStep] = useState<number>(0) // 0: Catalog list, 1-5: Product upload wizard steps
|
||||
const [productFormDetails, setProductFormDetails] = useState({
|
||||
id: '',
|
||||
name: '',
|
||||
category: 'Textiles & Apparel',
|
||||
description: '',
|
||||
isGiTagged: false,
|
||||
primaryImage: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=300',
|
||||
additionalViews: [] as string[],
|
||||
videoUrl: '',
|
||||
view360Url: '',
|
||||
basePrice: '',
|
||||
compareAtPrice: '',
|
||||
trackInventory: true,
|
||||
sku: '',
|
||||
initialStock: '',
|
||||
shippingProfile: 'Standard Fragile',
|
||||
processingDays: 3,
|
||||
packageWeight: 1.0
|
||||
})
|
||||
const [selectedOrderDetail, setSelectedOrderDetail] = useState<any | null>(null)
|
||||
const [orderNotes, setOrderNotes] = useState<string>('')
|
||||
|
||||
// Payout outstanding states
|
||||
const [wallet, setWallet] = useState({
|
||||
outstanding: 850.00,
|
||||
withdrawn: 1250.00,
|
||||
history: [
|
||||
{ id: 'TX-9031', date: '2026-08-01', amount: 500.00, status: 'Transferred' },
|
||||
{ id: 'TX-9022', date: '2026-07-15', amount: 750.00, status: 'Transferred' }
|
||||
]
|
||||
})
|
||||
const [withdrawAmount, setWithdrawAmount] = useState('')
|
||||
|
||||
// Load data from backend on mount or when profile is complete / logged in
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`)
|
||||
.then(res => {
|
||||
if (res.ok) return res.json();
|
||||
throw new Error('Session expired');
|
||||
})
|
||||
.then(user => {
|
||||
if (user && user.profile) {
|
||||
const profile = user.profile;
|
||||
setPhone(profile.phone || '');
|
||||
setPhoneVerified(profile.phone_verified || false);
|
||||
setEmail(user.email || '');
|
||||
setEmailVerified(profile.email_verified || false);
|
||||
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
||||
setIsGstinVerified(profile.is_gstin_verified || false);
|
||||
setAadharFile(profile.aadhar_file || null);
|
||||
setPanFile(profile.pan_file || null);
|
||||
setAadharS3Key(profile.aadhar_s3_key || null);
|
||||
setPanS3Key(profile.pan_s3_key || null);
|
||||
setAadharUrl(profile.aadhar_url || null); // New
|
||||
setPanUrl(profile.pan_url || null); // New
|
||||
|
||||
setBusinessType(profile.business_type || 'registered_company');
|
||||
setStoreSlug(profile.store_slug || '');
|
||||
setSupportEmail(profile.support_email || '');
|
||||
setSupportPhone(profile.support_phone || '');
|
||||
setSelectedCategories(profile.categories || []);
|
||||
|
||||
setStoreName(profile.store_name || 'My Artisan Handloom');
|
||||
setStoreLogo(profile.store_logo || null);
|
||||
setLogoS3Key(profile.logo_s3_key || null);
|
||||
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
||||
setAddress({
|
||||
street: profile.street || '123 Handloom Lane',
|
||||
city: profile.city || 'Textile Town',
|
||||
state: profile.state || 'Karnataka',
|
||||
pincode: profile.pincode || '560001'
|
||||
});
|
||||
if (profile.latitude && profile.longitude) {
|
||||
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
||||
}
|
||||
|
||||
const step = profile.onboarding_step;
|
||||
// The onboarding flow is now just 1 step, so anything >= 2 (or 4 if we keep old backend state) means complete
|
||||
if (step >= 4 || profile.is_profile_complete) {
|
||||
setIsProfileComplete(true);
|
||||
setCurrentPage('dashboard');
|
||||
} else {
|
||||
setIsProfileComplete(false);
|
||||
setCurrentPage('profile-completion');
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Session restore failed:', err);
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token');
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isProfileComplete || currentPage === 'dashboard') {
|
||||
// Fetch Products
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) setProducts(data);
|
||||
else if (data && Array.isArray(data.results)) setProducts(data.results);
|
||||
})
|
||||
.catch(err => console.error('Error fetching products:', err));
|
||||
|
||||
// Fetch Orders
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) setOrders(data);
|
||||
else if (data && Array.isArray(data.results)) setOrders(data.results);
|
||||
})
|
||||
.catch(err => console.error('Error fetching orders:', err));
|
||||
|
||||
// Fetch Returns
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) setReturns(data);
|
||||
else if (data && Array.isArray(data.results)) setReturns(data.results);
|
||||
})
|
||||
.catch(err => console.error('Error fetching returns:', err));
|
||||
|
||||
// Fetch Wallet
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data) setWallet(data);
|
||||
})
|
||||
.catch(err => console.error('Error fetching wallet:', err));
|
||||
}
|
||||
}, [isProfileComplete, currentPage]);
|
||||
|
||||
const uploadDocument = async (file: File, fileType: string) => {
|
||||
try {
|
||||
const response = await apiFetch(`${CONFIG.apiBaseUrl}/api/profile/presigned-url/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_type: fileType, content_type: file.type })
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to get presigned URL');
|
||||
const data = await response.json();
|
||||
|
||||
// Simulate file upload PUT to mock presigned_url
|
||||
await fetch(data.presigned_url, {
|
||||
method: 'PUT',
|
||||
body: file,
|
||||
headers: { 'Content-Type': file.type }
|
||||
}).catch(err => console.log('Mock S3 upload:', err));
|
||||
|
||||
return data.s3_key;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return `suppliers/default/${fileType}.jpg`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcceptOrder = (id: string) => {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/accept/`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updatedOrder => {
|
||||
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
||||
alert(`Order accepted successfully!`)
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
const handleRejectOrder = (id: string) => {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/reject/`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updatedOrder => {
|
||||
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
||||
alert(`Order rejected.`)
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
// Bulk Upload
|
||||
const [bulkLog, setBulkLog] = useState<string[]>([])
|
||||
const [isParsingBulk, setIsParsingBulk] = useState(false)
|
||||
const [bulkCsvFile, setBulkCsvFile] = useState<File | null>(null)
|
||||
const [bulkZipFile, setBulkZipFile] = useState<File | null>(null)
|
||||
|
||||
// GSTIN verification simulator
|
||||
const handleVerifyGstin = () => {
|
||||
if (!gstin.trim() || gstin.length !== 15) {
|
||||
alert('Invalid GSTIN length. Must be 15 chars.');
|
||||
return;
|
||||
}
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/submit-gstin/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ gstin })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to verify GSTIN.');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.verified) {
|
||||
setIsGstinVerified(true);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Logo Upload simulation
|
||||
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
setStoreLogo(reader.result as string)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
const s3Key = await uploadDocument(file, 'logo');
|
||||
setLogoS3Key(s3Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Route/Navigation Guard: If profile is not complete, redirect to profile-completion
|
||||
const navigateTo = (page: Page, forceComplete: boolean = false) => {
|
||||
const publicPages: Page[] = ['home', 'about', 'contact', 'login', 'signup', 'forgot-password', 'login-otp']
|
||||
const complete = isProfileComplete || forceComplete
|
||||
|
||||
if (!complete && !publicPages.includes(page) && page !== 'profile-completion') {
|
||||
alert('Access Denied: Please complete your supplier profile first!')
|
||||
setCurrentPage('profile-completion')
|
||||
} else {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/logout/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
.catch(err => console.error('Error logging out:', err))
|
||||
.finally(() => {
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
// Reset all onboarding & profile states to their defaults
|
||||
setIsProfileComplete(false)
|
||||
setEmail('')
|
||||
setPhone('')
|
||||
setPassword('')
|
||||
setConfirmPassword('')
|
||||
setPolicyAccepted(false)
|
||||
|
||||
setPhoneVerified(false)
|
||||
setEmailVerified(false)
|
||||
setPhoneOtpSent(false)
|
||||
setEmailOtpSent(false)
|
||||
setEnteredPhoneOtp('')
|
||||
setEnteredEmailOtp('')
|
||||
|
||||
setGstin('29AAAAA1111A1Z1')
|
||||
setIsGstinVerified(false)
|
||||
setAadharFile(null)
|
||||
setPanFile(null)
|
||||
setAadharS3Key(null)
|
||||
setAadharUrl(null)
|
||||
setPanS3Key(null)
|
||||
setPanUrl(null)
|
||||
|
||||
setBusinessType('registered_company')
|
||||
setStoreSlug('')
|
||||
setSupportEmail('')
|
||||
setSupportPhone('')
|
||||
setSelectedCategories([])
|
||||
|
||||
setStoreName('My Artisan Handloom')
|
||||
setStoreLogo(null)
|
||||
setLogoS3Key(null)
|
||||
setBusinessBio('Traditional weaving and local sustainable designs.')
|
||||
setAddress({
|
||||
street: '123 Handloom Lane',
|
||||
city: 'Textile Town',
|
||||
state: 'Karnataka',
|
||||
pincode: '560001'
|
||||
})
|
||||
setMapCoordinates({ lat: 12.9716, lng: 77.5946 })
|
||||
|
||||
navigateTo('home', true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const handleRegisterSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Email Validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
alert('Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Phone Validation
|
||||
const phoneRegex = /^[6-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
alert('Please enter a valid 10-digit mobile number starting with 6, 7, 8, or 9.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Password Validation
|
||||
if (password.length < 8) {
|
||||
alert('Password must be at least 8 characters long.');
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
alert('Passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Policy Validation
|
||||
if (!policyAccepted) {
|
||||
alert('You must accept the Terms of Service and Privacy Policy.');
|
||||
return;
|
||||
}
|
||||
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/register/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: email,
|
||||
email: email,
|
||||
phone: phone,
|
||||
password: password,
|
||||
confirm_password: confirmPassword,
|
||||
policy_accepted: policyAccepted
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Registration failed. Username/email might already be taken.');
|
||||
return res.json();
|
||||
})
|
||||
.then(() => {
|
||||
// Auto login after signup
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: email, password: password })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Auto-login failed.');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token);
|
||||
navigateTo('profile-completion')
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
const handleLoginSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email.trim()) {
|
||||
alert('Please enter your email or phone number.');
|
||||
return;
|
||||
}
|
||||
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: email, password: password })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Invalid credentials.');
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token);
|
||||
if (data.user && data.user.profile) {
|
||||
const profile = data.user.profile;
|
||||
setPhone(profile.phone || '');
|
||||
setPhoneVerified(profile.phone_verified || false);
|
||||
setEmail(data.user.email || '');
|
||||
setEmailVerified(profile.email_verified || false);
|
||||
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
||||
setIsGstinVerified(profile.is_gstin_verified || false);
|
||||
setAadharFile(profile.aadhar_file || null);
|
||||
setPanFile(profile.pan_file || null);
|
||||
setAadharS3Key(profile.aadhar_s3_key || null);
|
||||
setPanS3Key(profile.pan_s3_key || null);
|
||||
setAadharUrl(profile.aadhar_url || null);
|
||||
setPanUrl(profile.pan_url || null);
|
||||
setBusinessType(profile.business_type || 'registered_company');
|
||||
setStoreSlug(profile.store_slug || '');
|
||||
setSupportEmail(profile.support_email || '');
|
||||
setSupportPhone(profile.support_phone || '');
|
||||
setSelectedCategories(profile.categories || []);
|
||||
setStoreName(profile.store_name || 'My Artisan Handloom');
|
||||
setStoreLogo(profile.store_logo || null);
|
||||
setLogoS3Key(profile.logo_s3_key || null);
|
||||
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
||||
setAddress({
|
||||
street: profile.street || '123 Handloom Lane',
|
||||
city: profile.city || 'Textile Town',
|
||||
state: profile.state || 'Karnataka',
|
||||
pincode: profile.pincode || '560001'
|
||||
});
|
||||
if (profile.latitude && profile.longitude) {
|
||||
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
||||
}
|
||||
|
||||
const step = profile.onboarding_step;
|
||||
if (step >= 4 || profile.is_profile_complete) {
|
||||
setIsProfileComplete(true);
|
||||
navigateTo('dashboard', true);
|
||||
} else {
|
||||
setIsProfileComplete(false);
|
||||
navigateTo('profile-completion');
|
||||
}
|
||||
} else {
|
||||
setIsProfileComplete(false);
|
||||
navigateTo('profile-completion');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
const saveProfileBackend = (isComplete: boolean) => {
|
||||
return apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
phone: phone,
|
||||
phone_verified: phoneVerified,
|
||||
email_verified: emailVerified,
|
||||
gstin: gstin,
|
||||
is_gstin_verified: isGstinVerified,
|
||||
business_type: businessType,
|
||||
store_slug: storeSlug,
|
||||
support_email: supportEmail,
|
||||
support_phone: supportPhone,
|
||||
categories: selectedCategories,
|
||||
aadhar_file: aadharFile,
|
||||
pan_file: panFile,
|
||||
aadhar_s3_key: aadharS3Key || null,
|
||||
pan_s3_key: panS3Key || null,
|
||||
store_name: storeName,
|
||||
store_logo: storeLogo || 'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?auto=format&fit=crop&q=80&w=800',
|
||||
logo_s3_key: logoS3Key || null,
|
||||
business_bio: businessBio,
|
||||
street: address.street,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
pincode: address.pincode,
|
||||
latitude: mapCoordinates.lat,
|
||||
longitude: mapCoordinates.lng,
|
||||
is_profile_complete: isComplete
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to update profile.');
|
||||
return res.json();
|
||||
});
|
||||
};
|
||||
|
||||
const handleProfileSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
saveProfileBackend(true)
|
||||
.then(() => {
|
||||
setIsProfileComplete(true);
|
||||
setCurrentPage('dashboard');
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Dashboard Logic Actions ---
|
||||
|
||||
// Product creation/modification
|
||||
const handleSaveProduct = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const url = isEditingProduct
|
||||
? `${CONFIG.apiBaseUrl}/api/products/${productForm.id}/`
|
||||
: `${CONFIG.apiBaseUrl}/api/products/`
|
||||
const method = isEditingProduct ? 'PUT' : 'POST'
|
||||
|
||||
apiFetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: productForm.title,
|
||||
category: productForm.category,
|
||||
price: String(productForm.price),
|
||||
stock: Number(productForm.stock),
|
||||
sku: productForm.sku || `PROD-${Date.now().toString().slice(-6)}`,
|
||||
image: productForm.image
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(() => {
|
||||
// Refresh products from backend
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||
.then(r => r.json())
|
||||
.then(prods => {
|
||||
if (Array.isArray(prods)) setProducts(prods);
|
||||
else if (prods && Array.isArray(prods.results)) setProducts(prods.results);
|
||||
})
|
||||
setIsEditingProduct(false)
|
||||
alert(isEditingProduct ? 'Product modified successfully!' : 'Product uploaded successfully!')
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
|
||||
// reset form
|
||||
setProductForm({ id: '', title: '', category: 'Apparel', price: 0, stock: 0, sku: '', image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100' })
|
||||
}
|
||||
|
||||
const handleEditClick = (p: Product) => {
|
||||
setProductForm(p)
|
||||
setIsEditingProduct(true)
|
||||
}
|
||||
|
||||
const handleDeleteProduct = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this listing?')) {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/${id}/`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(() => {
|
||||
setProducts(products.filter(p => p.id !== id))
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkUploadSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!bulkCsvFile) {
|
||||
alert("Please select a CSV file to upload.");
|
||||
return;
|
||||
}
|
||||
setIsParsingBulk(true);
|
||||
setBulkLog(['Uploading files to server...', 'Parsing CSV data and extracting ZIP images...']);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('csv_file', bulkCsvFile);
|
||||
if (bulkZipFile) {
|
||||
formData.append('zip_file', bulkZipFile);
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${CONFIG.apiBaseUrl}/api/products/bulk-upload/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to complete bulk upload.');
|
||||
}
|
||||
|
||||
setBulkLog([
|
||||
'CSV file parsed successfully.',
|
||||
`Extracted and matched images for SKUs from ZIP file.`,
|
||||
`Successfully added ${data.products?.length || 0} product listings!`,
|
||||
]);
|
||||
|
||||
// Refresh product list
|
||||
const prodRes = await apiFetch(`${CONFIG.apiBaseUrl}/api/products/`);
|
||||
const prods = await prodRes.json();
|
||||
if (Array.isArray(prods)) {
|
||||
setProducts(prods);
|
||||
} else if (prods && Array.isArray(prods.results)) {
|
||||
setProducts(prods.results);
|
||||
}
|
||||
setBulkCsvFile(null);
|
||||
setBulkZipFile(null);
|
||||
} catch (err: any) {
|
||||
setBulkLog(prev => [...prev, `Error: ${err.message}`]);
|
||||
} finally {
|
||||
setIsParsingBulk(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Wallet outstanding requests
|
||||
const handleWithdrawRequest = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const amount = Number(withdrawAmount)
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
alert('Please enter a valid amount')
|
||||
return
|
||||
}
|
||||
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/withdraw/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ amount: String(amount) })
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Insufficient funds or invalid request');
|
||||
return res.json();
|
||||
})
|
||||
.then(updatedWallet => {
|
||||
setWallet(updatedWallet)
|
||||
setWithdrawAmount('')
|
||||
alert(`Payout of $${amount} successfully transferred!`)
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message)
|
||||
})
|
||||
}
|
||||
|
||||
// Returns actions
|
||||
const handleReturnAction = (id: string, action: 'Approved' | 'Rejected') => {
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/${id}/action/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: action })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(updatedReturn => {
|
||||
setReturns(returns.map(ret => ret.id === id ? updatedReturn : ret))
|
||||
alert(`Return request ${action.toLowerCase()}!`)
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
|
||||
// Compute realtime metrics based on state loaded from backend
|
||||
const computeRealtimeMetrics = () => {
|
||||
const totalSales = orders
|
||||
.filter(o => o.status !== 'Cancelled' && o.status !== 'Rejected')
|
||||
.reduce((sum, o) => sum + Number(o.total), 0)
|
||||
|
||||
const totalEarned = Number(wallet.outstanding) + Number(wallet.withdrawn)
|
||||
const totalStock = products.reduce((sum, p) => sum + Number(p.stock), 0)
|
||||
const totalReturns = returns.length
|
||||
|
||||
const isLoggedIn = currentPage === 'dashboard' || currentPage === 'profile-completion' || currentPage === 'welcome-tour' || isProfileComplete
|
||||
|
||||
if (isLoggedIn) {
|
||||
return {
|
||||
totalSales,
|
||||
totalEarned,
|
||||
stockDetails: totalStock,
|
||||
returnedItems: totalReturns,
|
||||
chartValues: orders.length > 0
|
||||
? orders.map(o => Number(o.total))
|
||||
: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSales: 45280.00,
|
||||
totalEarned: 38488.00,
|
||||
stockDetails: 342,
|
||||
returnedItems: 12,
|
||||
chartValues: CONFIG.dashboardData.filters[dateFilter]?.chartValues || [30, 45, 35, 60, 50, 75, 65, 80, 70, 95, 90, 110]
|
||||
}
|
||||
}
|
||||
|
||||
const selectedMetrics = computeRealtimeMetrics()
|
||||
|
||||
|
||||
|
||||
const contextValue = {
|
||||
setBusinessBio, email, resetPassword, setLogoS3Key, setStoreName, setShowLoginPass, storeName, otpLoginSent, setOtpLoginSent, aadharS3Key, logoS3Key, customDates, setProductFormDetails, handleBulkUploadSubmit, setMapCoordinates, setAadharFile, gstin, mapCoordinates, setIsGstinVerified, enteredPhoneOtp, address, setEmailVerified, dashTab, selectedOrderDetail, setShowSignupConfirmPass, handleRegisterSubmit, setBulkCsvFile, confirmPassword, navigateTo, setResetPassword, orderNotes, setOrders, handleRejectOrder, barcodeProductSku, setEnteredEmailOtp, setIsEditingProduct, setBulkLog, isMobileMenuOpen, resetOtp, productWizardStep, setCustomDates, setConfirmPassword, isGstinVerified, bulkCsvFile, setAddress, storeLogo, handleProfileSubmit, setPhoneOtpSent, setOrderNotes, handleDeleteProduct, setWallet, policyAccepted, emailOtpSent, setResetOtpSent, setGstin, phoneOtpSent, showLoginPass, showSignupConfirmPass, setPhoneVerified, aadharFile, setDateFilter, activeTab, computeRealtimeMetrics, setIsParsingBulk, handleLogout, panS3Key, setCurrentPage, selectedCategories, setEmailOtpSent, showSignupPass, resetEmail, handleLogoChange, dateFilter, setShowSignupPass, businessBio, setWithdrawAmount, phoneVerified, currentPage, setResetOtp, enteredEmailOtp, setEnteredPhoneOtp, setPanFile, setIsSidebarOpen, setSelectedOrderDetail, handleAcceptOrder, saveProfileBackend, supportPhone, handleEditClick, setStoreLogo, setEmail, password, setOtpLoginPhone, bulkLog, businessType, setPanS3Key, setProducts, setSupportEmail, setOtpLoginCode, withdrawAmount, handleSaveProduct, setStoreSlug, setBarcodeProductSku, setProductForm, setBulkZipFile, productFormDetails, setIsProfileComplete, otpLoginPhone, orders, setActiveTab, bulkZipFile, isSidebarOpen, setBarcodeGenerated, setBusinessType, handleReturnAction, setSupportPhone, barcodeGenerated, products, productForm, setProductWizardStep, resetOtpSent, emailVerified, panFile, isParsingBulk, setDashTab, setReturns, setPassword, handleWithdrawRequest, uploadDocument, setPolicyAccepted, isEditingProduct, setIsMobileMenuOpen, isProfileComplete, setSelectedCategories, storeSlug, phone, setPhone, setAadharS3Key, handleVerifyGstin, returns, handleLoginSubmit, setResetEmail, wallet, otpLoginCode, supportEmail, aadharUrl, setAadharUrl, panUrl, setPanUrl
|
||||
};
|
||||
|
||||
return (
|
||||
<SellerContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SellerContext.Provider>
|
||||
);
|
||||
};
|
||||
1100
src/index.css
8
start.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
# start.sh - Script to run the React application locally
|
||||
|
||||
# Navigate to the directory containing this script
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
echo "Starting React development server..."
|
||||
npm run dev
|
||||
4
test-results/.last-run.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,5 +3,15 @@ import react from '@vitejs/plugin-react'
|
|||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ export default mergeConfig(
|
|||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './src/setupTests.ts',
|
||||
exclude: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/cypress/**',
|
||||
'**/playwright/**',
|
||||
'playwright.config.ts'
|
||||
],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
|||