diff --git a/.storybook/main.ts b/.storybook/main.ts
index cb64a2d..e7579aa 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -5,10 +5,9 @@ const config: StorybookConfig = {
addons: [
"@chromatic-com/storybook",
"@storybook/addon-docs",
- "@storybook/addon-onboarding",
"@storybook/addon-a11y",
"@storybook/addon-vitest",
- "@storybook/addon-styling-webpack"
+ "@storybook/addon-styling-webpack",
],
framework: {
name: "@storybook/nextjs-vite",
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 876860f..a009243 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,5 +1,5 @@
import type { Preview } from "@storybook/nextjs-vite";
-import { Geist, Geist_Mono, Inter, Manrope } from "next/font/google";
+import { Geist, Geist_Mono, Inter, Manrope, Poppins } from "next/font/google";
import "../src/app/globals.css";
import React from "react";
@@ -25,6 +25,12 @@ const inter = Inter({
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
});
+const poppins = Poppins({
+ variable: "--font-poppins",
+ subsets: ["latin"],
+ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
+});
+
const preview: Preview = {
parameters: {
controls: {
@@ -53,7 +59,7 @@ const preview: Preview = {
decorators: [
(Story) => (
diff --git a/package-lock.json b/package-lock.json
index 6405da2..ac2eb8c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
@@ -35,7 +37,6 @@
"@eslint/eslintrc": "^3",
"@storybook/addon-a11y": "^9.1.6",
"@storybook/addon-docs": "^9.1.6",
- "@storybook/addon-onboarding": "^9.1.6",
"@storybook/addon-styling-webpack": "^2.0.0",
"@storybook/addon-vitest": "^9.1.6",
"@storybook/nextjs-vite": "^9.1.6",
@@ -1979,6 +1980,37 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz",
+ "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@@ -2002,6 +2034,33 @@
}
}
},
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
+ "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz",
@@ -2032,6 +2091,36 @@
}
}
},
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz",
+ "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -2528,6 +2617,24 @@
}
}
},
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
@@ -3021,20 +3128,6 @@
"storybook": "^9.1.6"
}
},
- "node_modules/@storybook/addon-onboarding": {
- "version": "9.1.6",
- "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.6.tgz",
- "integrity": "sha512-NkV9+08S9sOivtiLBctZo8Xebkw7cbBe0dDE7HsWYRmDiL+ZOOwRn+AUY5055pIBsCYG2GMS5fFfxSPrTJRJgw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/storybook"
- },
- "peerDependencies": {
- "storybook": "^9.1.6"
- }
- },
"node_modules/@storybook/addon-styling-webpack": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@storybook/addon-styling-webpack/-/addon-styling-webpack-2.0.0.tgz",
@@ -11484,6 +11577,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
diff --git a/package.json b/package.json
index e4f2c2b..c5a6089 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
@@ -47,7 +49,6 @@
"@eslint/eslintrc": "^3",
"@storybook/addon-a11y": "^9.1.6",
"@storybook/addon-docs": "^9.1.6",
- "@storybook/addon-onboarding": "^9.1.6",
"@storybook/addon-styling-webpack": "^2.0.0",
"@storybook/addon-vitest": "^9.1.6",
"@storybook/nextjs-vite": "^9.1.6",
diff --git a/public/avatars/male-1.jpg b/public/avatars/male-1.jpg
new file mode 100644
index 0000000..71c2c06
Binary files /dev/null and b/public/avatars/male-1.jpg differ
diff --git a/public/avatars/male-2.jpg b/public/avatars/male-2.jpg
new file mode 100644
index 0000000..71151dd
Binary files /dev/null and b/public/avatars/male-2.jpg differ
diff --git a/public/avatars/male-3.jpg b/public/avatars/male-3.jpg
new file mode 100644
index 0000000..2d409d0
Binary files /dev/null and b/public/avatars/male-3.jpg differ
diff --git a/public/check-mark.svg b/public/check-mark.svg
new file mode 100644
index 0000000..6b95774
--- /dev/null
+++ b/public/check-mark.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/soulmate-portrait-delivered-male.jpg b/public/soulmate-portrait-delivered-male.jpg
new file mode 100644
index 0000000..b39ef03
Binary files /dev/null and b/public/soulmate-portrait-delivered-male.jpg differ
diff --git a/public/trial-payment/avatars/1.jpg b/public/trial-payment/avatars/1.jpg
new file mode 100644
index 0000000..88e03fc
Binary files /dev/null and b/public/trial-payment/avatars/1.jpg differ
diff --git a/public/trial-payment/avatars/2.jpg b/public/trial-payment/avatars/2.jpg
new file mode 100644
index 0000000..5694f0d
Binary files /dev/null and b/public/trial-payment/avatars/2.jpg differ
diff --git a/public/trial-payment/avatars/3.jpg b/public/trial-payment/avatars/3.jpg
new file mode 100644
index 0000000..adee2fb
Binary files /dev/null and b/public/trial-payment/avatars/3.jpg differ
diff --git a/public/trial-payment/avatars/4.jpg b/public/trial-payment/avatars/4.jpg
new file mode 100644
index 0000000..a4901ea
Binary files /dev/null and b/public/trial-payment/avatars/4.jpg differ
diff --git a/public/trial-payment/avatars/5.jpg b/public/trial-payment/avatars/5.jpg
new file mode 100644
index 0000000..1bf6519
Binary files /dev/null and b/public/trial-payment/avatars/5.jpg differ
diff --git a/public/trial-payment/payment-methods/apple.svg b/public/trial-payment/payment-methods/apple.svg
new file mode 100644
index 0000000..2d551e2
--- /dev/null
+++ b/public/trial-payment/payment-methods/apple.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/trial-payment/payment-methods/discover.svg b/public/trial-payment/payment-methods/discover.svg
new file mode 100644
index 0000000..fe5b3ba
--- /dev/null
+++ b/public/trial-payment/payment-methods/discover.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/trial-payment/payment-methods/google.svg b/public/trial-payment/payment-methods/google.svg
new file mode 100644
index 0000000..c11b22c
--- /dev/null
+++ b/public/trial-payment/payment-methods/google.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/public/trial-payment/payment-methods/mastercard.svg b/public/trial-payment/payment-methods/mastercard.svg
new file mode 100644
index 0000000..cfcf14c
--- /dev/null
+++ b/public/trial-payment/payment-methods/mastercard.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/public/trial-payment/payment-methods/paypal.svg b/public/trial-payment/payment-methods/paypal.svg
new file mode 100644
index 0000000..33c5847
--- /dev/null
+++ b/public/trial-payment/payment-methods/paypal.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/trial-payment/payment-methods/visa.svg b/public/trial-payment/payment-methods/visa.svg
new file mode 100644
index 0000000..2c6eff7
--- /dev/null
+++ b/public/trial-payment/payment-methods/visa.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/trial-payment/portrait-female.jpg b/public/trial-payment/portrait-female.jpg
new file mode 100644
index 0000000..0c456c7
Binary files /dev/null and b/public/trial-payment/portrait-female.jpg differ
diff --git a/public/trial-payment/reviews/avatars/1.jpg b/public/trial-payment/reviews/avatars/1.jpg
new file mode 100644
index 0000000..fa20477
Binary files /dev/null and b/public/trial-payment/reviews/avatars/1.jpg differ
diff --git a/public/trial-payment/reviews/avatars/2.jpg b/public/trial-payment/reviews/avatars/2.jpg
new file mode 100644
index 0000000..85bb0d8
Binary files /dev/null and b/public/trial-payment/reviews/avatars/2.jpg differ
diff --git a/public/trial-payment/reviews/avatars/3.jpg b/public/trial-payment/reviews/avatars/3.jpg
new file mode 100644
index 0000000..048de41
Binary files /dev/null and b/public/trial-payment/reviews/avatars/3.jpg differ
diff --git a/public/trial-payment/reviews/photos/1.jpg b/public/trial-payment/reviews/photos/1.jpg
new file mode 100644
index 0000000..a7d49db
Binary files /dev/null and b/public/trial-payment/reviews/photos/1.jpg differ
diff --git a/public/trial-payment/reviews/photos/2.jpg b/public/trial-payment/reviews/photos/2.jpg
new file mode 100644
index 0000000..6e2b2e3
Binary files /dev/null and b/public/trial-payment/reviews/photos/2.jpg differ
diff --git a/public/trial-payment/reviews/photos/3.jpg b/public/trial-payment/reviews/photos/3.jpg
new file mode 100644
index 0000000..6d067aa
Binary files /dev/null and b/public/trial-payment/reviews/photos/3.jpg differ
diff --git a/public/trial-payment/reviews/portraits/1.jpg b/public/trial-payment/reviews/portraits/1.jpg
new file mode 100644
index 0000000..13e7b16
Binary files /dev/null and b/public/trial-payment/reviews/portraits/1.jpg differ
diff --git a/public/trial-payment/reviews/portraits/2.jpg b/public/trial-payment/reviews/portraits/2.jpg
new file mode 100644
index 0000000..56a8967
Binary files /dev/null and b/public/trial-payment/reviews/portraits/2.jpg differ
diff --git a/public/trial-payment/reviews/portraits/3.jpg b/public/trial-payment/reviews/portraits/3.jpg
new file mode 100644
index 0000000..96ccffa
Binary files /dev/null and b/public/trial-payment/reviews/portraits/3.jpg differ
diff --git a/public/trial-payment/users-portraits/1.jpg b/public/trial-payment/users-portraits/1.jpg
new file mode 100644
index 0000000..d4e108b
Binary files /dev/null and b/public/trial-payment/users-portraits/1.jpg differ
diff --git a/public/trial-payment/users-portraits/2.jpg b/public/trial-payment/users-portraits/2.jpg
new file mode 100644
index 0000000..8c89020
Binary files /dev/null and b/public/trial-payment/users-portraits/2.jpg differ
diff --git a/public/trial-payment/users-portraits/3.jpg b/public/trial-payment/users-portraits/3.jpg
new file mode 100644
index 0000000..9ec805e
Binary files /dev/null and b/public/trial-payment/users-portraits/3.jpg differ
diff --git a/public/trial-payment/wall-portrait-female.png b/public/trial-payment/wall-portrait-female.png
new file mode 100644
index 0000000..a8d0fc4
Binary files /dev/null and b/public/trial-payment/wall-portrait-female.png differ
diff --git a/src/app/globals.css b/src/app/globals.css
index a2c2082..869d17c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -12,6 +12,7 @@
--font-inter: var(--font-inter);
--font-geist-sans: var(--font-geist-sans);
--font-geist-mono: var(--font-geist-mono);
+ --font-poppins: var(--font-poppins);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -30,6 +31,7 @@
--color-placeholder-foreground: var(--placeholder-foreground);
--color-input: var(--input);
--color-border: var(--border);
+ --color-border-white: var(--border-white);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
@@ -56,6 +58,33 @@
--shadow-black-glow: 0px 8px 15px 0px #00000026, 0px 4px 6px 0px #00000014;
--shadow-coupon: 0px 20px 40px 0px #0000004d, 0px 8px 16px 0px #00000033;
--shadow-destructive: 0 0 0 2px rgba(239, 68, 68, 0.2);
+ --shadow-soulmate-portrait: 0px 6px 50px 0px #00000040;
+
+ /* TRIAL PAYMENT */
+
+ --color-trial-payment-background: var(--trial-payment-background);
+ --color-trial-payment-foreground: var(--trial-payment-foreground);
+
+ --color-trial-payment-secondary: var(--trial-payment-secondary);
+ --color-trial-payment-secondary-foreground: var(
+ --trial-payment-secondary-foreground
+ );
+
+ --color-trial-payment-primary: var(--trial-payment-primary);
+
+ --color-trial-payment-border: var(--trial-payment-border);
+ --color-trial-payment-border-secondary: var(--trial-payment-border-secondary);
+
+ /* TRIAL PAYMENT Shadows */
+ --shadow-trial-payment-header: 0px 1px 2px 0px #0000000d;
+ --shadow-trial-payment-card: 0px 1px 2px 0px #0000000d;
+ --shadow-trial-payment-step-active: 0px 10px 15px 0px #0000001a,
+ 0px 4px 6px 0px #0000001a;
+ --shadow-trial-payment-step-inactive: 0px 1px 11px 0px #3b82f6;
+ --shadow-trial-payment-review-photo: 0px 2px 4px 0px #00000040;
+
+ /* Animations */
+ --animate-scale-pulse: var(--animate-scale-pulse);
}
:root {
@@ -98,6 +127,7 @@
/* Border и Input */
--border: oklch(0.9288 0.0126 255.51); /* Светло-серая граница */
--border-black: oklch(0 0 0); /* Черная граница */
+ --border-white: oklch(1 0 0); /* Белая граница */
--input: oklch(0.922 0 0); /* Светло-серый фон инпутов */
--ring: oklch(0.6231 0.188 259.81); /* Синий фокус */
--placeholder-foreground: oklch(
@@ -126,6 +156,22 @@
--primary-light: oklch(0.954 0.025 259.8); /* #EBF5FF - для градиента */
--primary-lighter: oklch(0.909 0.045 259.8); /* #DBEAFE - для градиента */
--primary-dark: oklch(0.5461 0.2152 262.88); /* #2563EB - для градиента */
+
+ /* TRIAL PAYMENT COLORS */
+ --trial-payment-background: oklch(1 0 0); /* #ffffff */
+ --trial-payment-foreground: oklch(0.2101 0.0318 264.66); /* #111827 */
+
+ --trial-payment-secondary: oklch(0.9846 0.0017 247.84); /* #f9fafb */
+ --trial-payment-secondary-foreground: oklch(
+ 0.3729 0.0306 259.73
+ ); /* #374151 */
+
+ --trial-payment-primary: oklch(0.5219 0.2176 268.98); /* #3A55E4 */
+
+ --trial-payment-border: oklch(0.967 0.0029 264.54); /* #F3F4F6 */
+ --trial-payment-border-secondary: oklch(0.9276 0.0058 264.53); /* #E5E7EB */
+
+ --animate-scale-pulse: scale-pulse 2s infinite;
}
.dark {
@@ -144,6 +190,7 @@
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
+ --border-white: oklch(0 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
@@ -170,3 +217,16 @@
@apply bg-background text-foreground;
}
}
+
+@utility no-scrollbar {
+ @apply [scrollbar-width:none] [&::-webkit-scrollbar]:hidden;
+}
+
+@keyframes scale-pulse {
+ 0% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.05);
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 92bc6c6..6650de8 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import { Geist, Geist_Mono, Inter, Manrope } from "next/font/google";
+import { Geist, Geist_Mono, Inter, Manrope, Poppins } from "next/font/google";
import "./globals.css";
import { AppProviders } from "@/components/providers/AppProviders";
@@ -25,6 +25,12 @@ const inter = Inter({
weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
});
+const poppins = Poppins({
+ variable: "--font-poppins",
+ subsets: ["latin"],
+ weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
+});
+
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
@@ -38,7 +44,7 @@ export default function RootLayout({
return (
{children}
diff --git a/src/components/domains/TrialPayment/Card/Card.tsx b/src/components/domains/TrialPayment/Card/Card.tsx
new file mode 100644
index 0000000..8905c53
--- /dev/null
+++ b/src/components/domains/TrialPayment/Card/Card.tsx
@@ -0,0 +1,21 @@
+import { cn } from "@/lib/utils";
+
+type CardProps = React.ComponentProps<"section">;
+
+export default function Card({ ...props }: CardProps) {
+ return (
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/CommonQuestions/CommonQuestions.tsx b/src/components/domains/TrialPayment/Cards/CommonQuestions/CommonQuestions.tsx
new file mode 100644
index 0000000..9f20e0e
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/CommonQuestions/CommonQuestions.tsx
@@ -0,0 +1,55 @@
+import { Accordion } from "@/components/ui/accordion";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { CommonQuestion } from "../..";
+import { cn } from "@/lib/utils";
+import { useId } from "react";
+
+interface CommonQuestionsProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ questions?: React.ComponentProps[];
+ accordionProps?: React.ComponentProps;
+}
+
+export default function CommonQuestions({
+ title,
+ questions,
+ accordionProps,
+ ...props
+}: CommonQuestionsProps) {
+ const commonQuestionId = useId();
+ return (
+
+ {title && (
+
+ )}
+ {!!questions?.length && (
+
+ {questions?.map((question, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/FindingOneGuide/FindingOneGuide.tsx b/src/components/domains/TrialPayment/Cards/FindingOneGuide/FindingOneGuide.tsx
new file mode 100644
index 0000000..42a5d0f
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/FindingOneGuide/FindingOneGuide.tsx
@@ -0,0 +1,83 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { Card } from "../..";
+import { cn } from "@/lib/utils";
+
+interface FindingOneGuideProps extends React.ComponentProps {
+ header?: {
+ emoji?: TypographyProps<"span">;
+ title?: TypographyProps<"h3">;
+ };
+ text?: TypographyProps<"p">;
+ blur?: React.ComponentProps<"div"> & {
+ text?: TypographyProps<"p">;
+ icon?: React.ReactNode;
+ };
+}
+
+export default function FindingOneGuide({
+ header,
+ text,
+ blur,
+ ...props
+}: FindingOneGuideProps) {
+ return (
+
+ {header && (
+
+ {header.emoji && (
+
+ )}
+ {header.title && (
+
+ )}
+
+ )}
+
+ {text && (
+
+ )}
+ {blur && (
+
+ {blur?.icon}
+ {blur.text && (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/Footer/Footer.tsx b/src/components/domains/TrialPayment/Cards/Footer/Footer.tsx
new file mode 100644
index 0000000..a3ffbc1
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/Footer/Footer.tsx
@@ -0,0 +1,49 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import {
+ Contacts,
+ Legal,
+ PaymentMethods,
+} from "@/components/domains/TrialPayment";
+
+interface FooterProps extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ contacts?: React.ComponentProps;
+ legal?: React.ComponentProps;
+ paymentMethods?: React.ComponentProps;
+}
+
+export default function Footer({
+ title,
+ contacts,
+ legal,
+ paymentMethods,
+ ...props
+}: FooterProps) {
+ return (
+
+ {title && (
+
+ )}
+ {contacts && }
+ {legal && }
+ {paymentMethods && }
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/PaymentButtons/PaymentButtons.tsx b/src/components/domains/TrialPayment/Cards/PaymentButtons/PaymentButtons.tsx
new file mode 100644
index 0000000..b801e48
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/PaymentButtons/PaymentButtons.tsx
@@ -0,0 +1,36 @@
+import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
+import { cn } from "@/lib/utils";
+
+interface PaymentButtonsProps extends React.ComponentProps<"div"> {
+ buttons: (React.ComponentProps & {
+ icon?: React.ReactNode;
+ })[];
+}
+export default function PaymentButtons({
+ buttons,
+ ...props
+}: PaymentButtonsProps) {
+ return (
+
+ {buttons.map((button, index) => (
+
+ {button.icon}
+ {button.children}
+
+ ))}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/Reviews/Reviews.tsx b/src/components/domains/TrialPayment/Cards/Reviews/Reviews.tsx
new file mode 100644
index 0000000..0671aa3
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/Reviews/Reviews.tsx
@@ -0,0 +1,39 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { Review } from "../..";
+import { cn } from "@/lib/utils";
+import { useId } from "react";
+
+interface ReviewsProps extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ reviews: React.ComponentProps[];
+}
+
+export default function Reviews({ reviews, title, ...props }: ReviewsProps) {
+ const reviewId = useId();
+ return (
+
+ {title && (
+
+ )}
+ {!!reviews.length && (
+
+ {reviews.map((review, index) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepToSeeSoulmate.tsx b/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepToSeeSoulmate.tsx
new file mode 100644
index 0000000..b5fce48
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepToSeeSoulmate.tsx
@@ -0,0 +1,60 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface StepToSeeSoulmateProps
+ extends Omit, "title"> {
+ title: TypographyProps<"h4">;
+ description: TypographyProps<"p">;
+ icon: React.ReactNode;
+ isActive: boolean;
+}
+
+export default function StepToSeeSoulmate({
+ title,
+ description,
+ icon,
+ isActive,
+ ...props
+}: StepToSeeSoulmateProps) {
+ return (
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepsToSeeSoulmate.tsx b/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepsToSeeSoulmate.tsx
new file mode 100644
index 0000000..e8741ce
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/StepsToSeeSoulmate/StepsToSeeSoulmate.tsx
@@ -0,0 +1,37 @@
+import { cn } from "@/lib/utils";
+import StepToSeeSoulmate from "./StepToSeeSoulmate";
+import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
+
+interface StepsToSeeSoulmateProps extends React.ComponentProps<"div"> {
+ steps: React.ComponentProps[];
+ button?: React.ComponentProps;
+}
+
+export default function StepsToSeeSoulmate({
+ steps,
+ button,
+ ...props
+}: StepsToSeeSoulmateProps) {
+ return (
+
+
+ {steps.map((step, index) => (
+
+ ))}
+
+
+ {button && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/StillHaveQuestions/StillHaveQuestions.tsx b/src/components/domains/TrialPayment/Cards/StillHaveQuestions/StillHaveQuestions.tsx
new file mode 100644
index 0000000..88f1d43
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/StillHaveQuestions/StillHaveQuestions.tsx
@@ -0,0 +1,74 @@
+import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface StillHaveQuestionsProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ actionButton?: React.ComponentProps;
+ contactButton?: React.ComponentProps;
+}
+
+export default function StillHaveQuestions({
+ title,
+ actionButton,
+ contactButton,
+ ...props
+}: StillHaveQuestionsProps) {
+ return (
+
+ {title && (
+
+ )}
+ {actionButton && (
+
+ )}
+ {contactButton && (
+
+
+
+
+ {contactButton.children}
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/TotalPrice/TotalPrice.tsx b/src/components/domains/TrialPayment/Cards/TotalPrice/TotalPrice.tsx
new file mode 100644
index 0000000..ad362ba
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/TotalPrice/TotalPrice.tsx
@@ -0,0 +1,131 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import Card from "../../Card/Card";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+
+interface TotalPriceProps extends Omit, "title"> {
+ couponContainer: {
+ title?: TypographyProps<"h4">;
+ button?: React.ComponentProps;
+ };
+ priceContainer?: {
+ title?: TypographyProps<"h4">;
+ price?: TypographyProps<"span">;
+ oldPrice?: TypographyProps<"span">;
+ discount?: TypographyProps<"span">;
+ };
+}
+
+export default function TotalPrice({
+ couponContainer,
+ priceContainer,
+ ...props
+}: TotalPriceProps) {
+ return (
+
+ {couponContainer && (
+
+ {couponContainer.title && (
+
+ )}
+ {couponContainer.button && (
+
+ {couponContainer.button.children}
+
+
+
+
+ )}
+
+ )}
+ {priceContainer && (
+
+ {priceContainer.title && (
+
+ )}
+
+
+ {priceContainer.oldPrice && (
+
+ )}
+ {priceContainer.price && (
+
+ )}
+
+ {priceContainer.discount && (
+
+ )}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/TryForDays/TryForDays.tsx b/src/components/domains/TrialPayment/Cards/TryForDays/TryForDays.tsx
new file mode 100644
index 0000000..b54a747
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/TryForDays/TryForDays.tsx
@@ -0,0 +1,54 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import TextList from "@/components/widgets/TextList/TextList";
+import { cn } from "@/lib/utils";
+
+interface TryForDaysProps extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ textListProps?: React.ComponentProps;
+}
+
+export default function TryForDays({
+ title,
+ textListProps,
+ ...props
+}: TryForDaysProps) {
+ return (
+
+ {title && (
+
+ )}
+ {textListProps && (
+
+
+
+ }
+ {...textListProps}
+ className={cn(
+ "mt-[17px] pl-[37px] space-y-3",
+ textListProps.className
+ )}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/UnlockYourSketch/UnlockYourSketch.tsx b/src/components/domains/TrialPayment/Cards/UnlockYourSketch/UnlockYourSketch.tsx
new file mode 100644
index 0000000..ac7afb6
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/UnlockYourSketch/UnlockYourSketch.tsx
@@ -0,0 +1,112 @@
+import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import Image from "next/image";
+
+interface UnlockYourSketchProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ subtitle?: TypographyProps<"p">;
+ button?: React.ComponentProps;
+ image?: React.ComponentProps;
+ blur?: React.ComponentProps<"div"> & {
+ text?: TypographyProps<"p">;
+ icon?: React.ReactNode;
+ };
+}
+
+export default function UnlockYourSketch({
+ title,
+ subtitle: description,
+ button,
+ image,
+ blur,
+ ...props
+}: UnlockYourSketchProps) {
+ return (
+
+ {title && (
+
+ )}
+ {description && (
+
+ )}
+
+ {image && (
+
+ )}
+ {blur && (
+
+ {blur?.icon}
+ {blur.text && (
+
+ )}
+
+ )}
+
+ {button && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/UsersPortraits/UsersPortraits.tsx b/src/components/domains/TrialPayment/Cards/UsersPortraits/UsersPortraits.tsx
new file mode 100644
index 0000000..1de4ff0
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/UsersPortraits/UsersPortraits.tsx
@@ -0,0 +1,73 @@
+import { ActionButton } from "@/components/ui/ActionButton/ActionButton";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import Image from "next/image";
+
+interface UsersPortraitsProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ imgs?: React.ComponentProps[];
+ button?: React.ComponentProps;
+}
+
+export default function UsersPortraits({
+ title,
+ imgs,
+ button,
+ ...props
+}: UsersPortraitsProps) {
+ return (
+
+ {title && (
+
+ )}
+
+ {imgs &&
+ imgs.map((img, index) => (
+
+ ))}
+
+ {button && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Cards/index.ts b/src/components/domains/TrialPayment/Cards/index.ts
new file mode 100644
index 0000000..eb50b5c
--- /dev/null
+++ b/src/components/domains/TrialPayment/Cards/index.ts
@@ -0,0 +1,11 @@
+export { default as UnlockYourSketch } from "./UnlockYourSketch/UnlockYourSketch";
+export { default as FindingOneGuide } from "./FindingOneGuide/FindingOneGuide";
+export { default as TotalPrice } from "./TotalPrice/TotalPrice";
+export { default as PaymentButtons } from "./PaymentButtons/PaymentButtons";
+export { default as UsersPortraits } from "./UsersPortraits/UsersPortraits";
+export { default as StepsToSeeSoulmate } from "./StepsToSeeSoulmate/StepsToSeeSoulmate";
+export { default as TryForDays } from "./TryForDays/TryForDays";
+export { default as Reviews } from "./Reviews/Reviews";
+export { default as CommonQuestions } from "./CommonQuestions/CommonQuestions";
+export { default as StillHaveQuestions } from "./StillHaveQuestions/StillHaveQuestions";
+export { default as Footer } from "./Footer/Footer";
diff --git a/src/components/domains/TrialPayment/CommonQuestion/CommonQuestion.tsx b/src/components/domains/TrialPayment/CommonQuestion/CommonQuestion.tsx
new file mode 100644
index 0000000..81485fc
--- /dev/null
+++ b/src/components/domains/TrialPayment/CommonQuestion/CommonQuestion.tsx
@@ -0,0 +1,75 @@
+import {
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { cn } from "@/lib/utils";
+import Card from "../Card/Card";
+
+interface CommonQuestionProps
+ extends Omit, "content"> {
+ trigger?: React.ComponentProps;
+ content?: React.ComponentProps;
+}
+
+export default function CommonQuestion({
+ trigger,
+ content,
+ ...props
+}: CommonQuestionProps) {
+ return (
+
+
+ {trigger && (
+
+
+ {trigger?.children}
+
+ )}
+ {content && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Contacts/Contacts.tsx b/src/components/domains/TrialPayment/Contacts/Contacts.tsx
new file mode 100644
index 0000000..3142941
--- /dev/null
+++ b/src/components/domains/TrialPayment/Contacts/Contacts.tsx
@@ -0,0 +1,91 @@
+import { Button } from "@/components/ui/button";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import Link from "next/link";
+
+interface ContactsProps extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ email?: React.ComponentProps & { href: string };
+ address?: TypographyProps<"address">;
+}
+export default function Contacts({
+ title,
+ email,
+ address,
+ ...props
+}: ContactsProps) {
+ return (
+
+ {title && (
+
+ )}
+
+ {email && (
+
+
+
+
+
+ {email.children}
+
+
+ )}
+ {address && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Header/Header.tsx b/src/components/domains/TrialPayment/Header/Header.tsx
new file mode 100644
index 0000000..364b298
--- /dev/null
+++ b/src/components/domains/TrialPayment/Header/Header.tsx
@@ -0,0 +1,71 @@
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { useTimer, UseTimerOptions } from "@/hooks/timer/useTimer";
+
+interface HeaderProps extends React.ComponentProps<"header"> {
+ button?: React.ComponentProps;
+ timerHookProps?: UseTimerOptions;
+ timer?: TypographyProps<"span">;
+ text?: TypographyProps<"p">;
+}
+
+export default function Header({
+ button,
+ text,
+ timerHookProps,
+ timer,
+ ...props
+}: HeaderProps) {
+ const { time } = useTimer({
+ initialSeconds: timerHookProps?.initialSeconds || 600,
+ ...timerHookProps,
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/JoinedToday/JoinedToday.tsx b/src/components/domains/TrialPayment/JoinedToday/JoinedToday.tsx
new file mode 100644
index 0000000..3703133
--- /dev/null
+++ b/src/components/domains/TrialPayment/JoinedToday/JoinedToday.tsx
@@ -0,0 +1,47 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface JoinedTodayProps extends React.ComponentProps<"div"> {
+ icon?: React.ReactNode;
+ count?: TypographyProps<"span">;
+ text?: TypographyProps<"p">;
+}
+
+export default function JoinedToday({
+ icon,
+ count,
+ text,
+ ...props
+}: JoinedTodayProps) {
+ return (
+
+ {icon}
+ {count && (
+
+ )}
+ {text && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/JoinedTodayWithAvatars/JoinedTodayWithAvatars.tsx b/src/components/domains/TrialPayment/JoinedTodayWithAvatars/JoinedTodayWithAvatars.tsx
new file mode 100644
index 0000000..87a1c09
--- /dev/null
+++ b/src/components/domains/TrialPayment/JoinedTodayWithAvatars/JoinedTodayWithAvatars.tsx
@@ -0,0 +1,59 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import Avatars from "@/components/widgets/Avatars/Avatars";
+import { cn } from "@/lib/utils";
+
+interface JoinedTodayWithAvatarsProps extends React.ComponentProps<"div"> {
+ avatars?: React.ComponentProps;
+ count?: TypographyProps<"span">;
+ text?: TypographyProps<"p">;
+}
+
+export default function JoinedTodayWithAvatars({
+ avatars,
+ count,
+ text,
+ ...props
+}: JoinedTodayWithAvatarsProps) {
+ return (
+
+ {avatars && (
+
+ )}
+ {text && (
+
+ {count && (
+
+ )}{" "}
+ {text.children}
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Legal/Legal.tsx b/src/components/domains/TrialPayment/Legal/Legal.tsx
new file mode 100644
index 0000000..3247bca
--- /dev/null
+++ b/src/components/domains/TrialPayment/Legal/Legal.tsx
@@ -0,0 +1,63 @@
+import { Button } from "@/components/ui/button";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import Link from "next/link";
+
+interface LegalProps extends Omit, "title"> {
+ links?: (React.ComponentProps & { href: string })[];
+ copyright?: TypographyProps<"p">;
+ title?: TypographyProps<"h3">;
+}
+
+export default function Legal({
+ links,
+ copyright,
+ title,
+ ...props
+}: LegalProps) {
+ return (
+
+ {title && (
+
+ )}
+
+ {links &&
+ links.map((link, index) => (
+
+ {link.children}
+
+ ))}
+
+ {copyright && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/MoneyBackGuarantee/MoneyBackGuarantee.tsx b/src/components/domains/TrialPayment/MoneyBackGuarantee/MoneyBackGuarantee.tsx
new file mode 100644
index 0000000..c022a83
--- /dev/null
+++ b/src/components/domains/TrialPayment/MoneyBackGuarantee/MoneyBackGuarantee.tsx
@@ -0,0 +1,79 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface MoneyBackGuaranteeProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h4">;
+ text?: TypographyProps<"p">;
+ icon?: React.ReactNode;
+}
+
+export default function MoneyBackGuarantee({
+ title,
+ text,
+ icon,
+ ...props
+}: MoneyBackGuaranteeProps) {
+ return (
+
+ {icon ? (
+ icon
+ ) : (
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {title && (
+
+ )}
+ {text && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/PaymentMethods/PaymentMethods.tsx b/src/components/domains/TrialPayment/PaymentMethods/PaymentMethods.tsx
new file mode 100644
index 0000000..f14d5c6
--- /dev/null
+++ b/src/components/domains/TrialPayment/PaymentMethods/PaymentMethods.tsx
@@ -0,0 +1,47 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import Image from "next/image";
+
+interface PaymentMethodsProps
+ extends Omit, "title"> {
+ title?: TypographyProps<"h3">;
+ methods?: React.ComponentProps[];
+}
+
+export default function PaymentMethods({
+ title,
+ methods,
+ ...props
+}: PaymentMethodsProps) {
+ return (
+
+ {title && (
+
+ )}
+
+ {methods &&
+ methods.map((method, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Policy/Policy.tsx b/src/components/domains/TrialPayment/Policy/Policy.tsx
new file mode 100644
index 0000000..d23815c
--- /dev/null
+++ b/src/components/domains/TrialPayment/Policy/Policy.tsx
@@ -0,0 +1,24 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface PolicyProps extends React.ComponentProps<"div"> {
+ text?: TypographyProps<"p">;
+}
+
+export default function Policy({ text, ...props }: PolicyProps) {
+ return (
+
+ {text && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/ProgressToSeeSoulmate/ProgressToSeeSoulmate.tsx b/src/components/domains/TrialPayment/ProgressToSeeSoulmate/ProgressToSeeSoulmate.tsx
new file mode 100644
index 0000000..fcaa2d8
--- /dev/null
+++ b/src/components/domains/TrialPayment/ProgressToSeeSoulmate/ProgressToSeeSoulmate.tsx
@@ -0,0 +1,80 @@
+import { Progress } from "@/components/ui/progress";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface ProgressToSeeSoulmateProps
+ extends Omit, "title"> {
+ title: TypographyProps<"h3">;
+ progress: React.ComponentProps;
+ progressText?: {
+ leftText?: TypographyProps<"span">;
+ rightText?: TypographyProps<"span">;
+ };
+}
+
+export default function ProgressToSeeSoulmate({
+ title,
+ progress,
+ progressText,
+ ...props
+}: ProgressToSeeSoulmateProps) {
+ return (
+
+ {title && (
+
+ )}
+
+ {progressText && (
+
+ {progressText.leftText && (
+
+ )}
+ {progressText.rightText && (
+
+ )}
+
+ )}
+ {progress && (
+
div]:bg-[#1047A2]",
+ progress.classNameProgressContainer
+ )}
+ />
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/Review/Review.tsx b/src/components/domains/TrialPayment/Review/Review.tsx
new file mode 100644
index 0000000..2dc12ac
--- /dev/null
+++ b/src/components/domains/TrialPayment/Review/Review.tsx
@@ -0,0 +1,100 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import Stars from "@/components/widgets/Stars/Stars";
+import { cn } from "@/lib/utils";
+import Image from "next/image";
+import { Card } from "..";
+import { ArrowRight } from "lucide-react";
+import Avatar from "@/components/ui/Avatar/Avatar";
+
+interface ReviewProps extends React.ComponentProps {
+ avatar?: React.ComponentProps;
+ name?: TypographyProps<"span">;
+ stars?: React.ComponentProps;
+ date?: TypographyProps<"span">;
+ text?: TypographyProps<"p">;
+ portrait?: React.ComponentProps;
+ photo?: React.ComponentProps;
+}
+
+export default function Review({
+ stars,
+ avatar,
+ name,
+ date,
+ text,
+ portrait,
+ photo,
+ ...props
+}: ReviewProps) {
+ return (
+
+
+ {avatar && (
+
+ )}
+
+ {name && (
+
+ )}
+
+ {stars && }
+ {date && (
+
+ )}
+
+
+
+ {text && (
+
+ )}
+
+ {portrait && (
+
+ )}
+
+ {photo && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/TrustedByOver/TrustedByOver.tsx b/src/components/domains/TrialPayment/TrustedByOver/TrustedByOver.tsx
new file mode 100644
index 0000000..8281cac
--- /dev/null
+++ b/src/components/domains/TrialPayment/TrustedByOver/TrustedByOver.tsx
@@ -0,0 +1,34 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+
+interface TrustedByOverProps extends React.ComponentProps<"div"> {
+ icon?: React.ReactNode;
+ text?: TypographyProps<"p">;
+}
+
+export default function TrustedByOver({
+ icon,
+ text,
+ ...props
+}: TrustedByOverProps) {
+ return (
+
+ {icon}
+ {text && (
+
+ )}
+
+ );
+}
diff --git a/src/components/domains/TrialPayment/index.ts b/src/components/domains/TrialPayment/index.ts
new file mode 100644
index 0000000..d76b213
--- /dev/null
+++ b/src/components/domains/TrialPayment/index.ts
@@ -0,0 +1,13 @@
+export { default as Header } from "./Header/Header";
+export { default as Card } from "./Card/Card";
+export { default as JoinedToday } from "./JoinedToday/JoinedToday";
+export { default as TrustedByOver } from "./TrustedByOver/TrustedByOver";
+export { default as MoneyBackGuarantee } from "./MoneyBackGuarantee/MoneyBackGuarantee";
+export { default as Policy } from "./Policy/Policy";
+export { default as JoinedTodayWithAvatars } from "./JoinedTodayWithAvatars/JoinedTodayWithAvatars";
+export { default as ProgressToSeeSoulmate } from "./ProgressToSeeSoulmate/ProgressToSeeSoulmate";
+export { default as Review } from "./Review/Review";
+export { default as CommonQuestion } from "./CommonQuestion/CommonQuestion";
+export { default as Contacts } from "./Contacts/Contacts";
+export { default as Legal } from "./Legal/Legal";
+export { default as PaymentMethods } from "./PaymentMethods/PaymentMethods";
diff --git a/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx b/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx
index eeff31e..0caa29f 100644
--- a/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx
+++ b/src/components/templates/SoulmatePortrait/SoulmatePortrait.stories.tsx
@@ -1,6 +1,7 @@
import { Meta, StoryObj } from "@storybook/nextjs-vite";
import SoulmatePortrait from "./SoulmatePortrait";
import { fn } from "storybook/test";
+import Typography from "@/components/ui/Typography/Typography";
/** Reusable SoulmatePortrait page Component */
const meta: Meta = {
@@ -30,6 +31,74 @@ const meta: Meta = {
title: {
children: "Soulmate Portrait",
},
+ subtitle: {
+ children: "Готов увидеть, кто твоя настоящая Родственная душа?",
+ },
+ textListProps: {
+ items: [
+ {
+ children:
+ "Всего 2 минуты — и Портрет откроет того, кто связан с тобой судьбой.",
+ },
+ {
+ children: "Поразительная точность 99%.",
+ },
+ {
+ children: "Тебя ждёт неожиданное открытие.",
+ },
+ {
+ children: "Осталось лишь осмелиться взглянуть.",
+ },
+ ],
+ },
+ soulmatePortraitsDeliveredProps: {
+ image: "/soulmate-portrait-delivered-male.jpg",
+ textProps: {
+ children: "soulmate portraits delivered today",
+ },
+ avatarsProps: {
+ avatars: [
+ {
+ imageProps: {
+ src: "/avatars/male-1.jpg",
+ alt: "Male 1",
+ },
+ fallbackProps: {
+ children: "M1",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-2.jpg",
+ alt: "Male 2",
+ },
+ fallbackProps: {
+ children: "M2",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-3.jpg",
+ alt: "Male 3",
+ },
+ fallbackProps: {
+ children: "M3",
+ },
+ },
+ {
+ fallbackProps: {
+ children: (
+
+ 900+
+
+ ),
+ className: "bg-background",
+ },
+ className: "w-fit px-1",
+ },
+ ],
+ },
+ },
},
argTypes: {},
};
diff --git a/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx b/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx
index f16902e..4d221d6 100644
--- a/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx
+++ b/src/components/templates/SoulmatePortrait/SoulmatePortrait.tsx
@@ -3,6 +3,7 @@ import Typography, {
} from "@/components/ui/Typography/Typography";
import { BottomActionButton } from "@/components/widgets/BottomActionButton/BottomActionButton";
import PrivacyTermsConsent from "@/components/widgets/PrivacyTermsConsent/PrivacyTermsConsent";
+import SoulmatePortraitsDelivered from "@/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered";
import { useDynamicSize } from "@/hooks/DOM/useDynamicSize";
import { cn } from "@/lib/utils";
@@ -10,13 +11,23 @@ export interface SoulmatePortraitProps
extends Omit, "title"> {
bottomActionButtonProps?: React.ComponentProps;
privacyTermsConsentProps?: React.ComponentProps;
- title?: TypographyProps<"h2">;
+ title?: TypographyProps<"h1">;
+ soulmatePortraitsDeliveredProps?: React.ComponentProps<
+ typeof SoulmatePortraitsDelivered
+ >;
+ subtitle?: TypographyProps<"h2">;
+ textListProps?: React.ComponentProps<"ul"> & {
+ items: TypographyProps<"li">[];
+ };
}
export default function SoulmatePortrait({
bottomActionButtonProps,
privacyTermsConsentProps,
title,
+ subtitle,
+ soulmatePortraitsDeliveredProps,
+ textListProps,
...props
}: SoulmatePortraitProps) {
const {
@@ -28,7 +39,7 @@ export default function SoulmatePortrait({
return (
)}
+ {soulmatePortraitsDeliveredProps && (
+
+ )}
+
+ {subtitle && (
+
+ )}
+ {textListProps && (
+
+ {textListProps?.items.map((item, index) => (
+
+ ))}
+
+ )}
+
{bottomActionButtonProps && (
= {
+ title: "Templates/TrialPayment",
+ component: TrialPayment,
+ tags: ["autodocs"],
+ parameters: {
+ layout: "fullscreen",
+ },
+ args: {
+ header: {
+ timerHookProps: {
+ initialSeconds: 600,
+ },
+ text: {
+ children: "⚠️ Your sketch expires soon!",
+ },
+ button: {
+ children: "ПОЛУЧИТЬ",
+ onClick: fn(),
+ },
+ },
+ unlockYourSketch: {
+ title: {
+ children: "Unlock Your Sketch",
+ },
+ subtitle: {
+ children: "Just One Click to Reveal Your Match!",
+ },
+ image: {
+ src: "/trial-payment/portrait-female.jpg",
+ alt: "wall portrait female",
+ priority: true,
+ },
+ blur: {
+ text: {
+ children: "Unlock to reveal your personalized portrait",
+ },
+ icon: (
+
+
+
+ ),
+ },
+ button: {
+ children: "Get Me Soulmate Sketch",
+ onClick: fn(),
+ },
+ },
+ joinedToday: {
+ icon: (
+
+
+
+ ),
+ count: {
+ children: "954",
+ },
+ text: {
+ children: "Joined today",
+ },
+ },
+ trustedByOver: {
+ icon: (
+
+
+
+ ),
+ text: {
+ children: (
+ <>
+ Trusted by over 355,000 people.
+ >
+ ),
+ },
+ },
+ findingOneGuide: {
+ header: {
+ emoji: {
+ children: "❤️",
+ },
+ title: {
+ children: "Finding the One Guide",
+ },
+ },
+ text: {
+ children:
+ "You're not just looking for someone — you're. You're not just looking for someone — you'reYou're not just looking for someone — you'reYou're not just looking for someone — you'reYou're not just looking for someone — you're. You're not just looking for someone — you're. You're not just looking for someone — you'reYou're not just looking for someone — you'reYou're not just looking for someone — you'reYou're not just looking for someone — you're",
+ },
+ blur: {
+ text: {
+ children: "Чтобы открыть весь отчёт, нужен полный доступ.",
+ },
+ icon: (
+
+
+
+ ),
+ },
+ },
+ tryForDays: {
+ title: {
+ children: "Попробуйте в течение 7 дней!",
+ },
+ textListProps: {
+ items: [
+ {
+ children:
+ "Receive a hand-drawn sketch of your soulmate, crafted by a trained AI-model.",
+ },
+ {
+ children:
+ "Reveal the path to your soulmate with the Finding the One guide.",
+ },
+ {
+ children:
+ "Talk to live experts and get guidance on finding your soulmate.",
+ },
+ {
+ children:
+ "Start your 7-day trial for just $1.00 — then only $14.50/week for full access.",
+ },
+ {
+ children: "Cancel anytime—just 24 hours before renewal.",
+ },
+ ],
+ listStyleType: "none",
+ },
+ },
+ totalPrice: {
+ couponContainer: {
+ title: {
+ children: (
+ <>
+ Coupon
+
+ Code
+ >
+ ),
+ },
+ button: {
+ children: "SOULMATE94",
+ onClick: fn(),
+ },
+ },
+ priceContainer: {
+ title: {
+ children: "Total",
+ },
+ price: {
+ children: "$1.00",
+ },
+ oldPrice: {
+ children: "$14.99",
+ },
+ discount: {
+ children: "94% discount applied",
+ },
+ },
+ },
+ paymentButtons: {
+ buttons: [
+ {
+ children: "Pay",
+ icon: (
+
+
+
+
+
+
+
+
+
+
+ ),
+ },
+ {
+ children: "Pay",
+ icon: (
+
+
+
+
+
+
+ ),
+ },
+ {
+ children: "Credit or debit card",
+ icon: (
+
+
+
+
+
+
+
+
+
+
+ ),
+ className: "bg-primary",
+ },
+ ],
+ },
+ moneyBackGuarantee: {
+ title: {
+ children: "30-DAY MONEY-BACK GUARANTEE",
+ },
+ text: {
+ children:
+ "If you don't receive your soulmate sketch, we'll refund your money!",
+ },
+ },
+ policy: {
+ text: {
+ children:
+ "By clicking Continue, you agree to our Terms of Use & Service and Privacy Policy. You also acknowledge that your 1 week introductory plan to Respontika, billed at $1.00, will automatically renew at $14.50 every 1 week unless canceled before the end of the trial period.",
+ },
+ },
+ usersPortraits: {
+ title: {
+ children: "Our Users' Soulmate Portraits",
+ },
+ imgs: [
+ {
+ src: "/trial-payment/users-portraits/1.jpg",
+ alt: "wall portrait 1",
+ },
+ {
+ src: "/trial-payment/users-portraits/2.jpg",
+ alt: "wall portrait 2",
+ },
+ {
+ src: "/trial-payment/users-portraits/3.jpg",
+ alt: "wall portrait 3",
+ },
+ ],
+ button: {
+ children: "Get me soulmate sketch",
+ onClick: fn(),
+ },
+ },
+ joinedTodayWithAvatars: {
+ avatars: {
+ avatars: [
+ {
+ imageProps: {
+ src: "/trial-payment/avatars/1.jpg",
+ alt: "Avatar 1",
+ },
+ fallbackProps: {
+ children: "A1",
+ },
+ },
+ {
+ imageProps: {
+ src: "/trial-payment/avatars/2.jpg",
+ alt: "Avatar 2",
+ },
+ fallbackProps: {
+ children: "A2",
+ },
+ },
+ {
+ imageProps: {
+ src: "/trial-payment/avatars/3.jpg",
+ alt: "Avatar 3",
+ },
+ fallbackProps: {
+ children: "A3",
+ },
+ },
+ {
+ imageProps: {
+ src: "/trial-payment/avatars/4.jpg",
+ alt: "Avatar 4",
+ },
+ fallbackProps: {
+ children: "A4",
+ },
+ },
+ {
+ imageProps: {
+ src: "/trial-payment/avatars/5.jpg",
+ alt: "Avatar 5",
+ },
+ fallbackProps: {
+ children: "A5",
+ },
+ },
+ ],
+ },
+ count: {
+ children: "954",
+ },
+ text: {
+ children: "people joined today",
+ },
+ },
+ progressToSeeSoulmate: {
+ title: {
+ children: "See Your Soulmate – Just One Step Away",
+ },
+ progress: {
+ value: 92,
+ },
+ progressText: {
+ leftText: {
+ children: "Step 2 of 5",
+ },
+ rightText: {
+ children: "99% Complete",
+ },
+ },
+ },
+ stepsToSeeSoulmate: {
+ steps: [
+ {
+ title: { children: "Questions Answered" },
+ description: {
+ children:
+ "You've provided all the necessary information about your preferences and personality.",
+ },
+ icon: (
+
+
+
+ ),
+ isActive: true,
+ },
+ {
+ title: { children: "Profile Analysis" },
+ description: {
+ children:
+ "Our advanced system is creating your perfect soulmate profile.",
+ },
+ icon: (
+
+
+
+ ),
+ isActive: true,
+ },
+ {
+ title: { children: "Sketch Creation" },
+ description: {
+ children: "Your personalized soulmate sketch will be created.",
+ },
+ icon: (
+
+
+
+ ),
+ isActive: false,
+ },
+ {
+ title: { children: "Астрологические Идеи" },
+ description: {
+ children: "Уникальные астрологич...",
+ },
+ icon: (
+
+
+
+ ),
+ isActive: false,
+ },
+ {
+ title: { children: "Персонализированный чат с экспертом" },
+ description: {
+ children: "Персональные советы от экспертов по отношениям.",
+ },
+ icon: (
+
+
+
+ ),
+ isActive: false,
+ },
+ ],
+ button: {
+ children: "Show Me My Soulmate",
+ onClick: fn(),
+ },
+ },
+ reviews: {
+ title: {
+ children: (
+ <>
+ Loved and Trusted Worldwide
+ >
+ ),
+ },
+ reviews: [
+ {
+ avatar: {
+ imageProps: {
+ src: "/trial-payment/reviews/avatars/1.jpg",
+ alt: "Avatar 1",
+ },
+ fallbackProps: {
+ children: "A1",
+ },
+ },
+ portrait: {
+ src: "/trial-payment/reviews/portraits/1.jpg",
+ alt: "Portrait 1",
+ },
+ photo: {
+ src: "/trial-payment/reviews/photos/1.jpg",
+ alt: "Photo 1",
+ },
+ name: {
+ children: "Jennifer Wilson 🇺🇸",
+ },
+ stars: {
+ value: 5,
+ },
+ date: {
+ children: "1 day ago",
+ },
+ text: {
+ children: (
+ <>
+ “Я увидела свои ошибки… и нашла мужа”
+
+ Портрет сразу зацепил — было чувство, что я уже где-то его
+ видела. Но настоящий перелом произошёл после гайда: я поняла,
+ почему снова и снова выбирала «не тех». И самое удивительное —
+ вскоре я познакомилась с мужчиной, который оказался точной
+ копией того самого портрета. Сейчас он мой муж, и когда мы
+ сравнили рисунок с его фото, сходство было просто вау.
+ >
+ ),
+ },
+ },
+ {
+ avatar: {
+ imageProps: {
+ src: "/trial-payment/reviews/avatars/2.jpg",
+ alt: "Avatar 2",
+ },
+ fallbackProps: {
+ children: "A2",
+ },
+ },
+ portrait: {
+ src: "/trial-payment/reviews/portraits/2.jpg",
+ alt: "Portrait 2",
+ },
+ photo: {
+ src: "/trial-payment/reviews/photos/2.jpg",
+ alt: "Photo 2",
+ },
+ name: {
+ children: "Amanda Davis 🇨🇦",
+ },
+ stars: {
+ value: 5,
+ },
+ date: {
+ children: "4 days ago",
+ },
+ text: {
+ children: (
+ <>
+
+ “Я поняла своего партнёра лучше за один вечер, чем за
+ несколько лет”
+
+
+ Прошла тест ради интереса — портрет нас удивил. Но настоящий
+ прорыв случился, когда я прочитала гайд о второй половинке. Там
+ были точные подсказки о том, как мы можем поддерживать друг
+ друга. Цена смешная, а ценность огромная: теперь у нас меньше
+ недопониманий и больше тепла.
+ >
+ ),
+ },
+ },
+ {
+ avatar: {
+ imageProps: {
+ src: "/trial-payment/reviews/avatars/3.jpg",
+ alt: "Avatar 3",
+ },
+ fallbackProps: {
+ children: "A3",
+ },
+ },
+ portrait: {
+ src: "/trial-payment/reviews/portraits/3.jpg",
+ alt: "Portrait 3",
+ width: 96,
+ className: "w-[96px] h-[64px]",
+ },
+ photo: {
+ src: "/trial-payment/reviews/photos/3.jpg",
+ alt: "Photo 3",
+ },
+ name: {
+ children: "Michael Johnson 🇬🇧",
+ },
+ stars: {
+ value: 5,
+ },
+ date: {
+ children: "1 week ago",
+ },
+ text: {
+ children: (
+ <>
+ “Увидел её лицо — и мурашки по коже”
+
+ Когда пришёл результат теста и показали портрет, я реально
+ замер. Это была та самая девушка, с которой я начал встречаться
+ пару недель назад. И гайд прямо описал, почему мы тянемся друг к
+ другу. Честно, я не ожидал такого совпадения.
+ >
+ ),
+ },
+ },
+ ],
+ },
+ commonQuestions: {
+ title: {
+ children: "Common Questions",
+ },
+ questions: [
+ {
+ value: "when-will-i-receive-my-sketch",
+ trigger: {
+ children: "When will I receive my sketch?",
+ },
+ content: {
+ children:
+ "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account.",
+ },
+ },
+ {
+ value: "how-do-i-cancel-my-subscription",
+ trigger: {
+ children: "How do I cancel my subscription?",
+ },
+ content: {
+ children:
+ "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account.",
+ },
+ },
+ {
+ value: "how-accurate-are-the-readings",
+ trigger: {
+ children: "How accurate are the readings?",
+ },
+ content: {
+ children:
+ "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account.",
+ },
+ },
+ {
+ value: "is-my-data-secure-and-private",
+ trigger: {
+ children: "Is my data secure and private?",
+ },
+ content: {
+ children:
+ "Your personalized soulmate sketch will be delivered within 24-48 hours after completing your order. You'll receive an email notification when it's ready for viewing in your account.",
+ },
+ },
+ ],
+ accordionProps: {
+ defaultValue: "when-will-i-receive-my-sketch",
+ type: "single",
+ },
+ },
+ stillHaveQuestions: {
+ title: {
+ children: "Still have questions? We're here to help!",
+ },
+ actionButton: {
+ children: "Get me Soulmate Sketch",
+ onClick: fn(),
+ },
+ contactButton: {
+ children: "Contact Support",
+ onClick: fn(),
+ },
+ },
+ footer: {
+ title: {
+ children: "WIT LAB ©",
+ },
+ contacts: {
+ title: {
+ children: "CONTACTS",
+ },
+ email: {
+ href: "support@witlab.com",
+ children: "support@witlab.com",
+ },
+ address: {
+ children: "Wit Lab 2108 N ST STE N SACRAMENTO, CA95816, US",
+ },
+ },
+ legal: {
+ title: {
+ children: "LEGAL",
+ },
+ links: [
+ { href: "https://witlab.com/terms", children: "Terms of Service" },
+ { href: "https://witlab.com/privacy", children: "Privacy Policy" },
+ { href: "https://witlab.com/privacy", children: "Refund Policy" },
+ ],
+ copyright: {
+ children:
+ "Copyright © 2025 Wit Lab™. All rights reserved. All trademarks referenced herein are the properties of their respective owners.",
+ },
+ },
+ paymentMethods: {
+ title: {
+ children: "PAYMENT METHODS",
+ },
+ methods: [
+ {
+ src: "/trial-payment/payment-methods/visa.svg",
+ alt: "visa",
+ },
+ {
+ src: "/trial-payment/payment-methods/mastercard.svg",
+ alt: "mastercard",
+ },
+ {
+ src: "/trial-payment/payment-methods/discover.svg",
+ alt: "discover",
+ },
+ {
+ src: "/trial-payment/payment-methods/apple.svg",
+ alt: "apple",
+ },
+ {
+ src: "/trial-payment/payment-methods/google.svg",
+ alt: "google",
+ },
+ {
+ src: "/trial-payment/payment-methods/paypal.svg",
+ alt: "paypal",
+ },
+ ],
+ },
+ },
+ },
+ argTypes: {},
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default = {} satisfies Story;
diff --git a/src/components/templates/TrialPayment/TrialPayment.tsx b/src/components/templates/TrialPayment/TrialPayment.tsx
new file mode 100644
index 0000000..4dd79b7
--- /dev/null
+++ b/src/components/templates/TrialPayment/TrialPayment.tsx
@@ -0,0 +1,168 @@
+import {
+ Header,
+ JoinedToday,
+ JoinedTodayWithAvatars,
+ MoneyBackGuarantee,
+ Policy,
+ ProgressToSeeSoulmate,
+ TrustedByOver,
+} from "@/components/domains/TrialPayment";
+import {
+ FindingOneGuide,
+ CommonQuestions,
+ PaymentButtons,
+ Reviews,
+ StepsToSeeSoulmate,
+ TotalPrice,
+ TryForDays,
+ UnlockYourSketch,
+ UsersPortraits,
+ StillHaveQuestions,
+ Footer,
+} from "@/components/domains/TrialPayment/Cards";
+import { cn } from "@/lib/utils";
+
+interface TrialPaymentProps extends React.ComponentProps<"div"> {
+ header?: React.ComponentProps;
+ unlockYourSketch?: React.ComponentProps;
+ joinedToday?: React.ComponentProps;
+ trustedByOver?: React.ComponentProps;
+ findingOneGuide?: React.ComponentProps;
+ tryForDays?: React.ComponentProps;
+ totalPrice?: React.ComponentProps;
+ paymentButtons?: React.ComponentProps;
+ moneyBackGuarantee?: React.ComponentProps;
+ policy?: React.ComponentProps;
+ usersPortraits?: React.ComponentProps;
+ joinedTodayWithAvatars?: React.ComponentProps;
+ progressToSeeSoulmate?: React.ComponentProps;
+ stepsToSeeSoulmate?: React.ComponentProps;
+ reviews?: React.ComponentProps;
+ commonQuestions?: React.ComponentProps;
+ stillHaveQuestions?: React.ComponentProps;
+ footer?: React.ComponentProps;
+}
+
+export default function TrialPayment({
+ header,
+ unlockYourSketch,
+ joinedToday,
+ trustedByOver,
+ findingOneGuide,
+ tryForDays,
+ totalPrice,
+ paymentButtons,
+ moneyBackGuarantee,
+ policy,
+ usersPortraits,
+ joinedTodayWithAvatars,
+ progressToSeeSoulmate,
+ stepsToSeeSoulmate,
+ reviews,
+ commonQuestions,
+ stillHaveQuestions,
+ footer,
+ ...props
+}: TrialPaymentProps) {
+ return (
+
+ {header &&
}
+ {unlockYourSketch &&
}
+ {joinedToday && (
+
+ )}
+ {trustedByOver && (
+
+ )}
+ {findingOneGuide && (
+
+ )}
+ {tryForDays && (
+
+ )}
+ {totalPrice && (
+
+ )}
+ {paymentButtons && (
+
+ )}
+ {moneyBackGuarantee && (
+
+ )}
+ {policy && (
+
+ )}
+ {usersPortraits && (
+
+ )}
+ {joinedTodayWithAvatars && (
+
+ )}
+ {progressToSeeSoulmate && (
+
+ )}
+ {stepsToSeeSoulmate && (
+
+ )}
+ {reviews && (
+
+ )}
+ {commonQuestions && (
+
+ )}
+ {stillHaveQuestions && (
+
+ )}
+ {footer && (
+
+ )}
+
+ );
+}
diff --git a/src/components/ui/Avatar/Avatar.tsx b/src/components/ui/Avatar/Avatar.tsx
new file mode 100644
index 0000000..c49fc95
--- /dev/null
+++ b/src/components/ui/Avatar/Avatar.tsx
@@ -0,0 +1,23 @@
+import {
+ AvatarImage,
+ Avatar as AvatarComponent,
+ AvatarFallback,
+} from "../avatar";
+
+interface AvatarProps extends React.ComponentProps {
+ imageProps?: React.ComponentProps;
+ fallbackProps?: React.ComponentProps;
+}
+
+export default function Avatar({
+ imageProps,
+ fallbackProps,
+ ...props
+}: AvatarProps) {
+ return (
+
+ {imageProps && }
+ {fallbackProps && }
+
+ );
+}
diff --git a/src/components/ui/Typography/Typography.tsx b/src/components/ui/Typography/Typography.tsx
index 9b011d9..67da909 100644
--- a/src/components/ui/Typography/Typography.tsx
+++ b/src/components/ui/Typography/Typography.tsx
@@ -43,11 +43,12 @@ const typographyVariants = cva(cn("text-center text-foreground block"), {
inter: "font-inter",
geistSans: "font-geist-sans",
geistMono: "font-geist-mono",
+ poppins: "font-poppins",
},
},
defaultVariants: {
size: "md",
- weight: "regular",
+ weight: "regular",
color: "default",
align: "left",
font: "manrope",
@@ -56,7 +57,17 @@ const typographyVariants = cva(cn("text-center text-foreground block"), {
type TypographyElements = Pick<
JSX.IntrinsicElements,
- "span" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div"
+ | "span"
+ | "p"
+ | "h1"
+ | "h2"
+ | "h3"
+ | "h4"
+ | "h5"
+ | "h6"
+ | "div"
+ | "li"
+ | "address"
>;
export type TypographyProps =
@@ -87,12 +98,12 @@ export default function Typography<
);
// 🎨 Если включена разметка и это строка, используем MarkupText
- if (enableMarkup && typeof children === 'string') {
+ if (enableMarkup && typeof children === "string") {
return (
{children}
diff --git a/src/components/ui/accordion.stories.tsx b/src/components/ui/accordion.stories.tsx
new file mode 100644
index 0000000..2bb0361
--- /dev/null
+++ b/src/components/ui/accordion.stories.tsx
@@ -0,0 +1,144 @@
+import { Meta, StoryObj } from "@storybook/nextjs-vite";
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./accordion";
+
+/** Reusable Accordion Component */
+const meta: Meta = {
+ title: "Shadcn UI/Accordion",
+ component: Accordion,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ args: {
+ type: "single",
+ collapsible: true,
+ },
+ argTypes: {
+ type: {
+ control: { type: "select" },
+ options: ["single", "multiple"],
+ },
+ collapsible: {
+ control: { type: "boolean" },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default = {
+ render: (args) => (
+
+
+ Is it accessible?
+
+ Yes. It adheres to the WAI-ARIA design pattern.
+
+
+
+ Is it styled?
+
+ Yes. It comes with default styles that matches the other components' aesthetic.
+
+
+
+ Is it animated?
+
+ Yes. It's animated by default, but you can disable it if you prefer.
+
+
+
+ ),
+} satisfies Story;
+
+export const Multiple = {
+ args: {
+ type: "multiple",
+ },
+ render: (args) => (
+
+
+ Is it accessible?
+
+ Yes. It adheres to the WAI-ARIA design pattern.
+
+
+
+ Is it styled?
+
+ Yes. It comes with default styles that matches the other components' aesthetic.
+
+
+
+ Is it animated?
+
+ Yes. It's animated by default, but you can disable it if you prefer.
+
+
+
+ ),
+} satisfies Story;
+
+export const SingleItem = {
+ render: (args) => (
+
+
+ What is this component?
+
+ This is an accordion component built with Radix UI primitives. It provides a collapsible content area that can be expanded or collapsed by clicking the trigger.
+
+
+
+ ),
+} satisfies Story;
+
+export const LongContent = {
+ render: (args) => (
+
+
+ What are the features?
+
+
+
This accordion component includes:
+
+ Accessibility support with WAI-ARIA patterns
+ Smooth animations for opening and closing
+ Keyboard navigation support
+ Customizable styling with Tailwind CSS
+ Single or multiple item selection modes
+ Collapsible functionality
+
+
+ The component is built using Radix UI primitives, ensuring excellent accessibility and user experience across different devices and assistive technologies.
+
+
+
+
+
+ ),
+} satisfies Story;
+
+export const CustomStyling = {
+ render: (args) => (
+
+
+
+ Custom Styled Item
+
+
+ This accordion item has custom styling with blue colors and enhanced spacing.
+
+
+
+
+ Another Custom Item
+
+
+ Each item can have its own custom styling while maintaining the accordion functionality.
+
+
+
+ ),
+} satisfies Story;
+
diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..402ec74
--- /dev/null
+++ b/src/components/ui/accordion.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import * as React from "react";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { ChevronDownIcon } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ iconProps,
+ ...props
+}: React.ComponentProps & {
+ iconProps?: React.ComponentProps;
+}) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..5428990
--- /dev/null
+++ b/src/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
+
+import { cn } from "@/lib/utils";
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index 5cdba71..1dcda2f 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -17,7 +17,7 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
- "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ "border-2 border-primary bg-background shadow-xs dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
diff --git a/src/components/ui/progress.stories.tsx b/src/components/ui/progress.stories.tsx
index d3171cd..2db7f80 100644
--- a/src/components/ui/progress.stories.tsx
+++ b/src/components/ui/progress.stories.tsx
@@ -68,7 +68,7 @@ export const Complete = {
export const CustomColors = {
args: {
value: 80,
- classNameProgress: "bg-green-200 [&>div]:bg-green-600",
+ classNameProgressContainer: "bg-green-200 [&>div]:bg-green-600",
},
} satisfies Story;
diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx
index 450417e..d111a23 100644
--- a/src/components/ui/progress.tsx
+++ b/src/components/ui/progress.tsx
@@ -8,12 +8,12 @@ import { Label } from "./label";
interface ProgressProps
extends React.ComponentProps {
label?: string;
- classNameProgress?: string;
+ classNameProgressContainer?: string;
}
function Progress({
className,
- classNameProgress,
+ classNameProgressContainer,
value,
label,
...props
@@ -29,7 +29,7 @@ function Progress({
data-slot="progress"
className={cn(
"bg-border relative h-1.5 w-full overflow-hidden rounded-full",
- classNameProgress
+ classNameProgressContainer
)}
{...props}
>
diff --git a/src/components/widgets/Avatars/Avatars.stories.tsx b/src/components/widgets/Avatars/Avatars.stories.tsx
new file mode 100644
index 0000000..07d8f6d
--- /dev/null
+++ b/src/components/widgets/Avatars/Avatars.stories.tsx
@@ -0,0 +1,58 @@
+import { Meta, StoryObj } from "@storybook/nextjs-vite";
+import Avatars from "./Avatars";
+import Typography from "@/components/ui/Typography/Typography";
+
+/** Reusable Avatars Component */
+const meta: Meta = {
+ title: "Widgets/Avatars",
+ component: Avatars,
+ tags: ["autodocs"],
+ args: {
+ avatars: [
+ {
+ imageProps: {
+ src: "/avatars/male-1.jpg",
+ alt: "Male 1",
+ },
+ fallbackProps: {
+ children: "M1",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-2.jpg",
+ alt: "Male 2",
+ },
+ fallbackProps: {
+ children: "M2",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-3.jpg",
+ alt: "Male 3",
+ },
+ fallbackProps: {
+ children: "M3",
+ },
+ },
+ {
+ fallbackProps: {
+ children: (
+
+ 900+
+
+ ),
+ className: "bg-background",
+ },
+ className: "w-fit px-1",
+ },
+ ],
+ },
+ argTypes: {},
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default = {} satisfies Story;
diff --git a/src/components/widgets/Avatars/Avatars.tsx b/src/components/widgets/Avatars/Avatars.tsx
new file mode 100644
index 0000000..0e9f1b0
--- /dev/null
+++ b/src/components/widgets/Avatars/Avatars.tsx
@@ -0,0 +1,38 @@
+import Avatar from "@/components/ui/Avatar/Avatar";
+import { cn } from "@/lib/utils";
+
+interface AvatarsProps extends React.ComponentProps<"div"> {
+ avatars: React.ComponentProps[];
+ generalAvatarProps?: React.ComponentProps;
+}
+export default function Avatars({
+ avatars,
+ generalAvatarProps,
+ ...props
+}: AvatarsProps) {
+ return (
+
+ {avatars.map((avatar, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.stories.tsx b/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.stories.tsx
new file mode 100644
index 0000000..e8365be
--- /dev/null
+++ b/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.stories.tsx
@@ -0,0 +1,64 @@
+import { Meta, StoryObj } from "@storybook/nextjs-vite";
+import SoulmatePortraitsDelivered from "./SoulmatePortraitsDelivered";
+import Typography from "@/components/ui/Typography/Typography";
+
+/** Reusable SoulmatePortraitsDelivered Component */
+const meta: Meta = {
+ title: "Widgets/SoulmatePortraitsDelivered",
+ component: SoulmatePortraitsDelivered,
+ tags: ["autodocs"],
+ args: {
+ image: "/soulmate-portrait-delivered-male.jpg",
+ textProps: {
+ children: "soulmate portraits delivered today",
+ },
+ avatarsProps: {
+ avatars: [
+ {
+ imageProps: {
+ src: "/avatars/male-1.jpg",
+ alt: "Male 1",
+ },
+ fallbackProps: {
+ children: "M1",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-2.jpg",
+ alt: "Male 2",
+ },
+ fallbackProps: {
+ children: "M2",
+ },
+ },
+ {
+ imageProps: {
+ src: "/avatars/male-3.jpg",
+ alt: "Male 3",
+ },
+ fallbackProps: {
+ children: "M3",
+ },
+ },
+ {
+ fallbackProps: {
+ children: (
+
+ 900+
+
+ ),
+ className: "bg-background",
+ },
+ className: "w-fit px-1",
+ },
+ ],
+ },
+ },
+ argTypes: {},
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default = {} satisfies Story;
diff --git a/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.tsx b/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.tsx
new file mode 100644
index 0000000..ec78c53
--- /dev/null
+++ b/src/components/widgets/SoulmatePortraitsDelivered/SoulmatePortraitsDelivered.tsx
@@ -0,0 +1,52 @@
+import { cn } from "@/lib/utils";
+import Avatars from "../Avatars/Avatars";
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+
+interface SoulmatePortraitsDeliveredProps extends React.ComponentProps<"div"> {
+ image?: string;
+ textProps?: TypographyProps<"p">;
+ avatarsProps?: React.ComponentProps;
+}
+
+export default function SoulmatePortraitsDelivered({
+ image,
+ avatarsProps,
+ textProps,
+ ...props
+}: SoulmatePortraitsDeliveredProps) {
+ return (
+
+
+ {avatarsProps &&
}
+ {textProps && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/widgets/Stars/Stars.tsx b/src/components/widgets/Stars/Stars.tsx
new file mode 100644
index 0000000..b398583
--- /dev/null
+++ b/src/components/widgets/Stars/Stars.tsx
@@ -0,0 +1,31 @@
+import { cn } from "@/lib/utils";
+
+interface StarsProps extends React.ComponentProps<"div"> {
+ value?: number;
+ star?: React.ReactNode;
+}
+
+export default function Stars({ value = 5, star, ...props }: StarsProps) {
+ return (
+
+ {Array.from({ length: value }).map(
+ (_, index) =>
+ star || (
+
+
+
+ )
+ )}
+
+ );
+}
diff --git a/src/components/widgets/TextList/TextList.stories.tsx b/src/components/widgets/TextList/TextList.stories.tsx
new file mode 100644
index 0000000..4035878
--- /dev/null
+++ b/src/components/widgets/TextList/TextList.stories.tsx
@@ -0,0 +1,78 @@
+import { Meta, StoryObj } from "@storybook/nextjs-vite";
+import TextList from "./TextList";
+import { TypographyProps } from "@/components/ui/Typography/Typography";
+
+const meta: Meta = {
+ title: "Widgets/TextList",
+ component: TextList,
+ parameters: {
+ layout: "centered",
+ },
+ tags: ["autodocs"],
+ argTypes: {
+ items: {
+ control: { type: "object" },
+ description: "Массив элементов списка с пропсами Typography",
+ },
+ listStyleType: {
+ control: { type: "select" },
+ options: ["decimal", "disc", "none"],
+ description: "Тип маркера списка",
+ },
+ markerImage: {
+ control: { type: "text" },
+ description: "URL изображения для маркера списка",
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const defaultItems: TypographyProps<"li">[] = [
+ {
+ children: "Первый элемент списка",
+ },
+ {
+ children: "Второй элемент списка",
+ },
+ {
+ children: "Третий элемент списка",
+ },
+];
+
+export const Default: Story = {
+ args: {
+ items: defaultItems,
+ listStyleType: "disc",
+ },
+};
+
+export const Decimal: Story = {
+ args: {
+ items: defaultItems,
+ listStyleType: "decimal",
+ },
+};
+
+export const Disc: Story = {
+ args: {
+ items: defaultItems,
+ listStyleType: "disc",
+ },
+};
+
+export const None: Story = {
+ args: {
+ items: defaultItems,
+ listStyleType: "none",
+ },
+};
+
+export const WithImage: Story = {
+ args: {
+ items: defaultItems,
+ listStyleType: "image",
+ markerImage: "/check-mark.svg",
+ },
+};
diff --git a/src/components/widgets/TextList/TextList.tsx b/src/components/widgets/TextList/TextList.tsx
new file mode 100644
index 0000000..0678852
--- /dev/null
+++ b/src/components/widgets/TextList/TextList.tsx
@@ -0,0 +1,71 @@
+import Typography, {
+ TypographyProps,
+} from "@/components/ui/Typography/Typography";
+import { cn } from "@/lib/utils";
+import { cva, VariantProps } from "class-variance-authority";
+import React, { useId } from "react";
+
+const textListVariants = cva("", {
+ variants: {
+ listStyleType: {
+ decimal: "list-decimal pl-6",
+ disc: "list-disc pl-6",
+ image: "list-image pl-6",
+ none: "list-none pl-0",
+ },
+ },
+ defaultVariants: {
+ listStyleType: "disc",
+ },
+});
+
+interface TextListProps
+ extends React.ComponentProps<"ul">,
+ VariantProps {
+ items?: TypographyProps<"li">[];
+ itemProps?: Partial>;
+ markerImage?: string;
+ customMarker?: React.ReactNode;
+}
+
+export default function TextList({
+ items,
+ itemProps,
+ listStyleType,
+ markerImage,
+ customMarker,
+ ...props
+}: TextListProps) {
+ const itemId = useId();
+ return (
+
+ {items?.map((item, index) => (
+
+ {customMarker && (
+
+ {customMarker}
+
+ )}
+ {item.children || itemProps?.children}
+
+ ))}
+
+ );
+}
diff --git a/src/hooks/timer/useTimer.ts b/src/hooks/timer/useTimer.ts
new file mode 100644
index 0000000..1fb0b33
--- /dev/null
+++ b/src/hooks/timer/useTimer.ts
@@ -0,0 +1,71 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+
+import { formatSecondsToHHMMSS } from "@/shared/utils/date";
+
+export interface UseTimerOptions {
+ initialSeconds: number;
+ persist?: boolean;
+ storageKey?: string;
+}
+
+export function useTimer({
+ initialSeconds,
+ persist = false,
+ storageKey,
+}: UseTimerOptions) {
+ const [seconds, setSeconds] = useState(() => {
+ if (persist && storageKey) {
+ const saved = localStorage.getItem(storageKey);
+ if (saved !== null) {
+ const parsed = parseInt(saved, 10);
+ if (!isNaN(parsed)) return parsed;
+ }
+ }
+ return initialSeconds;
+ });
+
+ const intervalRef = useRef(null);
+
+ useEffect(() => {
+ if (persist && storageKey) {
+ localStorage.setItem(storageKey, seconds.toString());
+ }
+ }, [seconds, persist, storageKey]);
+
+ useEffect(() => {
+ if (seconds <= 0) return;
+ intervalRef.current = setInterval(() => {
+ setSeconds((prev) => {
+ if (prev <= 1) {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ return () => {
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, [seconds]);
+
+ const reset = useCallback(() => {
+ setSeconds(initialSeconds);
+ if (persist && storageKey) {
+ localStorage.setItem(storageKey, initialSeconds.toString());
+ }
+ }, [initialSeconds, persist, storageKey]);
+
+ return useMemo(
+ () => ({
+ time: formatSecondsToHHMMSS(seconds, { isHours: false }),
+ seconds,
+ reset,
+ isFinished: seconds === 0,
+ }),
+ [seconds, reset]
+ );
+}
diff --git a/src/shared/utils/date.ts b/src/shared/utils/date.ts
new file mode 100644
index 0000000..cef6707
--- /dev/null
+++ b/src/shared/utils/date.ts
@@ -0,0 +1,40 @@
+export const formatDate = (date: string | null) => {
+ if (!date) return null;
+ return new Date(date).toLocaleDateString("en-US", {
+ month: "long",
+ day: "numeric",
+ year: "numeric",
+ });
+};
+
+export const formatTime = (date: string | null) => {
+ if (!date) return null;
+ return new Date(date).toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+};
+
+export const formatSecondsToHHMMSS = (
+ seconds: number,
+ availableValues?: Partial<
+ Record<"isHours" | "isMinutes" | "isSeconds", boolean>
+ >
+) => {
+ const {
+ isHours = true,
+ isMinutes = true,
+ isSeconds = true,
+ } = availableValues || {};
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ return [
+ isHours && String(hours).padStart(2, "0"),
+ isMinutes && String(minutes).padStart(2, "0"),
+ isSeconds && String(secs).padStart(2, "0"),
+ ]
+ .filter(Boolean)
+ .join(":");
+};