@thorprovider/blocks 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -3,6 +3,7 @@ import { z } from 'zod'
|
|
|
3
3
|
import {
|
|
4
4
|
categoryCardPropsSchema,
|
|
5
5
|
categoryGridPropsSchema,
|
|
6
|
+
navbarPropsSchema,
|
|
6
7
|
productCardPropsSchema,
|
|
7
8
|
productGridPropsSchema,
|
|
8
9
|
storeCategorySchema,
|
|
@@ -171,3 +172,57 @@ describe('category cell and listing contracts', () => {
|
|
|
171
172
|
expect(categoryCardPropsSchema.safeParse({ categories: [category] }).success).toBe(false)
|
|
172
173
|
})
|
|
173
174
|
})
|
|
175
|
+
|
|
176
|
+
describe('navbarPropsSchema', () => {
|
|
177
|
+
const full = {
|
|
178
|
+
presentation: 'mega' as const,
|
|
179
|
+
logoText: 'ElectroCuba',
|
|
180
|
+
logoHref: '/',
|
|
181
|
+
currentPath: '/tienda',
|
|
182
|
+
links: [
|
|
183
|
+
{
|
|
184
|
+
label: 'Catálogo',
|
|
185
|
+
groups: [
|
|
186
|
+
{
|
|
187
|
+
title: 'Esenciales',
|
|
188
|
+
items: [{ label: 'Camisas', href: '/camisas', description: 'Cortes diarios', icon: 'shirt' }],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{ label: 'Nosotros', href: '/nosotros', emphasis: true },
|
|
193
|
+
],
|
|
194
|
+
featured: {
|
|
195
|
+
eyebrow: 'Nueva temporada',
|
|
196
|
+
title: 'Lino',
|
|
197
|
+
description: 'Capas transpirables',
|
|
198
|
+
href: '/lino',
|
|
199
|
+
linkLabel: 'Descubrir',
|
|
200
|
+
image: '/lino.jpg',
|
|
201
|
+
imageAlt: 'Camisa de lino',
|
|
202
|
+
},
|
|
203
|
+
signIn: { label: 'Entrar', href: '/cuenta' },
|
|
204
|
+
cta: { label: 'Comprar' },
|
|
205
|
+
className: 'bg-brand-nav text-brand-nav-foreground',
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
it('round-trips a fully populated header', () => {
|
|
209
|
+
expect(navbarPropsSchema.parse(full)).toEqual(full)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('requires the brand text and the links list', () => {
|
|
213
|
+
expect(navbarPropsSchema.safeParse({ ...full, logoText: undefined }).success).toBe(false)
|
|
214
|
+
expect(navbarPropsSchema.safeParse({ logoText: 'ElectroCuba' }).success).toBe(false)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('rejects non-string hrefs and unknown presentations', () => {
|
|
218
|
+
expect(navbarPropsSchema.safeParse({ logoText: 'X', links: [{ label: 'Y', href: 42 }] }).success).toBe(false)
|
|
219
|
+
expect(navbarPropsSchema.safeParse({ ...full, presentation: 'sidebar' }).success).toBe(false)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('cannot carry host bindings across the serializable boundary', () => {
|
|
223
|
+
const parsed = navbarPropsSchema.parse({ ...full, iconMap: { shirt: () => null }, onSignIn: () => {} })
|
|
224
|
+
|
|
225
|
+
expect(parsed).not.toHaveProperty('iconMap')
|
|
226
|
+
expect(parsed).not.toHaveProperty('onSignIn')
|
|
227
|
+
})
|
|
228
|
+
})
|
package/src/lib/contracts.ts
CHANGED
|
@@ -495,7 +495,12 @@ export const heroImageSchema = z.object({
|
|
|
495
495
|
})
|
|
496
496
|
export type HeroImage = z.infer<typeof heroImageSchema>
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Serializable contract for the canonical commerce storefront shell
|
|
500
|
+
* (`StorefrontNavbar`). Identity, cart, and search state stay host-owned: the
|
|
501
|
+
* schema carries content and display state only, never cart arithmetic.
|
|
502
|
+
*/
|
|
503
|
+
export const storefrontNavbarPropsSchema = z.object({
|
|
499
504
|
storeCategories: z.array(storeCategorySchema),
|
|
500
505
|
shippingNote: z.string().optional(),
|
|
501
506
|
utilityLinks: z.array(utilityLinkSchema),
|
|
@@ -519,7 +524,7 @@ export const navbar4PropsSchema = z.object({
|
|
|
519
524
|
accountPending: z.boolean().optional(),
|
|
520
525
|
accountUnavailable: z.boolean().optional(),
|
|
521
526
|
})
|
|
522
|
-
export type
|
|
527
|
+
export type StorefrontNavbarSpecProps = z.infer<typeof storefrontNavbarPropsSchema>
|
|
523
528
|
|
|
524
529
|
/**
|
|
525
530
|
* Serializable contracts for the two bounded editorial sections (`faq`, `cta`).
|
|
@@ -533,6 +538,63 @@ export const ctaSchema = z.object({
|
|
|
533
538
|
})
|
|
534
539
|
export type CtaSpec = z.infer<typeof ctaSchema>
|
|
535
540
|
|
|
541
|
+
/** Bounded presentations of the page-level navigation header. */
|
|
542
|
+
export const navbarPresentationSchema = z.enum(['bar', 'mega', 'floating'])
|
|
543
|
+
export type NavbarPresentationSpec = z.infer<typeof navbarPresentationSchema>
|
|
544
|
+
|
|
545
|
+
export const navbarItemSchema = z.object({
|
|
546
|
+
label: z.string(),
|
|
547
|
+
href: z.string(),
|
|
548
|
+
description: z.string().optional(),
|
|
549
|
+
/** Key resolved through the consumer's host-bound `iconMap`. */
|
|
550
|
+
icon: z.string().optional(),
|
|
551
|
+
})
|
|
552
|
+
export type NavbarItemSpec = z.infer<typeof navbarItemSchema>
|
|
553
|
+
|
|
554
|
+
export const navbarGroupSchema = z.object({
|
|
555
|
+
title: z.string().optional(),
|
|
556
|
+
items: z.array(navbarItemSchema),
|
|
557
|
+
})
|
|
558
|
+
export type NavbarGroupSpec = z.infer<typeof navbarGroupSchema>
|
|
559
|
+
|
|
560
|
+
export const navbarLinkSchema = z.object({
|
|
561
|
+
label: z.string(),
|
|
562
|
+
href: z.string().optional(),
|
|
563
|
+
groups: z.array(navbarGroupSchema).optional(),
|
|
564
|
+
emphasis: z.boolean().optional(),
|
|
565
|
+
})
|
|
566
|
+
export type NavbarLinkSpec = z.infer<typeof navbarLinkSchema>
|
|
567
|
+
|
|
568
|
+
export const navbarFeaturedSchema = z.object({
|
|
569
|
+
eyebrow: z.string(),
|
|
570
|
+
title: z.string(),
|
|
571
|
+
description: z.string().optional(),
|
|
572
|
+
href: z.string(),
|
|
573
|
+
linkLabel: z.string().optional(),
|
|
574
|
+
image: z.string().optional(),
|
|
575
|
+
imageAlt: z.string().optional(),
|
|
576
|
+
})
|
|
577
|
+
export type NavbarFeaturedSpec = z.infer<typeof navbarFeaturedSchema>
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Serializable contract for the canonical page-level header (`Navbar`). The
|
|
581
|
+
* serializable subset excludes the React-only bindings: `iconMap`, `onSignIn`,
|
|
582
|
+
* and `onCtaClick` are bound by the host, exactly like the footer's icon map.
|
|
583
|
+
*/
|
|
584
|
+
export const navbarPropsSchema = z.object({
|
|
585
|
+
presentation: navbarPresentationSchema.optional(),
|
|
586
|
+
logoText: z.string(),
|
|
587
|
+
logoHref: z.string().optional(),
|
|
588
|
+
currentPath: z.string().optional(),
|
|
589
|
+
links: z.array(navbarLinkSchema),
|
|
590
|
+
featured: navbarFeaturedSchema.optional(),
|
|
591
|
+
signIn: ctaSchema.optional(),
|
|
592
|
+
cta: ctaSchema.optional(),
|
|
593
|
+
/** Consumer-supplied classes merged onto the header surface last. */
|
|
594
|
+
className: z.string().optional(),
|
|
595
|
+
})
|
|
596
|
+
export type NavbarSpecProps = z.infer<typeof navbarPropsSchema>
|
|
597
|
+
|
|
536
598
|
/**
|
|
537
599
|
* Serializable subset of the canonical `StorefrontHero`'s props (callbacks
|
|
538
600
|
* excluded). Every popularity, review, count, or countdown field is optional
|
package/tsup.config.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
// Emit one module per source file (no bundling) so consumers can import a
|
|
5
5
|
// single block and bundle only what they import. `outbase: 'src'` preserves
|
|
6
|
-
// the source layout under dist/ (e.g. dist/blocks/.../navbar
|
|
6
|
+
// the source layout under dist/ (e.g. dist/blocks/.../navbar.mjs).
|
|
7
7
|
entry: ['src/**/*.ts', 'src/**/*.tsx', '!src/**/*.test.ts', '!src/**/*.test.tsx'],
|
|
8
8
|
format: ['cjs', 'esm'],
|
|
9
9
|
// Declarations are emitted per-file by `tsc -p tsconfig.build.json` (see the
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, type FormEvent } from 'react'
|
|
4
|
-
import { Eye, EyeOff, Lock, Mail, type LucideIcon } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
import { Button } from '../../../ui/button'
|
|
7
|
-
import { Card } from '../../../ui/card'
|
|
8
|
-
import { CheckboxCheck } from '../../e-commerce/checkout-forms/checkbox-check'
|
|
9
|
-
import { Input } from '../../../ui/input'
|
|
10
|
-
import { cn } from '../../../lib/utils'
|
|
11
|
-
|
|
12
|
-
export interface LoginPage1SocialButton {
|
|
13
|
-
name: string
|
|
14
|
-
icon: LucideIcon
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface LoginPage1FormData {
|
|
18
|
-
email: string
|
|
19
|
-
password: string
|
|
20
|
-
remember: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface LoginPage1Props {
|
|
24
|
-
logoText?: string
|
|
25
|
-
welcomeTitleLabel?: string
|
|
26
|
-
welcomeDescriptionLabel?: string
|
|
27
|
-
emailLabel?: string
|
|
28
|
-
emailPlaceholder?: string
|
|
29
|
-
passwordLabel?: string
|
|
30
|
-
passwordPlaceholder?: string
|
|
31
|
-
rememberMeLabel?: string
|
|
32
|
-
forgotPasswordLabel?: string
|
|
33
|
-
signInLabel?: string
|
|
34
|
-
orContinueWithLabel?: string
|
|
35
|
-
dontHaveAccountLabel?: string
|
|
36
|
-
createAccountLabel?: string
|
|
37
|
-
/** Template with a `{provider}` placeholder for the social login buttons' aria-labels. */
|
|
38
|
-
socialAriaLabelTemplate?: string
|
|
39
|
-
showPasswordAriaLabel?: string
|
|
40
|
-
hidePasswordAriaLabel?: string
|
|
41
|
-
socialButtons?: LoginPage1SocialButton[]
|
|
42
|
-
onSubmit?: (data: LoginPage1FormData) => void
|
|
43
|
-
onForgotPassword?: () => void
|
|
44
|
-
onCreateAccount?: () => void
|
|
45
|
-
onSocialLogin?: (providerName: string) => void
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function LoginPage1({
|
|
49
|
-
logoText,
|
|
50
|
-
welcomeTitleLabel = 'Bienvenido de nuevo',
|
|
51
|
-
welcomeDescriptionLabel = 'Inicia sesión para continuar',
|
|
52
|
-
emailLabel = 'Correo electrónico',
|
|
53
|
-
emailPlaceholder = 'taller@correo.com',
|
|
54
|
-
passwordLabel = 'Contraseña',
|
|
55
|
-
passwordPlaceholder = '••••••••',
|
|
56
|
-
rememberMeLabel = 'Recuérdame',
|
|
57
|
-
forgotPasswordLabel = '¿Olvidaste tu contraseña?',
|
|
58
|
-
signInLabel = 'Iniciar sesión',
|
|
59
|
-
orContinueWithLabel = 'O continúa con',
|
|
60
|
-
dontHaveAccountLabel = '¿No tienes una cuenta?',
|
|
61
|
-
createAccountLabel = 'Crea una cuenta',
|
|
62
|
-
socialAriaLabelTemplate = 'Iniciar sesión con {provider}',
|
|
63
|
-
showPasswordAriaLabel = 'Mostrar contraseña',
|
|
64
|
-
hidePasswordAriaLabel = 'Ocultar contraseña',
|
|
65
|
-
socialButtons = [],
|
|
66
|
-
onSubmit,
|
|
67
|
-
onForgotPassword,
|
|
68
|
-
onCreateAccount,
|
|
69
|
-
onSocialLogin,
|
|
70
|
-
}: LoginPage1Props) {
|
|
71
|
-
const [showPassword, setShowPassword] = useState(false)
|
|
72
|
-
const [email, setEmail] = useState('')
|
|
73
|
-
const [password, setPassword] = useState('')
|
|
74
|
-
const [remember, setRemember] = useState(false)
|
|
75
|
-
|
|
76
|
-
const handleSubmit = (event: FormEvent) => {
|
|
77
|
-
event.preventDefault()
|
|
78
|
-
onSubmit?.({ email, password, remember })
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const forgotPasswordClassName = cn(
|
|
82
|
-
'ms-auto inline-block text-sm underline-offset-4',
|
|
83
|
-
onForgotPassword ? 'cursor-pointer hover:underline' : 'text-muted-foreground'
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
const createAccountClassName = cn('underline underline-offset-4', onCreateAccount ? 'cursor-pointer' : undefined)
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<section className='from-background to-muted/50 relative isolate flex min-h-dvh w-full items-center justify-center overflow-hidden bg-linear-to-br'>
|
|
90
|
-
<div className='relative z-10 container mx-auto flex min-h-dvh items-center justify-center px-4 py-12'>
|
|
91
|
-
<Card className='relative w-full max-w-md ring-0 p-8 shadow-2xl'>
|
|
92
|
-
<div className='mb-8 flex flex-col items-center'>
|
|
93
|
-
<div className='my-4 flex justify-center'>
|
|
94
|
-
<div className='bg-secondary relative size-14 rounded-full border'>
|
|
95
|
-
<div className='flex h-full items-center justify-center'>
|
|
96
|
-
<svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg' aria-hidden='true'>
|
|
97
|
-
<path
|
|
98
|
-
d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
|
|
99
|
-
fill='currentColor'
|
|
100
|
-
/>
|
|
101
|
-
<path
|
|
102
|
-
d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
|
|
103
|
-
fill='currentColor'
|
|
104
|
-
/>
|
|
105
|
-
<path
|
|
106
|
-
d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
|
|
107
|
-
fill='currentColor'
|
|
108
|
-
/>
|
|
109
|
-
<path
|
|
110
|
-
d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
|
|
111
|
-
fill='currentColor'
|
|
112
|
-
/>
|
|
113
|
-
<path
|
|
114
|
-
d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
|
|
115
|
-
fill='currentColor'
|
|
116
|
-
/>
|
|
117
|
-
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
|
|
118
|
-
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
|
|
119
|
-
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
120
|
-
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
121
|
-
</svg>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
{logoText && <p className='text-muted-foreground mb-2 text-sm font-semibold'>{logoText}</p>}
|
|
126
|
-
<h1 className='mb-2 text-center text-2xl font-bold tracking-tight'>{welcomeTitleLabel}</h1>
|
|
127
|
-
<p className='text-muted-foreground text-center text-sm'>{welcomeDescriptionLabel}</p>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<form onSubmit={handleSubmit} className='flex flex-col gap-6'>
|
|
131
|
-
<div className='relative'>
|
|
132
|
-
<Input
|
|
133
|
-
type='email'
|
|
134
|
-
placeholder={emailPlaceholder}
|
|
135
|
-
aria-label={emailLabel}
|
|
136
|
-
autoComplete='email'
|
|
137
|
-
value={email}
|
|
138
|
-
onChange={event => setEmail(event.target.value)}
|
|
139
|
-
className='bg-transparent ps-10 h-9 text-sm'
|
|
140
|
-
/>
|
|
141
|
-
<Mail className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' aria-hidden='true' />
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<div className='relative'>
|
|
145
|
-
<Input
|
|
146
|
-
type={showPassword ? 'text' : 'password'}
|
|
147
|
-
placeholder={passwordPlaceholder}
|
|
148
|
-
aria-label={passwordLabel}
|
|
149
|
-
autoComplete='current-password'
|
|
150
|
-
value={password}
|
|
151
|
-
onChange={event => setPassword(event.target.value)}
|
|
152
|
-
className='bg-transparent ps-10 h-9 text-sm'
|
|
153
|
-
/>
|
|
154
|
-
<Lock className='text-muted-foreground absolute start-3 top-1/2 size-5 -translate-y-1/2' aria-hidden='true' />
|
|
155
|
-
<Button
|
|
156
|
-
type='button'
|
|
157
|
-
variant='ghost'
|
|
158
|
-
aria-label={showPassword ? hidePasswordAriaLabel : showPasswordAriaLabel}
|
|
159
|
-
className='absolute end-0 top-0 h-full cursor-pointer px-3 py-2 hover:bg-transparent'
|
|
160
|
-
onClick={() => setShowPassword(prev => !prev)}
|
|
161
|
-
>
|
|
162
|
-
{showPassword ? (
|
|
163
|
-
<EyeOff className='text-muted-foreground' />
|
|
164
|
-
) : (
|
|
165
|
-
<Eye className='text-muted-foreground' />
|
|
166
|
-
)}
|
|
167
|
-
</Button>
|
|
168
|
-
</div>
|
|
169
|
-
|
|
170
|
-
<div className='flex items-center justify-between'>
|
|
171
|
-
<div className='flex items-center gap-2'>
|
|
172
|
-
<CheckboxCheck
|
|
173
|
-
id='remember'
|
|
174
|
-
checked={remember}
|
|
175
|
-
onCheckedChange={value => setRemember(value === true)}
|
|
176
|
-
className='data-checked:bg-primary bg-transparent'
|
|
177
|
-
/>
|
|
178
|
-
<label
|
|
179
|
-
htmlFor='remember'
|
|
180
|
-
className='text-sm leading-none font-normal text-muted-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
|
181
|
-
>
|
|
182
|
-
{rememberMeLabel}
|
|
183
|
-
</label>
|
|
184
|
-
</div>
|
|
185
|
-
{onForgotPassword ? (
|
|
186
|
-
<button type='button' onClick={onForgotPassword} className={forgotPasswordClassName}>
|
|
187
|
-
{forgotPasswordLabel}
|
|
188
|
-
</button>
|
|
189
|
-
) : (
|
|
190
|
-
<span className={forgotPasswordClassName}>{forgotPasswordLabel}</span>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
|
|
194
|
-
<Button type='submit' className='h-9 w-full cursor-pointer px-4 py-2'>
|
|
195
|
-
{signInLabel}
|
|
196
|
-
</Button>
|
|
197
|
-
|
|
198
|
-
{socialButtons.length > 0 && (
|
|
199
|
-
<>
|
|
200
|
-
<div className='relative my-6 text-center'>
|
|
201
|
-
<div className='absolute inset-0 flex items-center'>
|
|
202
|
-
<div className='w-full border-t' />
|
|
203
|
-
</div>
|
|
204
|
-
<div className='relative flex justify-center text-xs uppercase'>
|
|
205
|
-
<span className='bg-card text-muted-foreground px-2'>{orContinueWithLabel}</span>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
<div className='grid grid-cols-3 gap-3'>
|
|
210
|
-
{socialButtons.map(button => {
|
|
211
|
-
const Icon = button.icon
|
|
212
|
-
return (
|
|
213
|
-
<Button
|
|
214
|
-
key={button.name}
|
|
215
|
-
type='button'
|
|
216
|
-
variant='outline'
|
|
217
|
-
aria-label={socialAriaLabelTemplate.replace('{provider}', button.name)}
|
|
218
|
-
className='h-9 w-full cursor-pointer px-4 py-2'
|
|
219
|
-
onClick={onSocialLogin ? () => onSocialLogin(button.name) : undefined}
|
|
220
|
-
>
|
|
221
|
-
<Icon className='size-5' aria-hidden='true' />
|
|
222
|
-
</Button>
|
|
223
|
-
)
|
|
224
|
-
})}
|
|
225
|
-
</div>
|
|
226
|
-
</>
|
|
227
|
-
)}
|
|
228
|
-
</form>
|
|
229
|
-
|
|
230
|
-
<p className='mt-6 flex justify-center gap-1 text-center text-sm'>
|
|
231
|
-
<span>{dontHaveAccountLabel}</span>
|
|
232
|
-
{onCreateAccount ? (
|
|
233
|
-
<button type='button' onClick={onCreateAccount} className={createAccountClassName}>
|
|
234
|
-
{createAccountLabel}
|
|
235
|
-
</button>
|
|
236
|
-
) : (
|
|
237
|
-
<span className={createAccountClassName}>{createAccountLabel}</span>
|
|
238
|
-
)}
|
|
239
|
-
</p>
|
|
240
|
-
</Card>
|
|
241
|
-
</div>
|
|
242
|
-
</section>
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
export default LoginPage1
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, type FormEvent } from 'react'
|
|
4
|
-
import { Eye, EyeOff, type LucideIcon } from 'lucide-react'
|
|
5
|
-
|
|
6
|
-
import { Button } from '../../../ui/button'
|
|
7
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../../ui/card'
|
|
8
|
-
import { Input } from '../../../ui/input'
|
|
9
|
-
import { Label } from '../../../ui/label'
|
|
10
|
-
import { Separator } from '../../../ui/separator'
|
|
11
|
-
import { CheckboxCheck } from '../../e-commerce/checkout-forms/checkbox-check'
|
|
12
|
-
|
|
13
|
-
export interface LoginPage2SocialButton {
|
|
14
|
-
name: string
|
|
15
|
-
icon: LucideIcon
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface LoginPage2Props {
|
|
19
|
-
/** Brand text rendered next to the block logo. Omit to hide the logo. */
|
|
20
|
-
logoText?: string
|
|
21
|
-
welcomeTitleLabel?: string
|
|
22
|
-
welcomeDescriptionLabel?: string
|
|
23
|
-
emailLabel?: string
|
|
24
|
-
emailPlaceholder?: string
|
|
25
|
-
passwordLabel?: string
|
|
26
|
-
passwordPlaceholder?: string
|
|
27
|
-
rememberMeLabel?: string
|
|
28
|
-
forgotPasswordLabel?: string
|
|
29
|
-
signInLabel?: string
|
|
30
|
-
orContinueWithLabel?: string
|
|
31
|
-
/** Social login buttons rendered in a 2-column grid. Pass an empty array to hide. */
|
|
32
|
-
socialButtons?: LoginPage2SocialButton[]
|
|
33
|
-
dontHaveAccountLabel?: string
|
|
34
|
-
signUpLabel?: string
|
|
35
|
-
showPasswordLabel?: string
|
|
36
|
-
hidePasswordLabel?: string
|
|
37
|
-
onSubmit?: (email: string, password: string) => void
|
|
38
|
-
onForgotPassword?: () => void
|
|
39
|
-
onSignUp?: () => void
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function LoginPage2({
|
|
43
|
-
logoText,
|
|
44
|
-
welcomeTitleLabel = 'Bienvenido de nuevo',
|
|
45
|
-
welcomeDescriptionLabel = 'Ingresa tus credenciales para acceder a tu cuenta',
|
|
46
|
-
emailLabel = 'Correo electrónico',
|
|
47
|
-
emailPlaceholder = 'tucorreo@ejemplo.com',
|
|
48
|
-
passwordLabel = 'Contraseña',
|
|
49
|
-
passwordPlaceholder = 'Tu contraseña',
|
|
50
|
-
rememberMeLabel = 'Recuérdame',
|
|
51
|
-
forgotPasswordLabel = '¿Olvidaste tu contraseña?',
|
|
52
|
-
signInLabel = 'Iniciar sesión',
|
|
53
|
-
orContinueWithLabel = 'O continúa con',
|
|
54
|
-
socialButtons = [],
|
|
55
|
-
dontHaveAccountLabel = '¿No tienes una cuenta?',
|
|
56
|
-
signUpLabel = 'Regístrate aquí',
|
|
57
|
-
showPasswordLabel = 'Mostrar contraseña',
|
|
58
|
-
hidePasswordLabel = 'Ocultar contraseña',
|
|
59
|
-
onSubmit,
|
|
60
|
-
onForgotPassword,
|
|
61
|
-
onSignUp,
|
|
62
|
-
}: LoginPage2Props) {
|
|
63
|
-
const [showPassword, setShowPassword] = useState(false)
|
|
64
|
-
const [email, setEmail] = useState('')
|
|
65
|
-
const [password, setPassword] = useState('')
|
|
66
|
-
|
|
67
|
-
const handleSubmit = (event: FormEvent) => {
|
|
68
|
-
event.preventDefault()
|
|
69
|
-
onSubmit?.(email, password)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className='bg-background flex min-h-dvh items-center justify-center p-4 py-12'>
|
|
74
|
-
<div className='flex w-full max-w-md flex-col gap-6'>
|
|
75
|
-
{logoText && (
|
|
76
|
-
<div className='mb-3 flex items-center gap-2 self-center font-medium'>
|
|
77
|
-
{/* Logo mark uses the bg-muted token so it inherits the active theme. */}
|
|
78
|
-
<div className='bg-muted flex size-10 items-center justify-center rounded-lg'>
|
|
79
|
-
<svg width='32' height='32' className='text-primary' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
|
80
|
-
<path
|
|
81
|
-
d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
|
|
82
|
-
fill='currentColor'
|
|
83
|
-
/>
|
|
84
|
-
<path
|
|
85
|
-
d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
|
|
86
|
-
fill='currentColor'
|
|
87
|
-
/>
|
|
88
|
-
<path
|
|
89
|
-
d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
|
|
90
|
-
fill='currentColor'
|
|
91
|
-
/>
|
|
92
|
-
<path
|
|
93
|
-
d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
|
|
94
|
-
fill='currentColor'
|
|
95
|
-
/>
|
|
96
|
-
<path
|
|
97
|
-
d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
|
|
98
|
-
fill='currentColor'
|
|
99
|
-
/>
|
|
100
|
-
<path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
|
|
101
|
-
<path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
|
|
102
|
-
<circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
103
|
-
<circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
|
|
104
|
-
</svg>
|
|
105
|
-
</div>
|
|
106
|
-
<div className='text-2xl font-medium'>{logoText}</div>
|
|
107
|
-
</div>
|
|
108
|
-
)}
|
|
109
|
-
<Card className='w-full max-w-md'>
|
|
110
|
-
<CardHeader className='text-center'>
|
|
111
|
-
<CardTitle className='text-2xl font-bold text-balance'>{welcomeTitleLabel}</CardTitle>
|
|
112
|
-
<CardDescription>{welcomeDescriptionLabel}</CardDescription>
|
|
113
|
-
</CardHeader>
|
|
114
|
-
<CardContent className='flex flex-col gap-5'>
|
|
115
|
-
<form className='flex flex-col gap-5' onSubmit={handleSubmit}>
|
|
116
|
-
<div className='flex flex-col gap-3'>
|
|
117
|
-
<Label htmlFor='email'>{emailLabel}</Label>
|
|
118
|
-
<Input
|
|
119
|
-
id='email'
|
|
120
|
-
type='email'
|
|
121
|
-
placeholder={emailPlaceholder}
|
|
122
|
-
value={email}
|
|
123
|
-
onChange={event => setEmail(event.target.value)}
|
|
124
|
-
className='h-9 text-sm'
|
|
125
|
-
/>
|
|
126
|
-
</div>
|
|
127
|
-
<div className='flex flex-col gap-3'>
|
|
128
|
-
<Label htmlFor='password'>{passwordLabel}</Label>
|
|
129
|
-
<div className='relative'>
|
|
130
|
-
<Input
|
|
131
|
-
id='password'
|
|
132
|
-
type={showPassword ? 'text' : 'password'}
|
|
133
|
-
placeholder={passwordPlaceholder}
|
|
134
|
-
value={password}
|
|
135
|
-
onChange={event => setPassword(event.target.value)}
|
|
136
|
-
className='h-9 pe-10 text-sm'
|
|
137
|
-
/>
|
|
138
|
-
<Button
|
|
139
|
-
type='button'
|
|
140
|
-
variant='ghost'
|
|
141
|
-
size='icon'
|
|
142
|
-
className='absolute end-0 top-1/2 size-9 -translate-y-1/2 cursor-pointer hover:bg-transparent'
|
|
143
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
144
|
-
>
|
|
145
|
-
{showPassword ? (
|
|
146
|
-
<EyeOff className='text-muted-foreground' />
|
|
147
|
-
) : (
|
|
148
|
-
<Eye className='text-muted-foreground' />
|
|
149
|
-
)}
|
|
150
|
-
<span className='sr-only'>{showPassword ? hidePasswordLabel : showPasswordLabel}</span>
|
|
151
|
-
</Button>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div className='flex items-center justify-between'>
|
|
155
|
-
<div className='flex items-center gap-2'>
|
|
156
|
-
<CheckboxCheck id='remember-me' />
|
|
157
|
-
<Label
|
|
158
|
-
htmlFor='remember-me'
|
|
159
|
-
className='text-sm leading-none font-normal text-muted-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
|
160
|
-
>
|
|
161
|
-
{rememberMeLabel}
|
|
162
|
-
</Label>
|
|
163
|
-
</div>
|
|
164
|
-
<Button variant='link' className='cursor-pointer p-0 text-sm' onClick={onForgotPassword}>
|
|
165
|
-
{forgotPasswordLabel}
|
|
166
|
-
</Button>
|
|
167
|
-
</div>
|
|
168
|
-
<Button type='submit' className='h-9 w-full cursor-pointer px-4 py-2'>
|
|
169
|
-
{signInLabel}
|
|
170
|
-
</Button>
|
|
171
|
-
</form>
|
|
172
|
-
|
|
173
|
-
{socialButtons.length > 0 && (
|
|
174
|
-
<>
|
|
175
|
-
{/* Flex divider rather than a rule masked by an opaque label: the mask has to
|
|
176
|
-
match whatever surface it sits on, and on a card it did not. */}
|
|
177
|
-
<div className='flex items-center gap-3'>
|
|
178
|
-
<Separator className='flex-1' />
|
|
179
|
-
<span className='text-muted-foreground text-xs uppercase'>{orContinueWithLabel}</span>
|
|
180
|
-
<Separator className='flex-1' />
|
|
181
|
-
</div>
|
|
182
|
-
|
|
183
|
-
<div className='grid grid-cols-2 gap-4'>
|
|
184
|
-
{socialButtons.map(button => (
|
|
185
|
-
<Button key={button.name} variant='outline' className='h-9 w-full cursor-pointer px-4 py-2'>
|
|
186
|
-
<button.icon />
|
|
187
|
-
{button.name}
|
|
188
|
-
</Button>
|
|
189
|
-
))}
|
|
190
|
-
</div>
|
|
191
|
-
</>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
<div className='text-muted-foreground text-center text-sm'>
|
|
195
|
-
{dontHaveAccountLabel}{' '}
|
|
196
|
-
<Button variant='link' className='cursor-pointer p-0 text-sm' onClick={onSignUp}>
|
|
197
|
-
{signUpLabel}
|
|
198
|
-
</Button>
|
|
199
|
-
</div>
|
|
200
|
-
</CardContent>
|
|
201
|
-
</Card>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export default LoginPage2
|