saafe-redirection-flow 2.0.0
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/.github/workflows/build-and-deploy.yml +41 -0
- package/.gitlab-ci.yml +108 -0
- package/.releaserc.json +18 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.ts +16 -0
- package/.storybook/vitest.setup.ts +9 -0
- package/.vite/deps/@radix-ui_react-avatar.js +230 -0
- package/.vite/deps/@radix-ui_react-avatar.js.map +7 -0
- package/.vite/deps/@radix-ui_react-slot.js +12 -0
- package/.vite/deps/@radix-ui_react-slot.js.map +7 -0
- package/.vite/deps/_metadata.json +79 -0
- package/.vite/deps/chunk-5VGQBUCU.js +597 -0
- package/.vite/deps/chunk-5VGQBUCU.js.map +7 -0
- package/.vite/deps/chunk-DC5AMYBS.js +38 -0
- package/.vite/deps/chunk-DC5AMYBS.js.map +7 -0
- package/.vite/deps/chunk-HUIEPYH7.js +11265 -0
- package/.vite/deps/chunk-HUIEPYH7.js.map +7 -0
- package/.vite/deps/chunk-TKHB4QMX.js +281 -0
- package/.vite/deps/chunk-TKHB4QMX.js.map +7 -0
- package/.vite/deps/chunk-YLDSBLSF.js +1139 -0
- package/.vite/deps/chunk-YLDSBLSF.js.map +7 -0
- package/.vite/deps/class-variance-authority.js +63 -0
- package/.vite/deps/class-variance-authority.js.map +7 -0
- package/.vite/deps/lucide-react.js +36984 -0
- package/.vite/deps/lucide-react.js.map +7 -0
- package/.vite/deps/package.json +3 -0
- package/.vite/deps/react-dom_client.js +17917 -0
- package/.vite/deps/react-dom_client.js.map +7 -0
- package/.vite/deps/react-router-dom.js +452 -0
- package/.vite/deps/react-router-dom.js.map +7 -0
- package/.vite/deps/react-router.js +234 -0
- package/.vite/deps/react-router.js.map +7 -0
- package/.vite/deps/react.js +5 -0
- package/.vite/deps/react.js.map +7 -0
- package/.vite/deps/react_jsx-dev-runtime.js +470 -0
- package/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/CHANGELOG.md +420 -0
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/RELEASE_CHEATSHEET.md +93 -0
- package/RELEASE_NOTES.md +120 -0
- package/components.json +21 -0
- package/docs/DEPLOYMENT_WORKFLOW.md +262 -0
- package/docs/RELEASE_GUIDE.md +591 -0
- package/docs/architecture.md +432 -0
- package/docs/components.md +199 -0
- package/docs/index.md +69 -0
- package/docs/local-release-workflow.md +234 -0
- package/docs/routes.md +118 -0
- package/docs/sdk-integration.md +325 -0
- package/docs/semantic-release.md +124 -0
- package/docs/user-flow.md +206 -0
- package/eslint.config.js +28 -0
- package/index.html +19 -0
- package/install.sh +198 -0
- package/package.json +115 -0
- package/public/images/bank-logo.png +0 -0
- package/public/saafe-icon.svg +9 -0
- package/src/App.tsx +171 -0
- package/src/__tests__/url-parameters.test.ts +82 -0
- package/src/assets/brand/applestore.svg +13 -0
- package/src/assets/brand/playstore.svg +23 -0
- package/src/assets/brand/saafe-color-white-logo.svg +14 -0
- package/src/assets/brand/saafe-icon.svg +9 -0
- package/src/assets/brand/saafe-logo.svg +18 -0
- package/src/assets/icons/check-icon-dark.svg +27 -0
- package/src/assets/icons/check-icon.svg +23 -0
- package/src/components/ErrorBoundary.tsx +132 -0
- package/src/components/alert/alert.tsx +27 -0
- package/src/components/auth/AuthGuard.tsx +76 -0
- package/src/components/cards/BankCard.stories.tsx +69 -0
- package/src/components/cards/BankCard.tsx +227 -0
- package/src/components/cards/OuterCard.tsx +109 -0
- package/src/components/cards/WrapperCard.tsx +64 -0
- package/src/components/documents/PrivacyContent.tsx +1 -0
- package/src/components/dummyFooter.tsx +29 -0
- package/src/components/icons/github.tsx +12 -0
- package/src/components/language/LanguageSwitcher.tsx +44 -0
- package/src/components/layouts/FrostedLayout.stories.tsx +42 -0
- package/src/components/layouts/FrostedLayout.tsx +333 -0
- package/src/components/layouts/MobileLayout.tsx +403 -0
- package/src/components/mobile-background.tsx +136 -0
- package/src/components/mobileAppDownload.tsx +30 -0
- package/src/components/modal/ModalComp.tsx +27 -0
- package/src/components/mode-toggle.tsx +36 -0
- package/src/components/page-header.tsx +50 -0
- package/src/components/session/SessionTimeoutScreen.tsx +134 -0
- package/src/components/session/SessionTimer.tsx +173 -0
- package/src/components/step-navigation.tsx +87 -0
- package/src/components/title/AppBar.stories.tsx +50 -0
- package/src/components/title/AppBar.tsx +150 -0
- package/src/components/title/SectionTitle.tsx +31 -0
- package/src/components/ui/AnimatedButton.module.css +13 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/animatedButton.tsx +111 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/badge.tsx +36 -0
- package/src/components/ui/bottom-sheet.tsx +122 -0
- package/src/components/ui/button.tsx +59 -0
- package/src/components/ui/calendar.tsx +86 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.stories.tsx +49 -0
- package/src/components/ui/checkbox.tsx +67 -0
- package/src/components/ui/collapsible.tsx +45 -0
- package/src/components/ui/dialog.tsx +134 -0
- package/src/components/ui/document-link.tsx +26 -0
- package/src/components/ui/dot-stepper.tsx +57 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/form.tsx +165 -0
- package/src/components/ui/frosted-panel.stories.tsx +86 -0
- package/src/components/ui/frosted-panel.tsx +276 -0
- package/src/components/ui/input.tsx +39 -0
- package/src/components/ui/label.stories.tsx +67 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/mobile-footer.tsx +54 -0
- package/src/components/ui/modal.tsx +90 -0
- package/src/components/ui/otp-input.stories.tsx +62 -0
- package/src/components/ui/otp-input.tsx +221 -0
- package/src/components/ui/platform-specific-behavior.tsx +28 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +56 -0
- package/src/components/ui/sdk-params-docs.tsx +53 -0
- package/src/components/ui/select.tsx +159 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/sidebar.tsx +724 -0
- package/src/components/ui/skeleton.stories.tsx +50 -0
- package/src/components/ui/skeleton.tsx +15 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/step.stories.tsx +132 -0
- package/src/components/ui/step.tsx +234 -0
- package/src/components/ui/stepper-progress.tsx +136 -0
- package/src/components/ui/stepper.tsx +259 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/url-decode-loader.tsx +36 -0
- package/src/components/ui/version-display.tsx +104 -0
- package/src/components/ui/web-footer.tsx +36 -0
- package/src/config/environments.ts +99 -0
- package/src/config/urls.ts +53 -0
- package/src/const/fiTypeCategoryMap.ts +19 -0
- package/src/contexts/LanguageContext.tsx +41 -0
- package/src/contexts/RTLContext.tsx +42 -0
- package/src/contexts/ThemeContext.tsx +93 -0
- package/src/hooks/use-account-discovery.ts +205 -0
- package/src/hooks/use-auth-query.ts +141 -0
- package/src/hooks/use-fip-query.ts +72 -0
- package/src/hooks/use-media-query.ts +32 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-page-title.tsx +48 -0
- package/src/hooks/use-platform.ts +52 -0
- package/src/hooks/use-trusted-count.ts +21 -0
- package/src/hooks/use-url-decode.ts +90 -0
- package/src/hooks/useStep.ts +170 -0
- package/src/index.css +154 -0
- package/src/interfaces/app.interfaces.ts +39 -0
- package/src/interfaces/services.interfaces.ts +65 -0
- package/src/lib/i18n.ts +68 -0
- package/src/lib/utils.ts +6 -0
- package/src/locales/en/common.json +167 -0
- package/src/locales/hi/common.json +137 -0
- package/src/locales/kn/common.json +137 -0
- package/src/locales/ml/common.json +137 -0
- package/src/locales/ta/common.json +137 -0
- package/src/locales/te/common.json +137 -0
- package/src/locales/ur/common.json +138 -0
- package/src/main.tsx +46 -0
- package/src/pages/Login.tsx +363 -0
- package/src/pages/accounts/AccountsToProceed.tsx +396 -0
- package/src/pages/accounts/Discover.tsx +76 -0
- package/src/pages/accounts/DiscoverAccount.tsx +751 -0
- package/src/pages/accounts/LinkSelectedAccounts.tsx +638 -0
- package/src/pages/accounts/OldUser.tsx +329 -0
- package/src/pages/accounts/link-accounts.tsx +913 -0
- package/src/pages/consent/ReviewConsent.tsx +836 -0
- package/src/pages/consent/rejected.tsx +253 -0
- package/src/pages/consent/success.tsx +220 -0
- package/src/providers/query-provider.tsx +24 -0
- package/src/providers/toast-provider.tsx +26 -0
- package/src/services/api/account.service.ts +296 -0
- package/src/services/api/auth.service.ts +206 -0
- package/src/services/api/axios.ts +138 -0
- package/src/services/api/consent.service.ts +142 -0
- package/src/services/api/decode.service.ts +53 -0
- package/src/services/api/feedback.service.ts +34 -0
- package/src/services/api/fip.service.ts +187 -0
- package/src/services/api/index.ts +9 -0
- package/src/services/api/public.service.ts +18 -0
- package/src/services/api.ts +2 -0
- package/src/services/postMessage.service.ts +179 -0
- package/src/store/NavigationBlockContext.tsx +34 -0
- package/src/store/auth.store.ts +79 -0
- package/src/store/fip.store.ts +396 -0
- package/src/store/mandatoryConsent.store.ts +24 -0
- package/src/store/redirect.store.ts +73 -0
- package/src/store/step.store.ts +124 -0
- package/src/stories/Button.stories.ts +53 -0
- package/src/stories/Button.tsx +37 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/Header.stories.ts +33 -0
- package/src/stories/Header.tsx +56 -0
- package/src/stories/Page.stories.ts +32 -0
- package/src/stories/Page.tsx +73 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/styles/rtl-utils.css +90 -0
- package/src/styles/rtl.css +105 -0
- package/src/utils/api-error.ts +26 -0
- package/src/utils/cn.ts +10 -0
- package/src/utils/error-callback.ts +116 -0
- package/src/utils/formatAccountNumber.ts +9 -0
- package/src/utils/handleIdentifiers.ts +90 -0
- package/src/utils/posthog.ts +67 -0
- package/src/utils/toast-helpers.ts +61 -0
- package/src/vite-env.d.ts +1 -0
- package/stage-aa-2506251021.zip +0 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +45 -0
- package/vitest.shims.d.ts +1 -0
- package/vitest.workspace.ts +46 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { Skeleton } from "./skeleton";
|
3
|
+
|
4
|
+
const meta: Meta<typeof Skeleton> = {
|
5
|
+
title: "UI/Skeleton",
|
6
|
+
component: Skeleton,
|
7
|
+
parameters: {
|
8
|
+
layout: "padded",
|
9
|
+
},
|
10
|
+
tags: ["autodocs"],
|
11
|
+
};
|
12
|
+
|
13
|
+
export default meta;
|
14
|
+
type Story = StoryObj<typeof Skeleton>;
|
15
|
+
|
16
|
+
export const Default: Story = {
|
17
|
+
args: {
|
18
|
+
className: "h-4 w-[250px]",
|
19
|
+
},
|
20
|
+
};
|
21
|
+
|
22
|
+
export const Circle: Story = {
|
23
|
+
args: {
|
24
|
+
className: "h-12 w-12 rounded-full",
|
25
|
+
},
|
26
|
+
};
|
27
|
+
|
28
|
+
export const Card: Story = {
|
29
|
+
args: {
|
30
|
+
className: "h-[200px] w-[300px] rounded-lg",
|
31
|
+
},
|
32
|
+
};
|
33
|
+
|
34
|
+
export const Text: Story = {
|
35
|
+
args: {
|
36
|
+
className: "h-4 w-[200px]",
|
37
|
+
},
|
38
|
+
};
|
39
|
+
|
40
|
+
export const Avatar: Story = {
|
41
|
+
args: {
|
42
|
+
className: "h-10 w-10 rounded-full",
|
43
|
+
},
|
44
|
+
};
|
45
|
+
|
46
|
+
export const CustomColor: Story = {
|
47
|
+
args: {
|
48
|
+
className: "h-4 w-[250px] bg-gray-300",
|
49
|
+
},
|
50
|
+
};
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { cn } from "@/lib/utils"
|
2
|
+
|
3
|
+
function Skeleton({
|
4
|
+
className,
|
5
|
+
...props
|
6
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
7
|
+
return (
|
8
|
+
<div
|
9
|
+
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
10
|
+
{...props}
|
11
|
+
/>
|
12
|
+
)
|
13
|
+
}
|
14
|
+
|
15
|
+
export { Skeleton }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { useTheme } from "next-themes"
|
2
|
+
import { Toaster as Sonner, ToasterProps } from "sonner"
|
3
|
+
|
4
|
+
const Toaster = ({ ...props }: ToasterProps) => {
|
5
|
+
const { theme = "system" } = useTheme()
|
6
|
+
|
7
|
+
return (
|
8
|
+
<Sonner
|
9
|
+
theme={theme as ToasterProps["theme"]}
|
10
|
+
className="toaster group"
|
11
|
+
style={
|
12
|
+
{
|
13
|
+
"--normal-bg": "var(--popover)",
|
14
|
+
"--normal-text": "var(--popover-foreground)",
|
15
|
+
"--normal-border": "var(--border)",
|
16
|
+
} as React.CSSProperties
|
17
|
+
}
|
18
|
+
{...props}
|
19
|
+
/>
|
20
|
+
)
|
21
|
+
}
|
22
|
+
|
23
|
+
export { Toaster }
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
2
|
+
import { Step, StepGroup, StepConnector } from "./step";
|
3
|
+
import { Link, Shield, FileText } from "lucide-react";
|
4
|
+
import { useState, useEffect } from "react";
|
5
|
+
|
6
|
+
const meta: Meta<typeof Step> = {
|
7
|
+
title: "UI/Step",
|
8
|
+
component: Step,
|
9
|
+
parameters: {
|
10
|
+
layout: "centered",
|
11
|
+
},
|
12
|
+
tags: ["autodocs"],
|
13
|
+
};
|
14
|
+
|
15
|
+
export default meta;
|
16
|
+
type Story = StoryObj<typeof Step>;
|
17
|
+
|
18
|
+
// Example of a step with dynamic child steps
|
19
|
+
const StepWithDynamicChildren = () => {
|
20
|
+
const [childSteps, setChildSteps] = useState<Array<{
|
21
|
+
id: string;
|
22
|
+
title: string;
|
23
|
+
description: string;
|
24
|
+
isActive: boolean;
|
25
|
+
isCompleted: boolean;
|
26
|
+
}>>([]);
|
27
|
+
|
28
|
+
useEffect(() => {
|
29
|
+
// Simulate API fetch
|
30
|
+
const fetchedSteps = [
|
31
|
+
{
|
32
|
+
id: "bank-1",
|
33
|
+
title: "Bank Accounts",
|
34
|
+
description: "Link your bank accounts",
|
35
|
+
isActive: true,
|
36
|
+
isCompleted: false,
|
37
|
+
},
|
38
|
+
{
|
39
|
+
id: "insurance-1",
|
40
|
+
title: "Insurance",
|
41
|
+
description: "Link your insurance policies",
|
42
|
+
isActive: false,
|
43
|
+
isCompleted: false,
|
44
|
+
},
|
45
|
+
{
|
46
|
+
id: "investment-1",
|
47
|
+
title: "Investments",
|
48
|
+
description: "Link your investment accounts",
|
49
|
+
isActive: false,
|
50
|
+
isCompleted: false,
|
51
|
+
},
|
52
|
+
];
|
53
|
+
|
54
|
+
// Simulate delayed loading of child steps
|
55
|
+
setTimeout(() => {
|
56
|
+
setChildSteps(fetchedSteps);
|
57
|
+
}, 1000);
|
58
|
+
}, []);
|
59
|
+
|
60
|
+
return (
|
61
|
+
<div className="w-[600px] p-8 bg-gray-100 rounded-lg">
|
62
|
+
<StepGroup activeStep={2} completedSteps={[1]} baseColor="#004d4d" spacing={32}>
|
63
|
+
<div className="relative">
|
64
|
+
<Step
|
65
|
+
step={1}
|
66
|
+
title="First Step"
|
67
|
+
description="This is the first step"
|
68
|
+
icon={<Shield className="w-4 h-4" />}
|
69
|
+
isActive={false}
|
70
|
+
isCompleted={true}
|
71
|
+
/>
|
72
|
+
<StepConnector isActive={false} isCompleted={true} />
|
73
|
+
</div>
|
74
|
+
<div className="relative">
|
75
|
+
<Step
|
76
|
+
step={2}
|
77
|
+
title="Link Accounts"
|
78
|
+
description="Select & Link the accounts you'd like to share"
|
79
|
+
icon={<Link className="w-4 h-4" />}
|
80
|
+
isActive={true}
|
81
|
+
isCompleted={false}
|
82
|
+
childSteps={childSteps}
|
83
|
+
/>
|
84
|
+
<StepConnector isActive={true} isCompleted={false} />
|
85
|
+
</div>
|
86
|
+
<div className="relative">
|
87
|
+
<Step
|
88
|
+
step={3}
|
89
|
+
title="Review Consent"
|
90
|
+
description="Review and approve the consent"
|
91
|
+
icon={<FileText className="w-4 h-4" />}
|
92
|
+
isActive={false}
|
93
|
+
isCompleted={false}
|
94
|
+
isLast={true}
|
95
|
+
/>
|
96
|
+
</div>
|
97
|
+
</StepGroup>
|
98
|
+
</div>
|
99
|
+
);
|
100
|
+
};
|
101
|
+
|
102
|
+
export const Default: Story = {
|
103
|
+
args: {
|
104
|
+
step: 1,
|
105
|
+
title: "Default Step",
|
106
|
+
description: "This is a default step with no special state",
|
107
|
+
},
|
108
|
+
};
|
109
|
+
|
110
|
+
export const Active: Story = {
|
111
|
+
args: {
|
112
|
+
step: 2,
|
113
|
+
title: "Active Step",
|
114
|
+
description: "This step is currently active",
|
115
|
+
isActive: true,
|
116
|
+
icon: <Shield className="w-4 h-4" />,
|
117
|
+
},
|
118
|
+
};
|
119
|
+
|
120
|
+
export const Completed: Story = {
|
121
|
+
args: {
|
122
|
+
step: 3,
|
123
|
+
title: "Completed Step",
|
124
|
+
description: "This step has been completed",
|
125
|
+
isCompleted: true,
|
126
|
+
icon: <Link className="w-4 h-4" />,
|
127
|
+
},
|
128
|
+
};
|
129
|
+
|
130
|
+
export const WithChildSteps: Story = {
|
131
|
+
render: () => <StepWithDynamicChildren />,
|
132
|
+
};
|
@@ -0,0 +1,234 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import React from "react";
|
4
|
+
import { cn } from "@/lib/utils";
|
5
|
+
import { Check, Circle } from "lucide-react";
|
6
|
+
import { motion } from "framer-motion";
|
7
|
+
import { useRTL } from '@/contexts/RTLContext';
|
8
|
+
|
9
|
+
export interface ChildStepProps {
|
10
|
+
id: string;
|
11
|
+
title: string;
|
12
|
+
description?: string;
|
13
|
+
isActive?: boolean;
|
14
|
+
isCompleted?: boolean;
|
15
|
+
}
|
16
|
+
|
17
|
+
export interface StepProps {
|
18
|
+
step: number;
|
19
|
+
title: string;
|
20
|
+
description?: string;
|
21
|
+
icon?: React.ReactNode;
|
22
|
+
isActive?: boolean;
|
23
|
+
isCompleted?: boolean;
|
24
|
+
isLast?: boolean;
|
25
|
+
baseColor?: string;
|
26
|
+
onStepClick?: () => void;
|
27
|
+
height?: number;
|
28
|
+
isChildStep?: boolean;
|
29
|
+
isParentPending?: boolean;
|
30
|
+
notAuthenticated?: boolean;
|
31
|
+
isRTL?: boolean;
|
32
|
+
}
|
33
|
+
|
34
|
+
export interface StepContextType {
|
35
|
+
activeStep: number;
|
36
|
+
completedSteps: number[];
|
37
|
+
baseColor: string;
|
38
|
+
onStepComplete: (step: number) => void;
|
39
|
+
spacing?: number;
|
40
|
+
}
|
41
|
+
|
42
|
+
export const StepContext = React.createContext<StepContextType>({
|
43
|
+
activeStep: 1,
|
44
|
+
completedSteps: [],
|
45
|
+
baseColor: "#004d4d",
|
46
|
+
onStepComplete: () => { },
|
47
|
+
spacing: 24,
|
48
|
+
});
|
49
|
+
|
50
|
+
export const Step = React.forwardRef<HTMLDivElement, StepProps>(
|
51
|
+
(
|
52
|
+
{
|
53
|
+
title,
|
54
|
+
description,
|
55
|
+
icon,
|
56
|
+
isActive = false,
|
57
|
+
isCompleted = false,
|
58
|
+
isParentPending = false,
|
59
|
+
isLast = false,
|
60
|
+
baseColor = "#004d4d",
|
61
|
+
onStepClick,
|
62
|
+
height = 96,
|
63
|
+
isChildStep = false,
|
64
|
+
notAuthenticated = false,
|
65
|
+
isRTL = false,
|
66
|
+
},
|
67
|
+
ref
|
68
|
+
) => {
|
69
|
+
|
70
|
+
const getStepIndicator = () => {
|
71
|
+
const sizeClasses = isChildStep ? "w-5 h-5" : "w-8 h-8";
|
72
|
+
|
73
|
+
if (isCompleted) {
|
74
|
+
return (
|
75
|
+
<motion.div
|
76
|
+
initial={{ scale: 0 }}
|
77
|
+
animate={{ scale: 1 }}
|
78
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
79
|
+
className={cn(sizeClasses, `rounded-full bg-white flex items-center justify-center`, isChildStep ? "bg-transparent border-none" : '')}
|
80
|
+
>
|
81
|
+
{isChildStep ? <Circle className={cn("w-3 h-3", `fill-white mt-1`, isRTL && "mr-[12px]")} /> : <Check className={cn("w-4 h-4", `text-[${baseColor}]`)} />}
|
82
|
+
</motion.div >
|
83
|
+
);
|
84
|
+
}
|
85
|
+
|
86
|
+
if (isActive) {
|
87
|
+
return (
|
88
|
+
<motion.div
|
89
|
+
initial={{ scale: 0.8 }}
|
90
|
+
animate={{ scale: 1 }}
|
91
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
92
|
+
className={cn(sizeClasses, "rounded-full bg-white flex items-center justify-center", isChildStep ? "bg-transparent border-none" : '')}
|
93
|
+
>
|
94
|
+
{icon && !isChildStep ? (
|
95
|
+
<div className={cn("w-4 h-4", `text-[${baseColor}]`)}>{icon}</div>
|
96
|
+
) : (
|
97
|
+
<Circle color="white" className={cn(isChildStep ? "w-3 h-3 border-none" : "w-4 h-4", `fill-white mt-1`)} />
|
98
|
+
)}
|
99
|
+
</motion.div>
|
100
|
+
);
|
101
|
+
}
|
102
|
+
|
103
|
+
return (
|
104
|
+
<div className={cn(sizeClasses, "rounded-full border-1 border-white/50 flex items-center justify-center", isChildStep && "border-none", isParentPending && "bg-white")}>
|
105
|
+
{icon && !isChildStep ? (
|
106
|
+
<div className={cn(isChildStep ? "w-3 h-3" : "w-4 h-4", "text-white/50", isParentPending && `text-[${baseColor}]`)}>{icon}</div>
|
107
|
+
) : (
|
108
|
+
<Circle className={cn(isChildStep ? "w-3 h-3" : "w-4 h-4", "text-white/50")} />
|
109
|
+
)}
|
110
|
+
</div>
|
111
|
+
);
|
112
|
+
};
|
113
|
+
|
114
|
+
return (
|
115
|
+
<div
|
116
|
+
ref={ref}
|
117
|
+
className={cn(
|
118
|
+
"relative flex flex-col gap-2 cursor-default transition-all duration-200",
|
119
|
+
isLast && "mb-0",
|
120
|
+
isChildStep && "ml-1.5"
|
121
|
+
)}
|
122
|
+
style={{ minHeight: `${height}px` }}
|
123
|
+
onClick={onStepClick}
|
124
|
+
>
|
125
|
+
<div className="flex items-start gap-4">
|
126
|
+
<div className="flex-shrink-0">{notAuthenticated ? <motion.div
|
127
|
+
initial={{ scale: 0.8 }}
|
128
|
+
animate={{ scale: 1 }}
|
129
|
+
transition={{ duration: 0.3, ease: "easeOut" }}
|
130
|
+
className={cn('w-8 h-8', "rounded-full bg-white flex items-center justify-center", isChildStep && "bg-transparent border-none")}
|
131
|
+
>
|
132
|
+
{icon && !isChildStep ? (
|
133
|
+
<div className={cn("w-4 h-4", `text-[${baseColor}]`)}>{icon}</div>
|
134
|
+
) : (
|
135
|
+
<Circle className={cn(isChildStep ? "w-3 h-3 border-none" : "w-4 h-4", `fill-white mt-1`)} />
|
136
|
+
)}
|
137
|
+
</motion.div> : getStepIndicator()}</div>
|
138
|
+
<div className="flex-1">
|
139
|
+
<h3
|
140
|
+
className={cn(
|
141
|
+
"text-white/80 transition-all duration-200",
|
142
|
+
isActive && !isParentPending ? "text-lg" : "text-sm",
|
143
|
+
"font-light",
|
144
|
+
(isActive || isCompleted || isParentPending) && "font-medium text-white",
|
145
|
+
isActive ? "mt-0" : '',
|
146
|
+
isChildStep ? 'mb-2' : ' mt-1'
|
147
|
+
)}
|
148
|
+
>
|
149
|
+
{title}
|
150
|
+
</h3>
|
151
|
+
{description && (
|
152
|
+
<p className={cn(
|
153
|
+
"text-white/60 mt-1",
|
154
|
+
isChildStep ? "text-xs" : "text-sm"
|
155
|
+
)}>
|
156
|
+
{description}
|
157
|
+
</p>
|
158
|
+
)}
|
159
|
+
</div>
|
160
|
+
</div>
|
161
|
+
</div>
|
162
|
+
);
|
163
|
+
}
|
164
|
+
);
|
165
|
+
|
166
|
+
Step.displayName = "Step";
|
167
|
+
|
168
|
+
export interface StepConnectorProps {
|
169
|
+
isActive: boolean;
|
170
|
+
isCompleted: boolean;
|
171
|
+
baseColor?: string;
|
172
|
+
}
|
173
|
+
|
174
|
+
export const StepConnector = ({ isActive, isCompleted, baseColor = "#FFF" }: StepConnectorProps) => {
|
175
|
+
const { isRTL } = useRTL();
|
176
|
+
|
177
|
+
return (
|
178
|
+
<div className={`absolute ${isRTL ? 'right-4' : 'left-4'} top-6 bottom-0 w-[1px]`}>
|
179
|
+
<motion.div
|
180
|
+
initial={{ height: 0, opacity: 0 }}
|
181
|
+
animate={{
|
182
|
+
height: "100%",
|
183
|
+
opacity: 1,
|
184
|
+
background: isCompleted
|
185
|
+
? baseColor
|
186
|
+
: isActive
|
187
|
+
? "linear-gradient(to bottom, #fff 50%, transparent 50%)"
|
188
|
+
: "transparent",
|
189
|
+
backgroundSize: "4px 4px",
|
190
|
+
}}
|
191
|
+
transition={{
|
192
|
+
duration: 0.8,
|
193
|
+
ease: "easeInOut",
|
194
|
+
opacity: { duration: 0.3 }
|
195
|
+
}}
|
196
|
+
className="w-full"
|
197
|
+
/>
|
198
|
+
</div>
|
199
|
+
);
|
200
|
+
};
|
201
|
+
|
202
|
+
export interface StepGroupProps {
|
203
|
+
children: React.ReactNode;
|
204
|
+
activeStep: number;
|
205
|
+
completedSteps: number[];
|
206
|
+
baseColor?: string;
|
207
|
+
onStepComplete?: (step: number) => void;
|
208
|
+
className?: string;
|
209
|
+
spacing?: number;
|
210
|
+
}
|
211
|
+
|
212
|
+
export const StepGroup = ({
|
213
|
+
children,
|
214
|
+
activeStep,
|
215
|
+
completedSteps,
|
216
|
+
baseColor = "#FFF",
|
217
|
+
onStepComplete = () => { },
|
218
|
+
className,
|
219
|
+
spacing = 24,
|
220
|
+
}: StepGroupProps) => {
|
221
|
+
const contextValue = {
|
222
|
+
activeStep,
|
223
|
+
completedSteps,
|
224
|
+
baseColor,
|
225
|
+
onStepComplete,
|
226
|
+
spacing,
|
227
|
+
};
|
228
|
+
|
229
|
+
return (
|
230
|
+
<StepContext.Provider value={contextValue}>
|
231
|
+
<div className={cn("relative", className)}>{children}</div>
|
232
|
+
</StepContext.Provider>
|
233
|
+
);
|
234
|
+
};
|
@@ -0,0 +1,136 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import * as React from "react";
|
4
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
5
|
+
import { cn } from "@/lib/utils";
|
6
|
+
import { Button } from "@/components/ui/button";
|
7
|
+
import {
|
8
|
+
Stepper,
|
9
|
+
StepperIndicator,
|
10
|
+
StepperItem,
|
11
|
+
StepperTrigger,
|
12
|
+
} from "@/components/ui/stepper";
|
13
|
+
import { motion } from "framer-motion";
|
14
|
+
import { useMediaQuery } from "@/hooks/use-media-query";
|
15
|
+
|
16
|
+
interface StepperProgressProps extends React.HTMLAttributes<HTMLDivElement> {
|
17
|
+
steps: number;
|
18
|
+
currentStep: number;
|
19
|
+
onStepChange?: (step: number) => void;
|
20
|
+
showControls?: boolean;
|
21
|
+
className?: string;
|
22
|
+
childProgress?: Record<number, number>; // Tracks progress for steps with children
|
23
|
+
maxChildProgress?: Record<number, number>; // Max progress for each parent
|
24
|
+
}
|
25
|
+
|
26
|
+
export function StepperProgress({
|
27
|
+
steps,
|
28
|
+
currentStep,
|
29
|
+
onStepChange,
|
30
|
+
showControls = false,
|
31
|
+
className,
|
32
|
+
childProgress = {},
|
33
|
+
maxChildProgress = {},
|
34
|
+
...props
|
35
|
+
}: StepperProgressProps) {
|
36
|
+
const stepsArray = Array.from({ length: steps }, (_, i) => i + 1);
|
37
|
+
const isMobile = useMediaQuery("(max-width: 768px)");
|
38
|
+
|
39
|
+
// Initialize correctly on mobile screens for the login page
|
40
|
+
React.useEffect(() => {
|
41
|
+
if (isMobile && window.location.pathname === '/login') {
|
42
|
+
// Reset child progress so progress bar doesn't show as completed
|
43
|
+
if (Object.keys(childProgress).length === 0 && currentStep === 1) {
|
44
|
+
// Skip this setup if the childProgress is already set
|
45
|
+
onStepChange?.(1);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}, [isMobile, currentStep, onStepChange, childProgress]);
|
49
|
+
|
50
|
+
const handlePrev = () => {
|
51
|
+
if (currentStep > 1 && onStepChange) {
|
52
|
+
onStepChange(currentStep - 1);
|
53
|
+
}
|
54
|
+
};
|
55
|
+
|
56
|
+
const handleNext = () => {
|
57
|
+
if (currentStep < steps && onStepChange) {
|
58
|
+
onStepChange(currentStep + 1);
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
62
|
+
return (
|
63
|
+
<div
|
64
|
+
className={cn("flex items-center gap-2 w-full", className)}
|
65
|
+
{...props}
|
66
|
+
>
|
67
|
+
{showControls && (
|
68
|
+
<Button
|
69
|
+
className="shrink-0"
|
70
|
+
variant="ghost"
|
71
|
+
size="icon"
|
72
|
+
onClick={handlePrev}
|
73
|
+
disabled={currentStep === 1}
|
74
|
+
aria-label="Previous step"
|
75
|
+
>
|
76
|
+
<ChevronLeftIcon size={16} strokeWidth={2} aria-hidden="true" />
|
77
|
+
</Button>
|
78
|
+
)}
|
79
|
+
|
80
|
+
<Stepper value={currentStep} onValueChange={onStepChange} className="gap-1 w-full">
|
81
|
+
{stepsArray.map((step) => {
|
82
|
+
const isCompleted = step < currentStep;
|
83
|
+
const hasPartialProgress = childProgress[step] !== undefined;
|
84
|
+
const progressPercentage = hasPartialProgress ?
|
85
|
+
(childProgress[step] / (maxChildProgress[step] || 1)) * 100 : 0;
|
86
|
+
|
87
|
+
return (
|
88
|
+
<StepperItem
|
89
|
+
key={step}
|
90
|
+
step={step}
|
91
|
+
className="flex-1 relative"
|
92
|
+
completed={isCompleted}
|
93
|
+
>
|
94
|
+
<StepperTrigger className="w-full flex-col items-start gap-2" asChild>
|
95
|
+
<div className="relative w-full h-1 bg-gray-200 rounded-full overflow-hidden">
|
96
|
+
{/* Partial progress indicator */}
|
97
|
+
{hasPartialProgress && !isCompleted && (
|
98
|
+
<motion.div
|
99
|
+
className="absolute left-0 top-0 h-full bg-primary"
|
100
|
+
initial={{ width: 0 }}
|
101
|
+
animate={{ width: `${progressPercentage}%` }}
|
102
|
+
transition={{ duration: 0.5, ease: "easeInOut" }}
|
103
|
+
/>
|
104
|
+
)}
|
105
|
+
|
106
|
+
{/* Full progress indicator - visible when step is completed */}
|
107
|
+
{isCompleted && (
|
108
|
+
<div className="absolute left-0 top-0 h-full w-full bg-primary" />
|
109
|
+
)}
|
110
|
+
|
111
|
+
{/* Hidden indicator for screen readers */}
|
112
|
+
<StepperIndicator asChild className="opacity-0 pointer-events-none">
|
113
|
+
<span className="sr-only">{step}</span>
|
114
|
+
</StepperIndicator>
|
115
|
+
</div>
|
116
|
+
</StepperTrigger>
|
117
|
+
</StepperItem>
|
118
|
+
);
|
119
|
+
})}
|
120
|
+
</Stepper>
|
121
|
+
|
122
|
+
{showControls && (
|
123
|
+
<Button
|
124
|
+
className="shrink-0"
|
125
|
+
variant="ghost"
|
126
|
+
size="icon"
|
127
|
+
onClick={handleNext}
|
128
|
+
disabled={currentStep === steps}
|
129
|
+
aria-label="Next step"
|
130
|
+
>
|
131
|
+
<ChevronRightIcon size={16} strokeWidth={2} aria-hidden="true" />
|
132
|
+
</Button>
|
133
|
+
)}
|
134
|
+
</div>
|
135
|
+
);
|
136
|
+
}
|