myoperator-ui 0.0.141 → 0.0.142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1730 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6609,6 +6609,1736 @@ export interface KeyValueRowProps {
|
|
|
6609
6609
|
/** Callback when row is deleted */
|
|
6610
6610
|
onDelete: (id: string) => void;
|
|
6611
6611
|
}
|
|
6612
|
+
`, prefix)
|
|
6613
|
+
}
|
|
6614
|
+
]
|
|
6615
|
+
},
|
|
6616
|
+
"api-feature-card": {
|
|
6617
|
+
name: "api-feature-card",
|
|
6618
|
+
description: "A card component for displaying API features with icon, title, description, and action button",
|
|
6619
|
+
dependencies: [
|
|
6620
|
+
"clsx",
|
|
6621
|
+
"tailwind-merge",
|
|
6622
|
+
"lucide-react"
|
|
6623
|
+
],
|
|
6624
|
+
internalDependencies: [
|
|
6625
|
+
"button"
|
|
6626
|
+
],
|
|
6627
|
+
isMultiFile: true,
|
|
6628
|
+
directory: "api-feature-card",
|
|
6629
|
+
mainFile: "api-feature-card.tsx",
|
|
6630
|
+
files: [
|
|
6631
|
+
{
|
|
6632
|
+
name: "api-feature-card.tsx",
|
|
6633
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
6634
|
+
import { Button } from "@/components/ui/button";
|
|
6635
|
+
import { cn } from "../../../lib/utils";
|
|
6636
|
+
|
|
6637
|
+
export interface Capability {
|
|
6638
|
+
/** Unique identifier for the capability */
|
|
6639
|
+
id: string;
|
|
6640
|
+
/** Display text for the capability */
|
|
6641
|
+
label: string;
|
|
6642
|
+
}
|
|
6643
|
+
|
|
6644
|
+
export interface ApiFeatureCardProps
|
|
6645
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
6646
|
+
/** Icon component to display */
|
|
6647
|
+
icon: React.ReactNode;
|
|
6648
|
+
/** Card title */
|
|
6649
|
+
title: string;
|
|
6650
|
+
/** Card description */
|
|
6651
|
+
description: string;
|
|
6652
|
+
/** List of key capabilities */
|
|
6653
|
+
capabilities?: Capability[];
|
|
6654
|
+
/** Text for the action button */
|
|
6655
|
+
actionLabel?: string;
|
|
6656
|
+
/** Icon for the action button */
|
|
6657
|
+
actionIcon?: React.ReactNode;
|
|
6658
|
+
/** Callback when action button is clicked */
|
|
6659
|
+
onAction?: () => void;
|
|
6660
|
+
/** Label for the capabilities section */
|
|
6661
|
+
capabilitiesLabel?: string;
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
/**
|
|
6665
|
+
* ApiFeatureCard displays an API feature with icon, title, description,
|
|
6666
|
+
* action button, and a list of key capabilities.
|
|
6667
|
+
*
|
|
6668
|
+
* @example
|
|
6669
|
+
* \`\`\`tsx
|
|
6670
|
+
* <ApiFeatureCard
|
|
6671
|
+
* icon={<Phone className="h-5 w-5" />}
|
|
6672
|
+
* title="Calling API"
|
|
6673
|
+
* description="Manage real-time call flow, recordings, and intelligent routing."
|
|
6674
|
+
* capabilities={[
|
|
6675
|
+
* { id: "1", label: "Real-time Call Control" },
|
|
6676
|
+
* { id: "2", label: "Live Call Events (Webhooks)" },
|
|
6677
|
+
* ]}
|
|
6678
|
+
* onAction={() => console.log("Manage clicked")}
|
|
6679
|
+
* />
|
|
6680
|
+
* \`\`\`
|
|
6681
|
+
*/
|
|
6682
|
+
export const ApiFeatureCard = React.forwardRef<
|
|
6683
|
+
HTMLDivElement,
|
|
6684
|
+
ApiFeatureCardProps
|
|
6685
|
+
>(
|
|
6686
|
+
(
|
|
6687
|
+
{
|
|
6688
|
+
icon,
|
|
6689
|
+
title,
|
|
6690
|
+
description,
|
|
6691
|
+
capabilities = [],
|
|
6692
|
+
actionLabel = "Manage",
|
|
6693
|
+
actionIcon,
|
|
6694
|
+
onAction,
|
|
6695
|
+
capabilitiesLabel = "Key Capabilities",
|
|
6696
|
+
className,
|
|
6697
|
+
...props
|
|
6698
|
+
},
|
|
6699
|
+
ref
|
|
6700
|
+
) => {
|
|
6701
|
+
return (
|
|
6702
|
+
<div
|
|
6703
|
+
ref={ref}
|
|
6704
|
+
className={cn(
|
|
6705
|
+
"flex flex-col gap-6 rounded-lg border border-semantic-border-layout bg-semantic-bg-primary p-6 overflow-hidden",
|
|
6706
|
+
className
|
|
6707
|
+
)}
|
|
6708
|
+
{...props}
|
|
6709
|
+
>
|
|
6710
|
+
{/* Header Section */}
|
|
6711
|
+
<div className="flex items-center justify-between">
|
|
6712
|
+
<div className="flex items-center gap-2.5">
|
|
6713
|
+
{/* Icon Container */}
|
|
6714
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[10px] bg-semantic-info-surface">
|
|
6715
|
+
<span className="text-[var(--color-primary-950)] [&_svg]:h-5 [&_svg]:w-5">
|
|
6716
|
+
{icon}
|
|
6717
|
+
</span>
|
|
6718
|
+
</div>
|
|
6719
|
+
|
|
6720
|
+
{/* Title and Description */}
|
|
6721
|
+
<div className="flex flex-col gap-1.5">
|
|
6722
|
+
<h3 className="m-0 text-base font-semibold text-semantic-text-primary">
|
|
6723
|
+
{title}
|
|
6724
|
+
</h3>
|
|
6725
|
+
<p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
|
|
6726
|
+
{description}
|
|
6727
|
+
</p>
|
|
6728
|
+
</div>
|
|
6729
|
+
</div>
|
|
6730
|
+
|
|
6731
|
+
{/* Action Button */}
|
|
6732
|
+
<Button
|
|
6733
|
+
variant="default"
|
|
6734
|
+
size="default"
|
|
6735
|
+
leftIcon={actionIcon}
|
|
6736
|
+
onClick={onAction}
|
|
6737
|
+
className="shrink-0"
|
|
6738
|
+
>
|
|
6739
|
+
{actionLabel}
|
|
6740
|
+
</Button>
|
|
6741
|
+
</div>
|
|
6742
|
+
|
|
6743
|
+
{/* Capabilities Section */}
|
|
6744
|
+
{capabilities.length > 0 && (
|
|
6745
|
+
<div className="flex flex-col gap-2.5 border-t border-semantic-border-layout bg-[var(--color-neutral-50)] -mx-6 -mb-6 p-6">
|
|
6746
|
+
<span className="text-sm font-semibold uppercase tracking-[0.014px] text-[var(--color-neutral-400)]">
|
|
6747
|
+
{capabilitiesLabel}
|
|
6748
|
+
</span>
|
|
6749
|
+
<div className="flex flex-wrap gap-x-6 gap-y-2">
|
|
6750
|
+
{capabilities.map((capability) => (
|
|
6751
|
+
<div
|
|
6752
|
+
key={capability.id}
|
|
6753
|
+
className="flex items-center gap-1.5"
|
|
6754
|
+
>
|
|
6755
|
+
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-neutral-400)]" />
|
|
6756
|
+
<span className="text-sm text-semantic-text-primary tracking-[0.035px]">
|
|
6757
|
+
{capability.label}
|
|
6758
|
+
</span>
|
|
6759
|
+
</div>
|
|
6760
|
+
))}
|
|
6761
|
+
</div>
|
|
6762
|
+
</div>
|
|
6763
|
+
)}
|
|
6764
|
+
</div>
|
|
6765
|
+
);
|
|
6766
|
+
}
|
|
6767
|
+
);
|
|
6768
|
+
|
|
6769
|
+
ApiFeatureCard.displayName = "ApiFeatureCard";
|
|
6770
|
+
`, prefix)
|
|
6771
|
+
},
|
|
6772
|
+
{
|
|
6773
|
+
name: "index.ts",
|
|
6774
|
+
content: prefixTailwindClasses(`export { ApiFeatureCard } from "./api-feature-card";
|
|
6775
|
+
export type { ApiFeatureCardProps, Capability } from "./api-feature-card";
|
|
6776
|
+
`, prefix)
|
|
6777
|
+
}
|
|
6778
|
+
]
|
|
6779
|
+
},
|
|
6780
|
+
"endpoint-details": {
|
|
6781
|
+
name: "endpoint-details",
|
|
6782
|
+
description: "A component for displaying API endpoint details with copy-to-clipboard and secret field support",
|
|
6783
|
+
dependencies: [
|
|
6784
|
+
"clsx",
|
|
6785
|
+
"tailwind-merge",
|
|
6786
|
+
"lucide-react"
|
|
6787
|
+
],
|
|
6788
|
+
internalDependencies: [
|
|
6789
|
+
"readable-field"
|
|
6790
|
+
],
|
|
6791
|
+
isMultiFile: true,
|
|
6792
|
+
directory: "endpoint-details",
|
|
6793
|
+
mainFile: "endpoint-details.tsx",
|
|
6794
|
+
files: [
|
|
6795
|
+
{
|
|
6796
|
+
name: "endpoint-details.tsx",
|
|
6797
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
6798
|
+
import { XCircle } from "lucide-react";
|
|
6799
|
+
import { cn } from "../../../lib/utils";
|
|
6800
|
+
import { ReadableField } from "@/components/ui/readable-field";
|
|
6801
|
+
|
|
6802
|
+
export interface EndpointDetailsProps
|
|
6803
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
6804
|
+
/** Card title */
|
|
6805
|
+
title?: string;
|
|
6806
|
+
/** Variant determines field layout and visibility */
|
|
6807
|
+
variant?: "calling" | "whatsapp";
|
|
6808
|
+
/** Base URL for the API endpoint */
|
|
6809
|
+
baseUrl: string;
|
|
6810
|
+
/** Company ID */
|
|
6811
|
+
companyId: string;
|
|
6812
|
+
/** Authentication token (secret in calling variant, visible in whatsapp variant) */
|
|
6813
|
+
authToken: string;
|
|
6814
|
+
/** Secret key (secret) - only shown in calling variant */
|
|
6815
|
+
secretKey?: string;
|
|
6816
|
+
/** API key (visible) - only shown in calling variant */
|
|
6817
|
+
apiKey?: string;
|
|
6818
|
+
/** Callback when a field value is copied */
|
|
6819
|
+
onValueCopy?: (field: string, value: string) => void;
|
|
6820
|
+
/** Callback when regenerate is clicked for a field - only used in calling variant */
|
|
6821
|
+
onRegenerate?: (field: "authToken" | "secretKey") => void;
|
|
6822
|
+
/** Callback when revoke access is clicked - only used in calling variant */
|
|
6823
|
+
onRevokeAccess?: () => void;
|
|
6824
|
+
/** Whether to show the revoke access section - only used in calling variant */
|
|
6825
|
+
showRevokeSection?: boolean;
|
|
6826
|
+
/** Custom revoke section title */
|
|
6827
|
+
revokeTitle?: string;
|
|
6828
|
+
/** Custom revoke section description */
|
|
6829
|
+
revokeDescription?: string;
|
|
6830
|
+
}
|
|
6831
|
+
|
|
6832
|
+
/**
|
|
6833
|
+
* EndpointDetails displays API endpoint credentials with copy functionality.
|
|
6834
|
+
* Used for showing API keys, authentication tokens, and other sensitive credentials.
|
|
6835
|
+
*
|
|
6836
|
+
* Supports two variants:
|
|
6837
|
+
* - \`calling\` (default): Full version with all 5 fields + revoke section
|
|
6838
|
+
* - \`whatsapp\`: Simplified with 3 fields (baseUrl, companyId, authToken), no revoke
|
|
6839
|
+
*
|
|
6840
|
+
* @example
|
|
6841
|
+
* \`\`\`tsx
|
|
6842
|
+
* // Calling API (default)
|
|
6843
|
+
* <EndpointDetails
|
|
6844
|
+
* variant="calling"
|
|
6845
|
+
* baseUrl="https://api.myoperator.co/v3/voice/gateway"
|
|
6846
|
+
* companyId="12"
|
|
6847
|
+
* authToken="sk_live_abc123"
|
|
6848
|
+
* secretKey="whsec_xyz789"
|
|
6849
|
+
* apiKey="tpb0syNDbO4k49ZbyiWeU5k8gFWQ7ODBJ7GYr3UO"
|
|
6850
|
+
* onRegenerate={(field) => console.log(\`Regenerate \${field}\`)}
|
|
6851
|
+
* onRevokeAccess={() => console.log("Revoke access")}
|
|
6852
|
+
* />
|
|
6853
|
+
*
|
|
6854
|
+
* // WhatsApp API
|
|
6855
|
+
* <EndpointDetails
|
|
6856
|
+
* variant="whatsapp"
|
|
6857
|
+
* baseUrl="https://api.myoperator.co/whatsapp"
|
|
6858
|
+
* companyId="WA-12345"
|
|
6859
|
+
* authToken="waba_token_abc123"
|
|
6860
|
+
* />
|
|
6861
|
+
* \`\`\`
|
|
6862
|
+
*/
|
|
6863
|
+
export const EndpointDetails = React.forwardRef<
|
|
6864
|
+
HTMLDivElement,
|
|
6865
|
+
EndpointDetailsProps
|
|
6866
|
+
>(
|
|
6867
|
+
(
|
|
6868
|
+
{
|
|
6869
|
+
title = "Endpoint Details",
|
|
6870
|
+
variant = "calling",
|
|
6871
|
+
baseUrl,
|
|
6872
|
+
companyId,
|
|
6873
|
+
authToken,
|
|
6874
|
+
secretKey,
|
|
6875
|
+
apiKey,
|
|
6876
|
+
onValueCopy,
|
|
6877
|
+
onRegenerate,
|
|
6878
|
+
onRevokeAccess,
|
|
6879
|
+
showRevokeSection = true,
|
|
6880
|
+
revokeTitle = "Revoke API Access",
|
|
6881
|
+
revokeDescription = "Revoking access will immediately disable all integrations using these keys.",
|
|
6882
|
+
className,
|
|
6883
|
+
...props
|
|
6884
|
+
},
|
|
6885
|
+
ref
|
|
6886
|
+
) => {
|
|
6887
|
+
const isCalling = variant === "calling";
|
|
6888
|
+
|
|
6889
|
+
const handleCopy = (field: string) => (value: string) => {
|
|
6890
|
+
onValueCopy?.(field, value);
|
|
6891
|
+
};
|
|
6892
|
+
|
|
6893
|
+
return (
|
|
6894
|
+
<div
|
|
6895
|
+
ref={ref}
|
|
6896
|
+
className={cn(
|
|
6897
|
+
"flex flex-col gap-6 rounded-lg border border-semantic-border-layout p-6",
|
|
6898
|
+
className
|
|
6899
|
+
)}
|
|
6900
|
+
{...props}
|
|
6901
|
+
>
|
|
6902
|
+
{/* Title */}
|
|
6903
|
+
<div className="flex items-start gap-5">
|
|
6904
|
+
<h2 className="m-0 text-base font-semibold text-semantic-text-primary">
|
|
6905
|
+
{title}
|
|
6906
|
+
</h2>
|
|
6907
|
+
</div>
|
|
6908
|
+
|
|
6909
|
+
{/* Credentials Grid */}
|
|
6910
|
+
<div className="flex flex-col gap-[30px]">
|
|
6911
|
+
{/* Row 1: Base URL + Company ID */}
|
|
6912
|
+
<div className="grid grid-cols-2 gap-[25px]">
|
|
6913
|
+
<ReadableField
|
|
6914
|
+
label="Base URL"
|
|
6915
|
+
value={baseUrl}
|
|
6916
|
+
onValueCopy={handleCopy("baseUrl")}
|
|
6917
|
+
/>
|
|
6918
|
+
<ReadableField
|
|
6919
|
+
label="Company ID"
|
|
6920
|
+
value={companyId}
|
|
6921
|
+
onValueCopy={handleCopy("companyId")}
|
|
6922
|
+
/>
|
|
6923
|
+
</div>
|
|
6924
|
+
|
|
6925
|
+
{/* Authentication field - different based on variant */}
|
|
6926
|
+
{isCalling ? (
|
|
6927
|
+
/* Calling variant: 2-col row with Authentication + Secret Key */
|
|
6928
|
+
<div className="grid grid-cols-2 gap-[25px]">
|
|
6929
|
+
<ReadableField
|
|
6930
|
+
label="Authentication"
|
|
6931
|
+
value={authToken}
|
|
6932
|
+
secret
|
|
6933
|
+
helperText="Used for client-side integrations."
|
|
6934
|
+
headerAction={{
|
|
6935
|
+
label: "Regenerate",
|
|
6936
|
+
onClick: () => onRegenerate?.("authToken"),
|
|
6937
|
+
}}
|
|
6938
|
+
onValueCopy={handleCopy("authToken")}
|
|
6939
|
+
/>
|
|
6940
|
+
{secretKey && (
|
|
6941
|
+
<ReadableField
|
|
6942
|
+
label="Secret Key"
|
|
6943
|
+
value={secretKey}
|
|
6944
|
+
secret
|
|
6945
|
+
helperText="Never share this key or expose it in client-side code."
|
|
6946
|
+
headerAction={{
|
|
6947
|
+
label: "Regenerate",
|
|
6948
|
+
onClick: () => onRegenerate?.("secretKey"),
|
|
6949
|
+
}}
|
|
6950
|
+
onValueCopy={handleCopy("secretKey")}
|
|
6951
|
+
/>
|
|
6952
|
+
)}
|
|
6953
|
+
</div>
|
|
6954
|
+
) : (
|
|
6955
|
+
/* WhatsApp variant: full-width Authentication, NOT secret, NO regenerate */
|
|
6956
|
+
<ReadableField
|
|
6957
|
+
label="Authentication"
|
|
6958
|
+
value={authToken}
|
|
6959
|
+
onValueCopy={handleCopy("authToken")}
|
|
6960
|
+
/>
|
|
6961
|
+
)}
|
|
6962
|
+
|
|
6963
|
+
{/* x-api-key (full width) - only for calling variant */}
|
|
6964
|
+
{isCalling && apiKey && (
|
|
6965
|
+
<ReadableField
|
|
6966
|
+
label="x-api-key"
|
|
6967
|
+
value={apiKey}
|
|
6968
|
+
onValueCopy={handleCopy("apiKey")}
|
|
6969
|
+
/>
|
|
6970
|
+
)}
|
|
6971
|
+
|
|
6972
|
+
{/* Revoke Section - only for calling variant */}
|
|
6973
|
+
{isCalling && showRevokeSection && (
|
|
6974
|
+
<div className="flex items-center justify-between border-t border-semantic-border-layout pt-6">
|
|
6975
|
+
<div className="flex flex-col gap-1">
|
|
6976
|
+
<h3 className="m-0 text-base font-semibold text-semantic-text-primary">
|
|
6977
|
+
{revokeTitle}
|
|
6978
|
+
</h3>
|
|
6979
|
+
<p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
|
|
6980
|
+
{revokeDescription}
|
|
6981
|
+
</p>
|
|
6982
|
+
</div>
|
|
6983
|
+
<button
|
|
6984
|
+
type="button"
|
|
6985
|
+
onClick={onRevokeAccess}
|
|
6986
|
+
className="flex items-center gap-1 text-sm text-semantic-error-primary hover:text-semantic-error-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-semantic-error-primary transition-colors tracking-[0.035px] rounded"
|
|
6987
|
+
>
|
|
6988
|
+
<XCircle className="size-4" />
|
|
6989
|
+
<span>Revoke Access</span>
|
|
6990
|
+
</button>
|
|
6991
|
+
</div>
|
|
6992
|
+
)}
|
|
6993
|
+
</div>
|
|
6994
|
+
</div>
|
|
6995
|
+
);
|
|
6996
|
+
}
|
|
6997
|
+
);
|
|
6998
|
+
|
|
6999
|
+
EndpointDetails.displayName = "EndpointDetails";
|
|
7000
|
+
`, prefix)
|
|
7001
|
+
},
|
|
7002
|
+
{
|
|
7003
|
+
name: "index.ts",
|
|
7004
|
+
content: prefixTailwindClasses(`export { EndpointDetails } from "./endpoint-details";
|
|
7005
|
+
export type { EndpointDetailsProps } from "./endpoint-details";
|
|
7006
|
+
`, prefix)
|
|
7007
|
+
}
|
|
7008
|
+
]
|
|
7009
|
+
},
|
|
7010
|
+
"alert-configuration": {
|
|
7011
|
+
name: "alert-configuration",
|
|
7012
|
+
description: "A configuration card for alert settings with inline editing modal",
|
|
7013
|
+
dependencies: [
|
|
7014
|
+
"clsx",
|
|
7015
|
+
"tailwind-merge",
|
|
7016
|
+
"lucide-react"
|
|
7017
|
+
],
|
|
7018
|
+
internalDependencies: [
|
|
7019
|
+
"button",
|
|
7020
|
+
"form-modal",
|
|
7021
|
+
"input"
|
|
7022
|
+
],
|
|
7023
|
+
isMultiFile: true,
|
|
7024
|
+
directory: "alert-configuration",
|
|
7025
|
+
mainFile: "alert-configuration.tsx",
|
|
7026
|
+
files: [
|
|
7027
|
+
{
|
|
7028
|
+
name: "alert-configuration.tsx",
|
|
7029
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7030
|
+
import { Button } from "@/components/ui/button";
|
|
7031
|
+
import { Pencil } from "lucide-react";
|
|
7032
|
+
import { cn } from "../../../lib/utils";
|
|
7033
|
+
|
|
7034
|
+
export interface AlertConfigurationProps {
|
|
7035
|
+
/** Minimum balance threshold */
|
|
7036
|
+
minimumBalance: number;
|
|
7037
|
+
/** Minimum top-up amount */
|
|
7038
|
+
minimumTopup: number;
|
|
7039
|
+
/** Currency symbol (default: \u20B9) */
|
|
7040
|
+
currencySymbol?: string;
|
|
7041
|
+
/** Callback when edit button is clicked */
|
|
7042
|
+
onEdit?: () => void;
|
|
7043
|
+
/** Custom className for the container */
|
|
7044
|
+
className?: string;
|
|
7045
|
+
}
|
|
7046
|
+
|
|
7047
|
+
/**
|
|
7048
|
+
* AlertConfiguration component displays current alert values for minimum balance and top-up.
|
|
7049
|
+
* Used in payment auto-pay setup to show notification thresholds.
|
|
7050
|
+
*/
|
|
7051
|
+
export const AlertConfiguration = React.forwardRef<
|
|
7052
|
+
HTMLDivElement,
|
|
7053
|
+
AlertConfigurationProps
|
|
7054
|
+
>(
|
|
7055
|
+
(
|
|
7056
|
+
{
|
|
7057
|
+
minimumBalance,
|
|
7058
|
+
minimumTopup,
|
|
7059
|
+
currencySymbol = "\u20B9",
|
|
7060
|
+
onEdit,
|
|
7061
|
+
className,
|
|
7062
|
+
},
|
|
7063
|
+
ref
|
|
7064
|
+
) => {
|
|
7065
|
+
const formatCurrency = (amount: number) => {
|
|
7066
|
+
const formatted = amount.toLocaleString("en-IN", {
|
|
7067
|
+
minimumFractionDigits: 2,
|
|
7068
|
+
maximumFractionDigits: 2,
|
|
7069
|
+
});
|
|
7070
|
+
return \`\${currencySymbol} \${formatted}\`;
|
|
7071
|
+
};
|
|
7072
|
+
|
|
7073
|
+
return (
|
|
7074
|
+
<div
|
|
7075
|
+
ref={ref}
|
|
7076
|
+
className={cn(
|
|
7077
|
+
"rounded-lg border border-semantic-border-layout bg-semantic-bg-primary",
|
|
7078
|
+
className
|
|
7079
|
+
)}
|
|
7080
|
+
>
|
|
7081
|
+
{/* Header */}
|
|
7082
|
+
<div className="flex items-center justify-between gap-3 px-4 py-4">
|
|
7083
|
+
<div className="flex flex-col gap-1">
|
|
7084
|
+
<h3 className="m-0 text-base font-semibold text-semantic-text-primary tracking-[0px]">
|
|
7085
|
+
Alert configurations
|
|
7086
|
+
</h3>
|
|
7087
|
+
<p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px]">
|
|
7088
|
+
Define when and how you receive balance notifications
|
|
7089
|
+
</p>
|
|
7090
|
+
</div>
|
|
7091
|
+
<Button
|
|
7092
|
+
variant="default"
|
|
7093
|
+
size="default"
|
|
7094
|
+
leftIcon={<Pencil className="h-3.5 w-3.5" />}
|
|
7095
|
+
onClick={onEdit}
|
|
7096
|
+
className="shrink-0"
|
|
7097
|
+
>
|
|
7098
|
+
Edit alert values
|
|
7099
|
+
</Button>
|
|
7100
|
+
</div>
|
|
7101
|
+
|
|
7102
|
+
{/* Alert Values Section with Top Border */}
|
|
7103
|
+
<div className="border-t border-semantic-border-layout px-4 py-4">
|
|
7104
|
+
<div className="flex items-start justify-between gap-4">
|
|
7105
|
+
{/* Minimum Balance */}
|
|
7106
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
7107
|
+
<div className="flex items-center justify-between">
|
|
7108
|
+
<span className="text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
|
|
7109
|
+
Minimum balance
|
|
7110
|
+
</span>
|
|
7111
|
+
<span
|
|
7112
|
+
className={cn(
|
|
7113
|
+
"text-sm tracking-[0.035px]",
|
|
7114
|
+
minimumBalance < 0
|
|
7115
|
+
? "text-semantic-error-primary"
|
|
7116
|
+
: "text-semantic-text-primary"
|
|
7117
|
+
)}
|
|
7118
|
+
>
|
|
7119
|
+
{minimumBalance < 0 ? "-" : ""}{formatCurrency(Math.abs(minimumBalance))}
|
|
7120
|
+
</span>
|
|
7121
|
+
</div>
|
|
7122
|
+
<p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px] leading-relaxed">
|
|
7123
|
+
You'll be notified by email and SMS when your balance falls below this level.
|
|
7124
|
+
</p>
|
|
7125
|
+
</div>
|
|
7126
|
+
|
|
7127
|
+
{/* Vertical Divider */}
|
|
7128
|
+
<div className="w-px h-14 bg-semantic-border-layout shrink-0" />
|
|
7129
|
+
|
|
7130
|
+
{/* Minimum Top-up */}
|
|
7131
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
7132
|
+
<div className="flex items-center justify-between">
|
|
7133
|
+
<span className="text-sm font-semibold text-semantic-text-primary tracking-[0.014px]">
|
|
7134
|
+
Minimum topup
|
|
7135
|
+
</span>
|
|
7136
|
+
<span className="text-sm text-semantic-text-link tracking-[0.035px]">
|
|
7137
|
+
{formatCurrency(minimumTopup)}
|
|
7138
|
+
</span>
|
|
7139
|
+
</div>
|
|
7140
|
+
<p className="m-0 text-sm text-semantic-text-muted tracking-[0.035px] leading-relaxed">
|
|
7141
|
+
A suggested recharge amount to top up your balance when it falls below the minimum.
|
|
7142
|
+
</p>
|
|
7143
|
+
</div>
|
|
7144
|
+
</div>
|
|
7145
|
+
</div>
|
|
7146
|
+
</div>
|
|
7147
|
+
);
|
|
7148
|
+
}
|
|
7149
|
+
);
|
|
7150
|
+
|
|
7151
|
+
AlertConfiguration.displayName = "AlertConfiguration";
|
|
7152
|
+
`, prefix)
|
|
7153
|
+
},
|
|
7154
|
+
{
|
|
7155
|
+
name: "alert-values-modal.tsx",
|
|
7156
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7157
|
+
import { FormModal } from "@/components/ui/form-modal";
|
|
7158
|
+
import { Input } from "@/components/ui/input";
|
|
7159
|
+
|
|
7160
|
+
export interface AlertValuesModalProps {
|
|
7161
|
+
/** Whether the modal is open */
|
|
7162
|
+
open: boolean;
|
|
7163
|
+
/** Callback when modal should close */
|
|
7164
|
+
onOpenChange: (open: boolean) => void;
|
|
7165
|
+
/** Initial minimum balance value */
|
|
7166
|
+
initialMinimumBalance?: number;
|
|
7167
|
+
/** Initial minimum top-up value */
|
|
7168
|
+
initialMinimumTopup?: number;
|
|
7169
|
+
/** Currency symbol (default: \u20B9) */
|
|
7170
|
+
currencySymbol?: string;
|
|
7171
|
+
/** Callback when values are saved */
|
|
7172
|
+
onSave?: (values: { minimumBalance: number; minimumTopup: number }) => void;
|
|
7173
|
+
/** Loading state for save button */
|
|
7174
|
+
loading?: boolean;
|
|
7175
|
+
}
|
|
7176
|
+
|
|
7177
|
+
/**
|
|
7178
|
+
* AlertValuesModal component for editing alert configuration values.
|
|
7179
|
+
* Displays a form with inputs for minimum balance and minimum top-up.
|
|
7180
|
+
*/
|
|
7181
|
+
export const AlertValuesModal = React.forwardRef<
|
|
7182
|
+
HTMLDivElement,
|
|
7183
|
+
AlertValuesModalProps
|
|
7184
|
+
>(
|
|
7185
|
+
(
|
|
7186
|
+
{
|
|
7187
|
+
open,
|
|
7188
|
+
onOpenChange,
|
|
7189
|
+
initialMinimumBalance = 0,
|
|
7190
|
+
initialMinimumTopup = 0,
|
|
7191
|
+
currencySymbol = "\u20B9",
|
|
7192
|
+
onSave,
|
|
7193
|
+
loading = false,
|
|
7194
|
+
},
|
|
7195
|
+
ref
|
|
7196
|
+
) => {
|
|
7197
|
+
const [minimumBalance, setMinimumBalance] = React.useState(
|
|
7198
|
+
initialMinimumBalance.toString()
|
|
7199
|
+
);
|
|
7200
|
+
const [minimumTopup, setMinimumTopup] = React.useState(
|
|
7201
|
+
initialMinimumTopup.toString()
|
|
7202
|
+
);
|
|
7203
|
+
|
|
7204
|
+
// Update form values when initial values change
|
|
7205
|
+
React.useEffect(() => {
|
|
7206
|
+
setMinimumBalance(initialMinimumBalance.toString());
|
|
7207
|
+
setMinimumTopup(initialMinimumTopup.toString());
|
|
7208
|
+
}, [initialMinimumBalance, initialMinimumTopup, open]);
|
|
7209
|
+
|
|
7210
|
+
const handleSave = () => {
|
|
7211
|
+
const balanceValue = parseFloat(minimumBalance) || 0;
|
|
7212
|
+
const topupValue = parseFloat(minimumTopup) || 0;
|
|
7213
|
+
|
|
7214
|
+
onSave?.({
|
|
7215
|
+
minimumBalance: balanceValue,
|
|
7216
|
+
minimumTopup: topupValue,
|
|
7217
|
+
});
|
|
7218
|
+
};
|
|
7219
|
+
|
|
7220
|
+
const handleCancel = () => {
|
|
7221
|
+
// Reset to initial values
|
|
7222
|
+
setMinimumBalance(initialMinimumBalance.toString());
|
|
7223
|
+
setMinimumTopup(initialMinimumTopup.toString());
|
|
7224
|
+
};
|
|
7225
|
+
|
|
7226
|
+
return (
|
|
7227
|
+
<FormModal
|
|
7228
|
+
ref={ref}
|
|
7229
|
+
open={open}
|
|
7230
|
+
onOpenChange={onOpenChange}
|
|
7231
|
+
title="Alert values"
|
|
7232
|
+
onSave={handleSave}
|
|
7233
|
+
onCancel={handleCancel}
|
|
7234
|
+
loading={loading}
|
|
7235
|
+
size="sm"
|
|
7236
|
+
>
|
|
7237
|
+
{/* Minimum Balance Input */}
|
|
7238
|
+
<div className="grid gap-2">
|
|
7239
|
+
<label
|
|
7240
|
+
htmlFor="minimum-balance"
|
|
7241
|
+
className="text-sm font-medium text-semantic-text-secondary"
|
|
7242
|
+
>
|
|
7243
|
+
Minimum balance
|
|
7244
|
+
</label>
|
|
7245
|
+
<div className="relative">
|
|
7246
|
+
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-sm text-semantic-text-muted">
|
|
7247
|
+
{currencySymbol}
|
|
7248
|
+
</span>
|
|
7249
|
+
<Input
|
|
7250
|
+
id="minimum-balance"
|
|
7251
|
+
type="number"
|
|
7252
|
+
value={minimumBalance}
|
|
7253
|
+
onChange={(e) => setMinimumBalance(e.target.value)}
|
|
7254
|
+
className="pl-8"
|
|
7255
|
+
placeholder="0"
|
|
7256
|
+
step="0.01"
|
|
7257
|
+
/>
|
|
7258
|
+
</div>
|
|
7259
|
+
</div>
|
|
7260
|
+
|
|
7261
|
+
{/* Minimum Top-up Input */}
|
|
7262
|
+
<div className="grid gap-2">
|
|
7263
|
+
<label
|
|
7264
|
+
htmlFor="minimum-topup"
|
|
7265
|
+
className="text-sm font-medium text-semantic-text-secondary"
|
|
7266
|
+
>
|
|
7267
|
+
Minimum topup
|
|
7268
|
+
</label>
|
|
7269
|
+
<div className="relative">
|
|
7270
|
+
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-sm text-semantic-text-muted">
|
|
7271
|
+
{currencySymbol}
|
|
7272
|
+
</span>
|
|
7273
|
+
<Input
|
|
7274
|
+
id="minimum-topup"
|
|
7275
|
+
type="number"
|
|
7276
|
+
value={minimumTopup}
|
|
7277
|
+
onChange={(e) => setMinimumTopup(e.target.value)}
|
|
7278
|
+
className="pl-8"
|
|
7279
|
+
placeholder="0"
|
|
7280
|
+
step="0.01"
|
|
7281
|
+
/>
|
|
7282
|
+
</div>
|
|
7283
|
+
</div>
|
|
7284
|
+
</FormModal>
|
|
7285
|
+
);
|
|
7286
|
+
}
|
|
7287
|
+
);
|
|
7288
|
+
|
|
7289
|
+
AlertValuesModal.displayName = "AlertValuesModal";
|
|
7290
|
+
`, prefix)
|
|
7291
|
+
},
|
|
7292
|
+
{
|
|
7293
|
+
name: "index.ts",
|
|
7294
|
+
content: prefixTailwindClasses(`export { AlertConfiguration } from "./alert-configuration";
|
|
7295
|
+
export type { AlertConfigurationProps } from "./alert-configuration";
|
|
7296
|
+
|
|
7297
|
+
export { AlertValuesModal } from "./alert-values-modal";
|
|
7298
|
+
export type { AlertValuesModalProps } from "./alert-values-modal";
|
|
7299
|
+
`, prefix)
|
|
7300
|
+
}
|
|
7301
|
+
]
|
|
7302
|
+
},
|
|
7303
|
+
"auto-pay-setup": {
|
|
7304
|
+
name: "auto-pay-setup",
|
|
7305
|
+
description: "A setup wizard component for configuring automatic payments with payment method selection",
|
|
7306
|
+
dependencies: [
|
|
7307
|
+
"clsx",
|
|
7308
|
+
"tailwind-merge",
|
|
7309
|
+
"lucide-react"
|
|
7310
|
+
],
|
|
7311
|
+
internalDependencies: [
|
|
7312
|
+
"accordion",
|
|
7313
|
+
"button"
|
|
7314
|
+
],
|
|
7315
|
+
isMultiFile: true,
|
|
7316
|
+
directory: "auto-pay-setup",
|
|
7317
|
+
mainFile: "auto-pay-setup.tsx",
|
|
7318
|
+
files: [
|
|
7319
|
+
{
|
|
7320
|
+
name: "auto-pay-setup.tsx",
|
|
7321
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7322
|
+
import { cn } from "../../../lib/utils";
|
|
7323
|
+
import { Button } from "@/components/ui/button";
|
|
7324
|
+
import {
|
|
7325
|
+
Accordion,
|
|
7326
|
+
AccordionItem,
|
|
7327
|
+
AccordionTrigger,
|
|
7328
|
+
AccordionContent,
|
|
7329
|
+
} from "@/components/ui/accordion";
|
|
7330
|
+
import type { AutoPaySetupProps } from "./types";
|
|
7331
|
+
|
|
7332
|
+
/**
|
|
7333
|
+
* AutoPaySetup provides a collapsible panel for setting up automatic payments.
|
|
7334
|
+
* It displays a description, an informational note callout, and a CTA button.
|
|
7335
|
+
*
|
|
7336
|
+
* @example
|
|
7337
|
+
* \`\`\`tsx
|
|
7338
|
+
* <AutoPaySetup
|
|
7339
|
+
* icon={<RefreshCw className="size-5 text-semantic-primary" />}
|
|
7340
|
+
* onCtaClick={() => console.log("Enable auto-pay")}
|
|
7341
|
+
* />
|
|
7342
|
+
* \`\`\`
|
|
7343
|
+
*/
|
|
7344
|
+
export const AutoPaySetup = React.forwardRef<HTMLDivElement, AutoPaySetupProps>(
|
|
7345
|
+
(
|
|
7346
|
+
{
|
|
7347
|
+
title = "Auto-pay setup",
|
|
7348
|
+
subtitle = "Hassle-free monthly billing",
|
|
7349
|
+
icon,
|
|
7350
|
+
bodyText = "Link your internet banking account or enroll your card for recurring payments on MyOperator, where your linked account/card is charged automatically for your subsequent bills and usages on MyOperator",
|
|
7351
|
+
noteText = "For card based subscriptions, your card would be charged minimum of \\u20B91 every month even if there are no usages to keep the subscription active, and \\u20B91 will be added as prepaid amount for your service. Initial deduction of \\u20B95 would be made for subscription, which will be auto-refunded.",
|
|
7352
|
+
noteLabel = "Note:",
|
|
7353
|
+
showCta = true,
|
|
7354
|
+
ctaText = "Enable Auto-Pay",
|
|
7355
|
+
onCtaClick,
|
|
7356
|
+
loading = false,
|
|
7357
|
+
disabled = false,
|
|
7358
|
+
defaultOpen = true,
|
|
7359
|
+
className,
|
|
7360
|
+
},
|
|
7361
|
+
ref
|
|
7362
|
+
) => {
|
|
7363
|
+
return (
|
|
7364
|
+
<div ref={ref} className={cn("w-full", className)}>
|
|
7365
|
+
<Accordion
|
|
7366
|
+
type="single"
|
|
7367
|
+
variant="bordered"
|
|
7368
|
+
defaultValue={defaultOpen ? ["auto-pay-setup"] : []}
|
|
7369
|
+
>
|
|
7370
|
+
<AccordionItem value="auto-pay-setup">
|
|
7371
|
+
<AccordionTrigger className="px-4 py-4">
|
|
7372
|
+
<div className="flex items-center gap-3">
|
|
7373
|
+
{icon && (
|
|
7374
|
+
<div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
|
|
7375
|
+
{icon}
|
|
7376
|
+
</div>
|
|
7377
|
+
)}
|
|
7378
|
+
<div className="flex flex-col gap-1 text-left">
|
|
7379
|
+
<span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
|
|
7380
|
+
{title}
|
|
7381
|
+
</span>
|
|
7382
|
+
<span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
7383
|
+
{subtitle}
|
|
7384
|
+
</span>
|
|
7385
|
+
</div>
|
|
7386
|
+
</div>
|
|
7387
|
+
</AccordionTrigger>
|
|
7388
|
+
|
|
7389
|
+
<AccordionContent>
|
|
7390
|
+
<div className="flex flex-col gap-4 border-t border-semantic-border-layout pt-4">
|
|
7391
|
+
{/* Description */}
|
|
7392
|
+
{bodyText && (
|
|
7393
|
+
<div className="m-0 text-sm font-normal text-semantic-text-primary leading-5 tracking-[0.035px]">
|
|
7394
|
+
{bodyText}
|
|
7395
|
+
</div>
|
|
7396
|
+
)}
|
|
7397
|
+
|
|
7398
|
+
{/* Note callout */}
|
|
7399
|
+
{noteText && (
|
|
7400
|
+
<div className="rounded bg-[var(--semantic-info-25,#f0f7ff)] border border-[#BEDBFF] px-4 py-3">
|
|
7401
|
+
<p className="m-0 text-sm font-normal text-semantic-text-muted leading-5 tracking-[0.035px]">
|
|
7402
|
+
{noteLabel && (
|
|
7403
|
+
<span className="font-medium text-semantic-text-primary">
|
|
7404
|
+
{noteLabel}{" "}
|
|
7405
|
+
</span>
|
|
7406
|
+
)}
|
|
7407
|
+
{noteText}
|
|
7408
|
+
</p>
|
|
7409
|
+
</div>
|
|
7410
|
+
)}
|
|
7411
|
+
|
|
7412
|
+
{/* CTA Button */}
|
|
7413
|
+
{showCta && (
|
|
7414
|
+
<Button
|
|
7415
|
+
variant="default"
|
|
7416
|
+
className="w-full"
|
|
7417
|
+
onClick={onCtaClick}
|
|
7418
|
+
loading={loading}
|
|
7419
|
+
disabled={disabled}
|
|
7420
|
+
>
|
|
7421
|
+
{ctaText}
|
|
7422
|
+
</Button>
|
|
7423
|
+
)}
|
|
7424
|
+
</div>
|
|
7425
|
+
</AccordionContent>
|
|
7426
|
+
</AccordionItem>
|
|
7427
|
+
</Accordion>
|
|
7428
|
+
</div>
|
|
7429
|
+
);
|
|
7430
|
+
}
|
|
7431
|
+
);
|
|
7432
|
+
|
|
7433
|
+
AutoPaySetup.displayName = "AutoPaySetup";
|
|
7434
|
+
`, prefix)
|
|
7435
|
+
},
|
|
7436
|
+
{
|
|
7437
|
+
name: "types.ts",
|
|
7438
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7439
|
+
|
|
7440
|
+
/**
|
|
7441
|
+
* Props for the AutoPaySetup component
|
|
7442
|
+
*/
|
|
7443
|
+
export interface AutoPaySetupProps {
|
|
7444
|
+
// Header
|
|
7445
|
+
/** Title displayed in the accordion header */
|
|
7446
|
+
title?: string;
|
|
7447
|
+
/** Subtitle displayed below the title */
|
|
7448
|
+
subtitle?: string;
|
|
7449
|
+
/** Icon displayed in the header (rendered inside a rounded container) */
|
|
7450
|
+
icon?: React.ReactNode;
|
|
7451
|
+
|
|
7452
|
+
// Body
|
|
7453
|
+
/** Description content displayed below the header when expanded. Accepts a string or JSX (e.g. text with a link). */
|
|
7454
|
+
bodyText?: React.ReactNode;
|
|
7455
|
+
|
|
7456
|
+
// Note callout
|
|
7457
|
+
/** Note/callout text displayed in a highlighted box */
|
|
7458
|
+
noteText?: string;
|
|
7459
|
+
/** Label prefix for the note (e.g., "Note:") */
|
|
7460
|
+
noteLabel?: string;
|
|
7461
|
+
|
|
7462
|
+
// CTA
|
|
7463
|
+
/** Whether to show the CTA button (defaults to true) */
|
|
7464
|
+
showCta?: boolean;
|
|
7465
|
+
/** Text for the CTA button (defaults to "Enable Auto-Pay") */
|
|
7466
|
+
ctaText?: string;
|
|
7467
|
+
/** Callback when CTA button is clicked */
|
|
7468
|
+
onCtaClick?: () => void;
|
|
7469
|
+
/** Whether the CTA button shows loading state */
|
|
7470
|
+
loading?: boolean;
|
|
7471
|
+
/** Whether the CTA button is disabled */
|
|
7472
|
+
disabled?: boolean;
|
|
7473
|
+
|
|
7474
|
+
// Accordion
|
|
7475
|
+
/** Whether the accordion is open by default */
|
|
7476
|
+
defaultOpen?: boolean;
|
|
7477
|
+
|
|
7478
|
+
// Styling
|
|
7479
|
+
/** Additional className for the root element */
|
|
7480
|
+
className?: string;
|
|
7481
|
+
}
|
|
7482
|
+
`, prefix)
|
|
7483
|
+
},
|
|
7484
|
+
{
|
|
7485
|
+
name: "index.ts",
|
|
7486
|
+
content: prefixTailwindClasses(`export { AutoPaySetup } from "./auto-pay-setup";
|
|
7487
|
+
export type { AutoPaySetupProps } from "./types";
|
|
7488
|
+
`, prefix)
|
|
7489
|
+
}
|
|
7490
|
+
]
|
|
7491
|
+
},
|
|
7492
|
+
"bank-details": {
|
|
7493
|
+
name: "bank-details",
|
|
7494
|
+
description: "A component for displaying bank account details with copy-to-clipboard functionality",
|
|
7495
|
+
dependencies: [
|
|
7496
|
+
"clsx",
|
|
7497
|
+
"tailwind-merge",
|
|
7498
|
+
"lucide-react"
|
|
7499
|
+
],
|
|
7500
|
+
internalDependencies: [
|
|
7501
|
+
"accordion"
|
|
7502
|
+
],
|
|
7503
|
+
isMultiFile: true,
|
|
7504
|
+
directory: "bank-details",
|
|
7505
|
+
mainFile: "bank-details.tsx",
|
|
7506
|
+
files: [
|
|
7507
|
+
{
|
|
7508
|
+
name: "bank-details.tsx",
|
|
7509
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7510
|
+
import { Copy, Check } from "lucide-react";
|
|
7511
|
+
import { cn } from "../../../lib/utils";
|
|
7512
|
+
import {
|
|
7513
|
+
Accordion,
|
|
7514
|
+
AccordionItem,
|
|
7515
|
+
AccordionTrigger,
|
|
7516
|
+
AccordionContent,
|
|
7517
|
+
} from "@/components/ui/accordion";
|
|
7518
|
+
import type { BankDetailsProps, BankDetailItem } from "./types";
|
|
7519
|
+
|
|
7520
|
+
/**
|
|
7521
|
+
* BankDetails displays bank account information inside a collapsible accordion
|
|
7522
|
+
* card. Each row shows a label-value pair, with optional copy-to-clipboard
|
|
7523
|
+
* support for individual values.
|
|
7524
|
+
*
|
|
7525
|
+
* @example
|
|
7526
|
+
* \`\`\`tsx
|
|
7527
|
+
* <BankDetails
|
|
7528
|
+
* icon={<Landmark className="size-5 text-semantic-primary" />}
|
|
7529
|
+
* items={[
|
|
7530
|
+
* { label: "Account holder's name", value: "MyOperator" },
|
|
7531
|
+
* { label: "Account Number", value: "2223330026552601", copyable: true },
|
|
7532
|
+
* { label: "IFSC Code", value: "UTIB000RAZP", copyable: true },
|
|
7533
|
+
* { label: "Bank Name", value: "AXIS BANK" },
|
|
7534
|
+
* ]}
|
|
7535
|
+
* />
|
|
7536
|
+
* \`\`\`
|
|
7537
|
+
*/
|
|
7538
|
+
export const BankDetails = React.forwardRef<HTMLDivElement, BankDetailsProps>(
|
|
7539
|
+
(
|
|
7540
|
+
{
|
|
7541
|
+
title = "Bank details",
|
|
7542
|
+
subtitle = "Direct NEFT/RTGS transfer",
|
|
7543
|
+
icon,
|
|
7544
|
+
items,
|
|
7545
|
+
defaultOpen = true,
|
|
7546
|
+
onCopy,
|
|
7547
|
+
className,
|
|
7548
|
+
},
|
|
7549
|
+
ref
|
|
7550
|
+
) => {
|
|
7551
|
+
return (
|
|
7552
|
+
<div ref={ref} className={cn("w-full", className)}>
|
|
7553
|
+
<Accordion
|
|
7554
|
+
type="single"
|
|
7555
|
+
variant="bordered"
|
|
7556
|
+
defaultValue={defaultOpen ? ["bank-details"] : []}
|
|
7557
|
+
>
|
|
7558
|
+
<AccordionItem value="bank-details">
|
|
7559
|
+
<AccordionTrigger className="px-4 py-4">
|
|
7560
|
+
<div className="flex items-center gap-3">
|
|
7561
|
+
{icon && (
|
|
7562
|
+
<div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
|
|
7563
|
+
{icon}
|
|
7564
|
+
</div>
|
|
7565
|
+
)}
|
|
7566
|
+
<div className="flex flex-col gap-1 text-left">
|
|
7567
|
+
<span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
|
|
7568
|
+
{title}
|
|
7569
|
+
</span>
|
|
7570
|
+
<span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
7571
|
+
{subtitle}
|
|
7572
|
+
</span>
|
|
7573
|
+
</div>
|
|
7574
|
+
</div>
|
|
7575
|
+
</AccordionTrigger>
|
|
7576
|
+
|
|
7577
|
+
<AccordionContent>
|
|
7578
|
+
<div className="border-t border-semantic-border-layout pt-4">
|
|
7579
|
+
<div className="rounded-md border border-[var(--semantic-info-200,#e8f1fc)] bg-[var(--semantic-info-25,#f6f8fd)] p-3">
|
|
7580
|
+
<div className="flex flex-col gap-4">
|
|
7581
|
+
{items.map((item, index) => (
|
|
7582
|
+
<BankDetailRow
|
|
7583
|
+
key={index}
|
|
7584
|
+
item={item}
|
|
7585
|
+
onCopy={onCopy}
|
|
7586
|
+
/>
|
|
7587
|
+
))}
|
|
7588
|
+
</div>
|
|
7589
|
+
</div>
|
|
7590
|
+
</div>
|
|
7591
|
+
</AccordionContent>
|
|
7592
|
+
</AccordionItem>
|
|
7593
|
+
</Accordion>
|
|
7594
|
+
</div>
|
|
7595
|
+
);
|
|
7596
|
+
}
|
|
7597
|
+
);
|
|
7598
|
+
|
|
7599
|
+
BankDetails.displayName = "BankDetails";
|
|
7600
|
+
|
|
7601
|
+
/* \u2500\u2500\u2500 Internal row component \u2500\u2500\u2500 */
|
|
7602
|
+
|
|
7603
|
+
function BankDetailRow({
|
|
7604
|
+
item,
|
|
7605
|
+
onCopy,
|
|
7606
|
+
}: {
|
|
7607
|
+
item: BankDetailItem;
|
|
7608
|
+
onCopy?: (item: BankDetailItem) => void;
|
|
7609
|
+
}) {
|
|
7610
|
+
const [copied, setCopied] = React.useState(false);
|
|
7611
|
+
|
|
7612
|
+
const handleCopy = async () => {
|
|
7613
|
+
try {
|
|
7614
|
+
await navigator.clipboard.writeText(item.value);
|
|
7615
|
+
setCopied(true);
|
|
7616
|
+
onCopy?.(item);
|
|
7617
|
+
setTimeout(() => setCopied(false), 2000);
|
|
7618
|
+
} catch {
|
|
7619
|
+
// Clipboard API may fail in insecure contexts; silently ignore
|
|
7620
|
+
}
|
|
7621
|
+
};
|
|
7622
|
+
|
|
7623
|
+
return (
|
|
7624
|
+
<div className="group flex items-center justify-between text-sm tracking-[0.035px]">
|
|
7625
|
+
<span className="text-semantic-text-muted">{item.label}</span>
|
|
7626
|
+
<div className="flex items-center">
|
|
7627
|
+
<span className="text-semantic-text-primary text-right">
|
|
7628
|
+
{item.value}
|
|
7629
|
+
</span>
|
|
7630
|
+
{item.copyable && (
|
|
7631
|
+
<button
|
|
7632
|
+
type="button"
|
|
7633
|
+
onClick={handleCopy}
|
|
7634
|
+
className={cn(
|
|
7635
|
+
"inline-flex items-center justify-center rounded p-0.5 transition-all duration-200 overflow-hidden",
|
|
7636
|
+
copied
|
|
7637
|
+
? "w-5 ml-1.5 opacity-100 text-semantic-success-primary"
|
|
7638
|
+
: "w-0 opacity-0 group-hover:w-5 group-hover:ml-1.5 group-hover:opacity-100 text-semantic-text-muted hover:text-semantic-text-primary"
|
|
7639
|
+
)}
|
|
7640
|
+
aria-label={\`Copy \${item.label}\`}
|
|
7641
|
+
>
|
|
7642
|
+
{copied ? (
|
|
7643
|
+
<Check className="size-3.5 shrink-0" />
|
|
7644
|
+
) : (
|
|
7645
|
+
<Copy className="size-3.5 shrink-0" />
|
|
7646
|
+
)}
|
|
7647
|
+
</button>
|
|
7648
|
+
)}
|
|
7649
|
+
</div>
|
|
7650
|
+
</div>
|
|
7651
|
+
);
|
|
7652
|
+
}
|
|
7653
|
+
`, prefix)
|
|
7654
|
+
},
|
|
7655
|
+
{
|
|
7656
|
+
name: "types.ts",
|
|
7657
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7658
|
+
|
|
7659
|
+
/**
|
|
7660
|
+
* A single row of bank detail information.
|
|
7661
|
+
*/
|
|
7662
|
+
export interface BankDetailItem {
|
|
7663
|
+
/** Label text displayed on the left (e.g., "Account Number") */
|
|
7664
|
+
label: string;
|
|
7665
|
+
/** Value text displayed on the right (e.g., "2223330026552601") */
|
|
7666
|
+
value: string;
|
|
7667
|
+
/** Whether to show a copy-to-clipboard button for this item's value */
|
|
7668
|
+
copyable?: boolean;
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
/**
|
|
7672
|
+
* Props for the BankDetails component
|
|
7673
|
+
*/
|
|
7674
|
+
export interface BankDetailsProps {
|
|
7675
|
+
// Header
|
|
7676
|
+
/** Title displayed in the accordion header */
|
|
7677
|
+
title?: string;
|
|
7678
|
+
/** Subtitle displayed below the title */
|
|
7679
|
+
subtitle?: string;
|
|
7680
|
+
/** Icon displayed in the header (rendered inside a rounded container) */
|
|
7681
|
+
icon?: React.ReactNode;
|
|
7682
|
+
|
|
7683
|
+
// Data
|
|
7684
|
+
/** Array of bank detail items to display */
|
|
7685
|
+
items: BankDetailItem[];
|
|
7686
|
+
|
|
7687
|
+
// Accordion
|
|
7688
|
+
/** Whether the accordion is open by default */
|
|
7689
|
+
defaultOpen?: boolean;
|
|
7690
|
+
|
|
7691
|
+
// Callbacks
|
|
7692
|
+
/** Callback fired when a value is copied to clipboard */
|
|
7693
|
+
onCopy?: (item: BankDetailItem) => void;
|
|
7694
|
+
|
|
7695
|
+
// Styling
|
|
7696
|
+
/** Additional className for the root element */
|
|
7697
|
+
className?: string;
|
|
7698
|
+
}
|
|
7699
|
+
`, prefix)
|
|
7700
|
+
},
|
|
7701
|
+
{
|
|
7702
|
+
name: "index.ts",
|
|
7703
|
+
content: prefixTailwindClasses(`export { BankDetails } from "./bank-details";
|
|
7704
|
+
export type { BankDetailsProps, BankDetailItem } from "./types";
|
|
7705
|
+
`, prefix)
|
|
7706
|
+
}
|
|
7707
|
+
]
|
|
7708
|
+
},
|
|
7709
|
+
"payment-summary": {
|
|
7710
|
+
name: "payment-summary",
|
|
7711
|
+
description: "A component for displaying payment summary with line items and total",
|
|
7712
|
+
dependencies: [
|
|
7713
|
+
"clsx",
|
|
7714
|
+
"tailwind-merge",
|
|
7715
|
+
"lucide-react"
|
|
7716
|
+
],
|
|
7717
|
+
internalDependencies: [
|
|
7718
|
+
"tooltip"
|
|
7719
|
+
],
|
|
7720
|
+
isMultiFile: true,
|
|
7721
|
+
directory: "payment-summary",
|
|
7722
|
+
mainFile: "payment-summary.tsx",
|
|
7723
|
+
files: [
|
|
7724
|
+
{
|
|
7725
|
+
name: "payment-summary.tsx",
|
|
7726
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7727
|
+
import { Info } from "lucide-react";
|
|
7728
|
+
import { cn } from "../../../lib/utils";
|
|
7729
|
+
import {
|
|
7730
|
+
Tooltip,
|
|
7731
|
+
TooltipContent,
|
|
7732
|
+
TooltipProvider,
|
|
7733
|
+
TooltipTrigger,
|
|
7734
|
+
TooltipArrow,
|
|
7735
|
+
} from "@/components/ui/tooltip";
|
|
7736
|
+
|
|
7737
|
+
/**
|
|
7738
|
+
* Represents a single row in the payment summary.
|
|
7739
|
+
*/
|
|
7740
|
+
export interface PaymentSummaryItem {
|
|
7741
|
+
/** Label text displayed on the left */
|
|
7742
|
+
label: string;
|
|
7743
|
+
/** Value text displayed on the right */
|
|
7744
|
+
value: string;
|
|
7745
|
+
/** Color variant for the value text */
|
|
7746
|
+
valueColor?: "default" | "success" | "error";
|
|
7747
|
+
/** Tooltip text shown when hovering the info icon next to the label */
|
|
7748
|
+
tooltip?: string;
|
|
7749
|
+
/** Whether to render label in bold (semibold weight) */
|
|
7750
|
+
bold?: boolean;
|
|
7751
|
+
/** Font size for the value \u2014 "lg" renders at 18px semibold */
|
|
7752
|
+
valueSize?: "default" | "lg";
|
|
7753
|
+
}
|
|
7754
|
+
|
|
7755
|
+
export interface PaymentSummaryProps {
|
|
7756
|
+
/** Line items displayed in the top section */
|
|
7757
|
+
items: PaymentSummaryItem[];
|
|
7758
|
+
/** Summary items displayed below the divider (e.g. totals) */
|
|
7759
|
+
summaryItems?: PaymentSummaryItem[];
|
|
7760
|
+
/** Custom className for the outer container */
|
|
7761
|
+
className?: string;
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
const valueColorMap: Record<string, string> = {
|
|
7765
|
+
default: "text-semantic-text-primary",
|
|
7766
|
+
success: "text-semantic-success-primary",
|
|
7767
|
+
error: "text-semantic-error-primary",
|
|
7768
|
+
};
|
|
7769
|
+
|
|
7770
|
+
const SummaryRow = ({ item }: { item: PaymentSummaryItem }) => (
|
|
7771
|
+
<div className="flex items-center justify-between w-full">
|
|
7772
|
+
<div className="flex items-center gap-1.5">
|
|
7773
|
+
<span
|
|
7774
|
+
className={cn(
|
|
7775
|
+
"text-sm tracking-[0.035px]",
|
|
7776
|
+
item.bold
|
|
7777
|
+
? "font-semibold text-semantic-text-primary"
|
|
7778
|
+
: "text-semantic-text-muted"
|
|
7779
|
+
)}
|
|
7780
|
+
>
|
|
7781
|
+
{item.label}
|
|
7782
|
+
</span>
|
|
7783
|
+
{item.tooltip && (
|
|
7784
|
+
<Tooltip>
|
|
7785
|
+
<TooltipTrigger asChild>
|
|
7786
|
+
<button
|
|
7787
|
+
type="button"
|
|
7788
|
+
className="inline-flex items-center justify-center rounded-full w-5 h-5 text-semantic-text-muted hover:text-semantic-text-primary hover:bg-semantic-bg-ui transition-colors"
|
|
7789
|
+
aria-label={\`Info about \${item.label}\`}
|
|
7790
|
+
>
|
|
7791
|
+
<Info className="h-3.5 w-3.5" />
|
|
7792
|
+
</button>
|
|
7793
|
+
</TooltipTrigger>
|
|
7794
|
+
<TooltipContent>
|
|
7795
|
+
<TooltipArrow />
|
|
7796
|
+
{item.tooltip}
|
|
7797
|
+
</TooltipContent>
|
|
7798
|
+
</Tooltip>
|
|
7799
|
+
)}
|
|
7800
|
+
</div>
|
|
7801
|
+
<span
|
|
7802
|
+
className={cn(
|
|
7803
|
+
"tracking-[0.035px]",
|
|
7804
|
+
item.valueSize === "lg" ? "text-lg font-semibold" : "text-sm",
|
|
7805
|
+
valueColorMap[item.valueColor ?? "default"]
|
|
7806
|
+
)}
|
|
7807
|
+
>
|
|
7808
|
+
{item.value}
|
|
7809
|
+
</span>
|
|
7810
|
+
</div>
|
|
7811
|
+
);
|
|
7812
|
+
|
|
7813
|
+
/**
|
|
7814
|
+
* PaymentSummary displays a card with line-item rows and an optional totals section
|
|
7815
|
+
* separated by a divider. Values can be color-coded (default, success, error) and
|
|
7816
|
+
* labels can optionally show info tooltips.
|
|
7817
|
+
*
|
|
7818
|
+
* @example
|
|
7819
|
+
* \`\`\`tsx
|
|
7820
|
+
* <PaymentSummary
|
|
7821
|
+
* items={[
|
|
7822
|
+
* { label: "Pending Rental", value: "\u20B90.00" },
|
|
7823
|
+
* { label: "Current Usage", value: "\u20B9163.98" },
|
|
7824
|
+
* { label: "Prepaid Wallet", value: "\u20B978,682.92", valueColor: "success" },
|
|
7825
|
+
* ]}
|
|
7826
|
+
* summaryItems={[
|
|
7827
|
+
* { label: "Total amount due", value: "-\u20B978,518.94", valueColor: "error", valueSize: "lg", bold: true, tooltip: "Sum of all charges" },
|
|
7828
|
+
* { label: "Credit limit", value: "\u20B910,000.00", tooltip: "Your current credit limit" },
|
|
7829
|
+
* ]}
|
|
7830
|
+
* />
|
|
7831
|
+
* \`\`\`
|
|
7832
|
+
*/
|
|
7833
|
+
export const PaymentSummary = React.forwardRef<
|
|
7834
|
+
HTMLDivElement,
|
|
7835
|
+
PaymentSummaryProps
|
|
7836
|
+
>(({ items, summaryItems, className }, ref) => {
|
|
7837
|
+
return (
|
|
7838
|
+
<TooltipProvider delayDuration={100}>
|
|
7839
|
+
<div
|
|
7840
|
+
ref={ref}
|
|
7841
|
+
className={cn(
|
|
7842
|
+
"rounded-lg border border-semantic-border-layout bg-semantic-bg-primary p-5",
|
|
7843
|
+
className
|
|
7844
|
+
)}
|
|
7845
|
+
>
|
|
7846
|
+
<div className="flex flex-col gap-5">
|
|
7847
|
+
{/* Line items */}
|
|
7848
|
+
{items.length > 0 && (
|
|
7849
|
+
<div
|
|
7850
|
+
className={cn(
|
|
7851
|
+
"flex flex-col gap-5",
|
|
7852
|
+
summaryItems && summaryItems.length > 0 &&
|
|
7853
|
+
"border-b border-semantic-border-layout pb-5"
|
|
7854
|
+
)}
|
|
7855
|
+
>
|
|
7856
|
+
{items.map((item, index) => (
|
|
7857
|
+
<SummaryRow key={index} item={item} />
|
|
7858
|
+
))}
|
|
7859
|
+
</div>
|
|
7860
|
+
)}
|
|
7861
|
+
|
|
7862
|
+
{/* Summary items (below divider) */}
|
|
7863
|
+
{summaryItems && summaryItems.length > 0 && (
|
|
7864
|
+
<div className="flex flex-col gap-5">
|
|
7865
|
+
{summaryItems.map((item, index) => (
|
|
7866
|
+
<SummaryRow key={index} item={item} />
|
|
7867
|
+
))}
|
|
7868
|
+
</div>
|
|
7869
|
+
)}
|
|
7870
|
+
</div>
|
|
7871
|
+
</div>
|
|
7872
|
+
</TooltipProvider>
|
|
7873
|
+
);
|
|
7874
|
+
});
|
|
7875
|
+
|
|
7876
|
+
PaymentSummary.displayName = "PaymentSummary";
|
|
7877
|
+
`, prefix)
|
|
7878
|
+
},
|
|
7879
|
+
{
|
|
7880
|
+
name: "index.ts",
|
|
7881
|
+
content: prefixTailwindClasses(`export { PaymentSummary } from "./payment-summary";
|
|
7882
|
+
export type {
|
|
7883
|
+
PaymentSummaryProps,
|
|
7884
|
+
PaymentSummaryItem,
|
|
7885
|
+
} from "./payment-summary";
|
|
7886
|
+
`, prefix)
|
|
7887
|
+
}
|
|
7888
|
+
]
|
|
7889
|
+
},
|
|
7890
|
+
"wallet-topup": {
|
|
7891
|
+
name: "wallet-topup",
|
|
7892
|
+
description: "A component for wallet top-up with amount selection and coupon support",
|
|
7893
|
+
dependencies: [
|
|
7894
|
+
"clsx",
|
|
7895
|
+
"tailwind-merge",
|
|
7896
|
+
"lucide-react"
|
|
7897
|
+
],
|
|
7898
|
+
internalDependencies: [
|
|
7899
|
+
"accordion",
|
|
7900
|
+
"button",
|
|
7901
|
+
"input"
|
|
7902
|
+
],
|
|
7903
|
+
isMultiFile: true,
|
|
7904
|
+
directory: "wallet-topup",
|
|
7905
|
+
mainFile: "wallet-topup.tsx",
|
|
7906
|
+
files: [
|
|
7907
|
+
{
|
|
7908
|
+
name: "wallet-topup.tsx",
|
|
7909
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
7910
|
+
import { Check, Ticket } from "lucide-react";
|
|
7911
|
+
import { cn } from "../../../lib/utils";
|
|
7912
|
+
import { Button } from "@/components/ui/button";
|
|
7913
|
+
import { Input } from "@/components/ui/input";
|
|
7914
|
+
import {
|
|
7915
|
+
Accordion,
|
|
7916
|
+
AccordionItem,
|
|
7917
|
+
AccordionTrigger,
|
|
7918
|
+
AccordionContent,
|
|
7919
|
+
} from "@/components/ui/accordion";
|
|
7920
|
+
import type { AmountOption, WalletTopupProps } from "./types";
|
|
7921
|
+
|
|
7922
|
+
/**
|
|
7923
|
+
* Normalize amount option to a consistent format
|
|
7924
|
+
*/
|
|
7925
|
+
function normalizeAmountOption(option: number | AmountOption): AmountOption {
|
|
7926
|
+
return typeof option === "number" ? { value: option } : option;
|
|
7927
|
+
}
|
|
7928
|
+
|
|
7929
|
+
/**
|
|
7930
|
+
* Format currency amount with symbol
|
|
7931
|
+
*/
|
|
7932
|
+
function formatCurrency(amount: number, symbol: string = "\u20B9"): string {
|
|
7933
|
+
return \`\${symbol}\${amount.toLocaleString("en-IN")}\`;
|
|
7934
|
+
}
|
|
7935
|
+
|
|
7936
|
+
/**
|
|
7937
|
+
* WalletTopup provides a collapsible panel for wallet recharge with
|
|
7938
|
+
* preset amount selection, custom amount input, voucher link, and payment CTA.
|
|
7939
|
+
*
|
|
7940
|
+
* @example
|
|
7941
|
+
* \`\`\`tsx
|
|
7942
|
+
* <WalletTopup
|
|
7943
|
+
* icon={<CreditCard className="size-5 text-semantic-primary" />}
|
|
7944
|
+
* amounts={[500, 1000, 5000, 10000]}
|
|
7945
|
+
* onPay={(amount) => console.log("Pay", amount)}
|
|
7946
|
+
* />
|
|
7947
|
+
* \`\`\`
|
|
7948
|
+
*/
|
|
7949
|
+
export const WalletTopup = React.forwardRef<HTMLDivElement, WalletTopupProps>(
|
|
7950
|
+
(
|
|
7951
|
+
{
|
|
7952
|
+
title = "Instant wallet top-up",
|
|
7953
|
+
description = "Add funds to your account balance",
|
|
7954
|
+
icon,
|
|
7955
|
+
amounts = [500, 1000, 5000, 10000],
|
|
7956
|
+
selectedAmount: controlledAmount,
|
|
7957
|
+
defaultSelectedAmount,
|
|
7958
|
+
onAmountChange,
|
|
7959
|
+
amountSectionLabel = "Select Amount",
|
|
7960
|
+
customAmount: controlledCustomAmount,
|
|
7961
|
+
onCustomAmountChange,
|
|
7962
|
+
customAmountPlaceholder = "Enter amount",
|
|
7963
|
+
customAmountLabel = "Custom Amount",
|
|
7964
|
+
currencySymbol = "\u20B9",
|
|
7965
|
+
showVoucherLink = true,
|
|
7966
|
+
voucherLinkText = "Have an offline code or voucher?",
|
|
7967
|
+
voucherIcon = <Ticket className="size-4" />,
|
|
7968
|
+
onVoucherClick,
|
|
7969
|
+
voucherCode: controlledVoucherCode,
|
|
7970
|
+
onVoucherCodeChange,
|
|
7971
|
+
voucherCodePlaceholder = "XXXX-XXXX-XXXX",
|
|
7972
|
+
voucherCodeLabel = "Enter Offline Code",
|
|
7973
|
+
voucherCancelText = "Cancel",
|
|
7974
|
+
voucherCodePattern,
|
|
7975
|
+
validateVoucherCode,
|
|
7976
|
+
redeemText = "Redeem voucher",
|
|
7977
|
+
onRedeem,
|
|
7978
|
+
ctaText,
|
|
7979
|
+
onPay,
|
|
7980
|
+
loading = false,
|
|
7981
|
+
disabled = false,
|
|
7982
|
+
defaultOpen = true,
|
|
7983
|
+
className,
|
|
7984
|
+
},
|
|
7985
|
+
ref
|
|
7986
|
+
) => {
|
|
7987
|
+
// Controlled/uncontrolled amount selection
|
|
7988
|
+
const isControlled = controlledAmount !== undefined;
|
|
7989
|
+
const [internalAmount, setInternalAmount] = React.useState<number | null>(
|
|
7990
|
+
defaultSelectedAmount ?? null
|
|
7991
|
+
);
|
|
7992
|
+
const selectedValue = isControlled ? controlledAmount : internalAmount;
|
|
7993
|
+
|
|
7994
|
+
// Custom amount state
|
|
7995
|
+
const isCustomControlled = controlledCustomAmount !== undefined;
|
|
7996
|
+
const [internalCustom, setInternalCustom] = React.useState("");
|
|
7997
|
+
const customValue = isCustomControlled
|
|
7998
|
+
? controlledCustomAmount
|
|
7999
|
+
: internalCustom;
|
|
8000
|
+
|
|
8001
|
+
// Voucher code input state
|
|
8002
|
+
const [showVoucherInput, setShowVoucherInput] = React.useState(false);
|
|
8003
|
+
const isVoucherCodeControlled = controlledVoucherCode !== undefined;
|
|
8004
|
+
const [internalVoucherCode, setInternalVoucherCode] = React.useState("");
|
|
8005
|
+
const voucherCodeValue = isVoucherCodeControlled
|
|
8006
|
+
? controlledVoucherCode
|
|
8007
|
+
: internalVoucherCode;
|
|
8008
|
+
|
|
8009
|
+
const handleVoucherLinkClick = () => {
|
|
8010
|
+
setShowVoucherInput(true);
|
|
8011
|
+
onVoucherClick?.();
|
|
8012
|
+
};
|
|
8013
|
+
|
|
8014
|
+
const handleVoucherCancel = () => {
|
|
8015
|
+
setShowVoucherInput(false);
|
|
8016
|
+
if (!isVoucherCodeControlled) {
|
|
8017
|
+
setInternalVoucherCode("");
|
|
8018
|
+
}
|
|
8019
|
+
onVoucherCodeChange?.("");
|
|
8020
|
+
};
|
|
8021
|
+
|
|
8022
|
+
const handleVoucherCodeChange = (
|
|
8023
|
+
e: React.ChangeEvent<HTMLInputElement>
|
|
8024
|
+
) => {
|
|
8025
|
+
const value = e.target.value;
|
|
8026
|
+
if (!isVoucherCodeControlled) {
|
|
8027
|
+
setInternalVoucherCode(value);
|
|
8028
|
+
}
|
|
8029
|
+
onVoucherCodeChange?.(value);
|
|
8030
|
+
};
|
|
8031
|
+
|
|
8032
|
+
const isVoucherCodeValid = React.useMemo(() => {
|
|
8033
|
+
if (!voucherCodeValue) return false;
|
|
8034
|
+
if (validateVoucherCode) return validateVoucherCode(voucherCodeValue);
|
|
8035
|
+
if (voucherCodePattern) return voucherCodePattern.test(voucherCodeValue);
|
|
8036
|
+
return true;
|
|
8037
|
+
}, [voucherCodeValue, validateVoucherCode, voucherCodePattern]);
|
|
8038
|
+
|
|
8039
|
+
const handleRedeem = () => {
|
|
8040
|
+
if (isVoucherCodeValid) {
|
|
8041
|
+
onRedeem?.(voucherCodeValue);
|
|
8042
|
+
}
|
|
8043
|
+
};
|
|
8044
|
+
|
|
8045
|
+
const normalizedAmounts = amounts.map(normalizeAmountOption);
|
|
8046
|
+
|
|
8047
|
+
const handleAmountSelect = (value: number) => {
|
|
8048
|
+
const newValue = selectedValue === value ? null : value;
|
|
8049
|
+
if (!isControlled) {
|
|
8050
|
+
setInternalAmount(newValue);
|
|
8051
|
+
}
|
|
8052
|
+
// Clear custom amount when preset is selected
|
|
8053
|
+
if (!isCustomControlled && newValue !== null) {
|
|
8054
|
+
setInternalCustom("");
|
|
8055
|
+
}
|
|
8056
|
+
onAmountChange?.(newValue);
|
|
8057
|
+
};
|
|
8058
|
+
|
|
8059
|
+
const handleCustomAmountChange = (
|
|
8060
|
+
e: React.ChangeEvent<HTMLInputElement>
|
|
8061
|
+
) => {
|
|
8062
|
+
const value = e.target.value;
|
|
8063
|
+
if (!isCustomControlled) {
|
|
8064
|
+
setInternalCustom(value);
|
|
8065
|
+
}
|
|
8066
|
+
// Clear preset selection when custom amount is entered
|
|
8067
|
+
if (value && !isControlled) {
|
|
8068
|
+
setInternalAmount(null);
|
|
8069
|
+
}
|
|
8070
|
+
if (value) {
|
|
8071
|
+
onAmountChange?.(null);
|
|
8072
|
+
}
|
|
8073
|
+
onCustomAmountChange?.(value);
|
|
8074
|
+
};
|
|
8075
|
+
|
|
8076
|
+
// Determine the effective pay amount
|
|
8077
|
+
const payAmount =
|
|
8078
|
+
selectedValue ?? (customValue ? Number(customValue) : 0);
|
|
8079
|
+
|
|
8080
|
+
const handlePay = () => {
|
|
8081
|
+
if (payAmount > 0) {
|
|
8082
|
+
onPay?.(payAmount);
|
|
8083
|
+
}
|
|
8084
|
+
};
|
|
8085
|
+
|
|
8086
|
+
const buttonText =
|
|
8087
|
+
ctaText ||
|
|
8088
|
+
(payAmount > 0
|
|
8089
|
+
? \`Pay \${formatCurrency(payAmount, currencySymbol)} now\`
|
|
8090
|
+
: "Select an amount");
|
|
8091
|
+
|
|
8092
|
+
return (
|
|
8093
|
+
<div ref={ref} className={cn("w-full", className)}>
|
|
8094
|
+
<Accordion
|
|
8095
|
+
type="single"
|
|
8096
|
+
variant="bordered"
|
|
8097
|
+
defaultValue={defaultOpen ? ["wallet-topup"] : []}
|
|
8098
|
+
>
|
|
8099
|
+
<AccordionItem value="wallet-topup">
|
|
8100
|
+
<AccordionTrigger className="px-4 py-4">
|
|
8101
|
+
<div className="flex items-center gap-3">
|
|
8102
|
+
{icon && (
|
|
8103
|
+
<div className="flex items-center justify-center size-10 rounded-[10px] bg-[var(--semantic-info-surface)] shrink-0">
|
|
8104
|
+
{icon}
|
|
8105
|
+
</div>
|
|
8106
|
+
)}
|
|
8107
|
+
<div className="flex flex-col gap-1 text-left">
|
|
8108
|
+
<span className="text-sm font-semibold text-semantic-text-primary tracking-[0.01px]">
|
|
8109
|
+
{title}
|
|
8110
|
+
</span>
|
|
8111
|
+
<span className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
8112
|
+
{description}
|
|
8113
|
+
</span>
|
|
8114
|
+
</div>
|
|
8115
|
+
</div>
|
|
8116
|
+
</AccordionTrigger>
|
|
8117
|
+
|
|
8118
|
+
<AccordionContent>
|
|
8119
|
+
<div className="flex flex-col gap-6 border-t border-semantic-border-layout pt-4">
|
|
8120
|
+
{/* Amount Selection */}
|
|
8121
|
+
<div className="flex flex-col gap-1.5">
|
|
8122
|
+
<label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
8123
|
+
{amountSectionLabel}
|
|
8124
|
+
</label>
|
|
8125
|
+
<div className="grid grid-cols-2 gap-4">
|
|
8126
|
+
{normalizedAmounts.map((option) => {
|
|
8127
|
+
const isSelected = selectedValue === option.value;
|
|
8128
|
+
return (
|
|
8129
|
+
<button
|
|
8130
|
+
key={option.value}
|
|
8131
|
+
type="button"
|
|
8132
|
+
role="radio"
|
|
8133
|
+
aria-checked={isSelected}
|
|
8134
|
+
onClick={() => handleAmountSelect(option.value)}
|
|
8135
|
+
className={cn(
|
|
8136
|
+
"flex items-center justify-between h-10 px-4 py-2.5 rounded text-sm text-semantic-text-primary transition-all cursor-pointer",
|
|
8137
|
+
isSelected
|
|
8138
|
+
? "border border-semantic-primary shadow-sm"
|
|
8139
|
+
: "border border-semantic-border-input hover:border-semantic-text-muted"
|
|
8140
|
+
)}
|
|
8141
|
+
>
|
|
8142
|
+
<span>
|
|
8143
|
+
{option.label ||
|
|
8144
|
+
formatCurrency(option.value, currencySymbol)}
|
|
8145
|
+
</span>
|
|
8146
|
+
{isSelected && (
|
|
8147
|
+
<Check className="size-5 text-semantic-primary" />
|
|
8148
|
+
)}
|
|
8149
|
+
</button>
|
|
8150
|
+
);
|
|
8151
|
+
})}
|
|
8152
|
+
</div>
|
|
8153
|
+
</div>
|
|
8154
|
+
|
|
8155
|
+
{/* Custom Amount */}
|
|
8156
|
+
<div className="flex flex-col gap-1.5">
|
|
8157
|
+
<label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
8158
|
+
{customAmountLabel}
|
|
8159
|
+
</label>
|
|
8160
|
+
<Input
|
|
8161
|
+
type="number"
|
|
8162
|
+
placeholder={customAmountPlaceholder}
|
|
8163
|
+
value={customValue}
|
|
8164
|
+
onChange={handleCustomAmountChange}
|
|
8165
|
+
/>
|
|
8166
|
+
</div>
|
|
8167
|
+
|
|
8168
|
+
{/* Voucher Link or Voucher Code Input */}
|
|
8169
|
+
{showVoucherLink && !showVoucherInput && (
|
|
8170
|
+
<button
|
|
8171
|
+
type="button"
|
|
8172
|
+
onClick={handleVoucherLinkClick}
|
|
8173
|
+
className="flex items-center gap-2 text-sm text-semantic-text-link tracking-[0.035px] hover:underline w-fit"
|
|
8174
|
+
>
|
|
8175
|
+
{voucherIcon}
|
|
8176
|
+
<span>{voucherLinkText}</span>
|
|
8177
|
+
</button>
|
|
8178
|
+
)}
|
|
8179
|
+
|
|
8180
|
+
{showVoucherInput && (
|
|
8181
|
+
<div className="flex flex-col gap-1.5">
|
|
8182
|
+
<div className="flex items-center justify-between">
|
|
8183
|
+
<label className="text-xs font-normal text-semantic-text-muted tracking-[0.048px]">
|
|
8184
|
+
{voucherCodeLabel}
|
|
8185
|
+
</label>
|
|
8186
|
+
<button
|
|
8187
|
+
type="button"
|
|
8188
|
+
onClick={handleVoucherCancel}
|
|
8189
|
+
className="text-xs text-semantic-text-link tracking-[0.048px] hover:underline"
|
|
8190
|
+
>
|
|
8191
|
+
{voucherCancelText}
|
|
8192
|
+
</button>
|
|
8193
|
+
</div>
|
|
8194
|
+
<Input
|
|
8195
|
+
placeholder={voucherCodePlaceholder}
|
|
8196
|
+
value={voucherCodeValue}
|
|
8197
|
+
onChange={handleVoucherCodeChange}
|
|
8198
|
+
/>
|
|
8199
|
+
</div>
|
|
8200
|
+
)}
|
|
8201
|
+
|
|
8202
|
+
{/* CTA Button */}
|
|
8203
|
+
{showVoucherInput ? (
|
|
8204
|
+
<Button
|
|
8205
|
+
variant="default"
|
|
8206
|
+
className="w-full bg-[var(--semantic-success-primary)] hover:bg-[var(--semantic-success-hover)]"
|
|
8207
|
+
onClick={handleRedeem}
|
|
8208
|
+
loading={loading}
|
|
8209
|
+
disabled={disabled || !isVoucherCodeValid}
|
|
8210
|
+
>
|
|
8211
|
+
{redeemText}
|
|
8212
|
+
</Button>
|
|
8213
|
+
) : (
|
|
8214
|
+
<Button
|
|
8215
|
+
variant="default"
|
|
8216
|
+
className="w-full"
|
|
8217
|
+
onClick={handlePay}
|
|
8218
|
+
loading={loading}
|
|
8219
|
+
disabled={disabled || payAmount <= 0}
|
|
8220
|
+
>
|
|
8221
|
+
{buttonText}
|
|
8222
|
+
</Button>
|
|
8223
|
+
)}
|
|
8224
|
+
</div>
|
|
8225
|
+
</AccordionContent>
|
|
8226
|
+
</AccordionItem>
|
|
8227
|
+
</Accordion>
|
|
8228
|
+
</div>
|
|
8229
|
+
);
|
|
8230
|
+
}
|
|
8231
|
+
);
|
|
8232
|
+
|
|
8233
|
+
WalletTopup.displayName = "WalletTopup";
|
|
8234
|
+
`, prefix)
|
|
8235
|
+
},
|
|
8236
|
+
{
|
|
8237
|
+
name: "types.ts",
|
|
8238
|
+
content: prefixTailwindClasses(`import * as React from "react";
|
|
8239
|
+
|
|
8240
|
+
/**
|
|
8241
|
+
* Represents a preset amount option in the selector grid
|
|
8242
|
+
*/
|
|
8243
|
+
export interface AmountOption {
|
|
8244
|
+
/** The numeric value of the amount */
|
|
8245
|
+
value: number;
|
|
8246
|
+
/** Optional custom display label (defaults to formatted currency) */
|
|
8247
|
+
label?: string;
|
|
8248
|
+
}
|
|
8249
|
+
|
|
8250
|
+
/**
|
|
8251
|
+
* Props for the WalletTopup component
|
|
8252
|
+
*/
|
|
8253
|
+
export interface WalletTopupProps {
|
|
8254
|
+
// Header
|
|
8255
|
+
/** Title displayed in the accordion header */
|
|
8256
|
+
title?: string;
|
|
8257
|
+
/** Description displayed below the title */
|
|
8258
|
+
description?: string;
|
|
8259
|
+
/** Icon displayed in the header (rendered inside a rounded container) */
|
|
8260
|
+
icon?: React.ReactNode;
|
|
8261
|
+
|
|
8262
|
+
// Amount selection
|
|
8263
|
+
/** Preset amount options to display in the grid */
|
|
8264
|
+
amounts?: number[] | AmountOption[];
|
|
8265
|
+
/** Currently selected amount (controlled) */
|
|
8266
|
+
selectedAmount?: number | null;
|
|
8267
|
+
/** Default selected amount (uncontrolled) */
|
|
8268
|
+
defaultSelectedAmount?: number;
|
|
8269
|
+
/** Callback when amount selection changes */
|
|
8270
|
+
onAmountChange?: (amount: number | null) => void;
|
|
8271
|
+
/** Label for the amount selection section */
|
|
8272
|
+
amountSectionLabel?: string;
|
|
8273
|
+
|
|
8274
|
+
// Custom amount
|
|
8275
|
+
/** Custom amount input value (controlled) */
|
|
8276
|
+
customAmount?: string;
|
|
8277
|
+
/** Callback when custom amount input changes */
|
|
8278
|
+
onCustomAmountChange?: (value: string) => void;
|
|
8279
|
+
/** Placeholder text for custom amount input */
|
|
8280
|
+
customAmountPlaceholder?: string;
|
|
8281
|
+
/** Label for the custom amount field */
|
|
8282
|
+
customAmountLabel?: string;
|
|
8283
|
+
|
|
8284
|
+
// Currency
|
|
8285
|
+
/** Currency symbol (default: "\u20B9") */
|
|
8286
|
+
currencySymbol?: string;
|
|
8287
|
+
|
|
8288
|
+
// Voucher link
|
|
8289
|
+
/** Whether to show the voucher/code link */
|
|
8290
|
+
showVoucherLink?: boolean;
|
|
8291
|
+
/** Custom text for the voucher link */
|
|
8292
|
+
voucherLinkText?: string;
|
|
8293
|
+
/** Icon for the voucher link */
|
|
8294
|
+
voucherIcon?: React.ReactNode;
|
|
8295
|
+
/** Callback when voucher link is clicked (also toggles inline code input) */
|
|
8296
|
+
onVoucherClick?: () => void;
|
|
8297
|
+
|
|
8298
|
+
// Voucher code input
|
|
8299
|
+
/** Voucher code value (controlled) */
|
|
8300
|
+
voucherCode?: string;
|
|
8301
|
+
/** Callback when voucher code changes */
|
|
8302
|
+
onVoucherCodeChange?: (code: string) => void;
|
|
8303
|
+
/** Placeholder for voucher code input */
|
|
8304
|
+
voucherCodePlaceholder?: string;
|
|
8305
|
+
/** Label for voucher code input */
|
|
8306
|
+
voucherCodeLabel?: string;
|
|
8307
|
+
/** Text for cancel link in voucher mode */
|
|
8308
|
+
voucherCancelText?: string;
|
|
8309
|
+
/** Regex pattern the voucher code must match to enable redeem (e.g. /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/) */
|
|
8310
|
+
voucherCodePattern?: RegExp;
|
|
8311
|
+
/** Custom validator function \u2014 return true if code is valid. Takes priority over voucherCodePattern. */
|
|
8312
|
+
validateVoucherCode?: (code: string) => boolean;
|
|
8313
|
+
/** Text for the redeem button */
|
|
8314
|
+
redeemText?: string;
|
|
8315
|
+
/** Callback when redeem voucher is clicked */
|
|
8316
|
+
onRedeem?: (code: string) => void;
|
|
8317
|
+
|
|
8318
|
+
// CTA
|
|
8319
|
+
/** Text for the pay button (defaults to "Pay {amount} now") */
|
|
8320
|
+
ctaText?: string;
|
|
8321
|
+
/** Callback when pay button is clicked */
|
|
8322
|
+
onPay?: (amount: number) => void;
|
|
8323
|
+
/** Whether the pay button shows loading state */
|
|
8324
|
+
loading?: boolean;
|
|
8325
|
+
/** Whether the pay button is disabled */
|
|
8326
|
+
disabled?: boolean;
|
|
8327
|
+
|
|
8328
|
+
// Accordion
|
|
8329
|
+
/** Whether the accordion is open by default */
|
|
8330
|
+
defaultOpen?: boolean;
|
|
8331
|
+
|
|
8332
|
+
// Styling
|
|
8333
|
+
/** Additional className for the root element */
|
|
8334
|
+
className?: string;
|
|
8335
|
+
}
|
|
8336
|
+
`, prefix)
|
|
8337
|
+
},
|
|
8338
|
+
{
|
|
8339
|
+
name: "index.ts",
|
|
8340
|
+
content: prefixTailwindClasses(`export { WalletTopup } from "./wallet-topup";
|
|
8341
|
+
export type { WalletTopupProps, AmountOption } from "./types";
|
|
6612
8342
|
`, prefix)
|
|
6613
8343
|
}
|
|
6614
8344
|
]
|