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 ( +
+
+ {icon} +
+
+ + +
+
+ ); +} 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 && ( + + )} +
+ )} + {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 && ( + {image.alt} + )} + {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) => ( + {img.alt} + ))} +
+ {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 && ( +
+ + + + +
+ )} + {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 ( +
+
+ {text && ( + + )} + + {time} + +
+ {button && ( +
+ ); +} 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) => ( + + ))} +
+ {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) => ( + {method.alt} + ))} +
+
+ ); +} 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 && ( + {portrait.alt + )} + + {photo && ( + {photo.alt + )} +
+
+ ); +} 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(":"); +};