@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -1,192 +0,0 @@
1
- import { Input } from '../../../ui/input'
2
- import { Button } from '../../../ui/button'
3
- import { ArrowRight, type LucideIcon } from 'lucide-react'
4
-
5
- export interface FooterLink {
6
- label: string
7
- href?: string
8
- }
9
-
10
- export interface FooterLinkColumn {
11
- title: string
12
- links: FooterLink[]
13
- }
14
-
15
- export interface SocialLink {
16
- label: string
17
- href?: string
18
- icon: LucideIcon
19
- }
20
-
21
- export interface FooterSection3Props {
22
- /** Texto del logo (identidad del bloque). Requerido. */
23
- logoText: string
24
- /** Descripción corta de la marca. Requerida. */
25
- description: string
26
- /** Columnas de enlaces (máximo recomendado 4). Requerido. */
27
- linkColumns: FooterLinkColumn[]
28
- /** Enlaces legales de la barra inferior. Por defecto []. */
29
- legalLinks?: FooterLink[]
30
- /** Redes sociales (cada una con su icono lucide). Por defecto []. */
31
- socialLinks?: SocialLink[]
32
- /** Título del bloque newsletter. Por defecto 'Suscríbete a nuestro boletín'. */
33
- newsletterTitle?: string
34
- /** Placeholder del input de email. Por defecto 'Tu correo electrónico'. */
35
- newsletterPlaceholder?: string
36
- /** aria-label del botón de envío (icon-only). Por defecto 'Suscribirse'. */
37
- newsletterLabel?: string
38
- /** Callback al enviar el formulario newsletter. Recibe el email. */
39
- onSubscribe?: (email: string) => void
40
- }
41
-
42
- export function FooterSection3({
43
- logoText,
44
- description,
45
- linkColumns,
46
- legalLinks = [],
47
- socialLinks = [],
48
- newsletterTitle = 'Suscríbete a nuestro boletín',
49
- newsletterPlaceholder = 'Tu correo electrónico',
50
- newsletterLabel = 'Suscribirse',
51
- onSubscribe,
52
- }: FooterSection3Props) {
53
- return (
54
- <footer className='w-full py-16' aria-label='Pie de página'>
55
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
56
- {/* Logo y branding */}
57
- <div className='mb-8 flex items-center gap-2'>
58
- <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
59
- <path
60
- 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'
61
- fill='currentColor'
62
- />
63
- <path
64
- 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'
65
- fill='currentColor'
66
- />
67
- <path
68
- 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'
69
- fill='currentColor'
70
- />
71
- <path
72
- 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'
73
- fill='currentColor'
74
- />
75
- <path
76
- 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'
77
- fill='currentColor'
78
- />
79
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
80
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
81
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
82
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
83
- </svg>
84
- <span className='text-2xl font-semibold'>{logoText}</span>
85
- </div>
86
-
87
- <p className='text-muted-foreground mb-8 max-w-lg text-sm'>{description}</p>
88
-
89
- {/* Grid principal */}
90
- <div className='grid grid-cols-2 gap-x-8 gap-y-12 md:grid-cols-4 lg:grid-cols-6'>
91
- {/* Columna newsletter */}
92
- <div className='col-span-2 me-8 flex flex-col gap-4 md:col-span-4 lg:col-span-2'>
93
- <h4 className='text-foreground text-base font-medium'>{newsletterTitle}</h4>
94
- <form
95
- className='relative'
96
- onSubmit={e => {
97
- e.preventDefault()
98
- if (onSubscribe) {
99
- const email = new FormData(e.currentTarget).get('email')
100
- if (typeof email === 'string') {
101
- onSubscribe(email)
102
- }
103
- }
104
- }}
105
- >
106
- <Input
107
- type='email'
108
- name='email'
109
- placeholder={newsletterPlaceholder}
110
- className='border-muted-foreground/20 h-10 bg-transparent pe-12'
111
- aria-label={newsletterPlaceholder}
112
- />
113
- <Button
114
- type='submit'
115
- variant='outline'
116
- size='icon'
117
- className='absolute end-1 top-1 size-8 cursor-pointer p-0'
118
- aria-label={newsletterLabel}
119
- >
120
- <ArrowRight className='size-4' />
121
- </Button>
122
- </form>
123
- </div>
124
-
125
- {/* Columnas de enlaces */}
126
- {linkColumns.map(column => (
127
- <div key={column.title} className='flex flex-col gap-4'>
128
- <h4 className='text-foreground text-base font-medium'>{column.title}</h4>
129
- <div className='flex flex-col gap-4'>
130
- {column.links.map(link => (
131
- <span
132
- key={link.label}
133
- className='text-muted-foreground hover:text-foreground w-fit text-sm transition-colors'
134
- >
135
- <span className='group relative inline-flex items-center'>
136
- {link.label}
137
- <span className='bg-primary absolute start-0 -bottom-0.5 h-px w-0 transition-all duration-300 ease-out group-hover:w-full' />
138
- </span>
139
- </span>
140
- ))}
141
- </div>
142
- </div>
143
- ))}
144
- </div>
145
-
146
- {/* Barra inferior */}
147
- <div className='border-muted-foreground/20 mt-16 flex flex-col items-center justify-between gap-6 border-t pt-8 md:flex-row'>
148
- {/* Enlaces legales */}
149
- <div className='flex gap-4'>
150
- {legalLinks.map(link => (
151
- <span
152
- key={link.label}
153
- className='text-muted-foreground hover:text-foreground w-fit text-sm transition-colors'
154
- >
155
- <span className='group relative inline-flex items-center'>
156
- {link.label}
157
- <span className='bg-primary absolute start-0 -bottom-0.5 h-px w-0 transition-all duration-300 ease-out group-hover:w-full' />
158
- </span>
159
- </span>
160
- ))}
161
- </div>
162
-
163
- {/* Iconos de redes sociales */}
164
- <div className='flex gap-4'>
165
- {socialLinks.map(({ label, href, icon: Icon }) =>
166
- href ? (
167
- <a
168
- key={label}
169
- href={href}
170
- className='bg-muted/50 text-muted-foreground hover:text-primary rounded-full p-2 transition-colors'
171
- aria-label={label}
172
- >
173
- <Icon className='size-4' />
174
- </a>
175
- ) : (
176
- <span
177
- key={label}
178
- className='bg-muted/50 text-muted-foreground rounded-full p-2'
179
- aria-hidden='true'
180
- >
181
- <Icon className='size-4' />
182
- </span>
183
- )
184
- )}
185
- </div>
186
- </div>
187
- </div>
188
- </footer>
189
- )
190
- }
191
-
192
- export default FooterSection3
@@ -1,355 +0,0 @@
1
- import type { ReactElement, ReactNode, SVGProps } from 'react'
2
- import { Input } from '../../../ui/input'
3
- import { Button } from '../../../ui/button'
4
- import { ArrowRight, Mail, MapPin, Phone } from 'lucide-react'
5
-
6
- export type FooterSection4SocialIcon = 'facebook' | 'twitter' | 'instagram'
7
-
8
- export interface FooterSection4Link {
9
- label: string
10
- href?: string
11
- }
12
-
13
- export interface FooterSection4LinkColumn {
14
- title: string
15
- links: FooterSection4Link[]
16
- }
17
-
18
- export interface FooterSection4Contact {
19
- /** Título de la columna de contacto. Requerido. */
20
- heading: string
21
- /** Dirección del negocio. Por defecto undefined. */
22
- address?: string
23
- /** href del enlace de dirección. Por defecto undefined. */
24
- addressHref?: string
25
- /** Teléfono de contacto. Por defecto undefined. */
26
- phone?: string
27
- /** href del enlace de teléfono (p. ej. tel:). Por defecto undefined. */
28
- phoneHref?: string
29
- /** Email de contacto. Por defecto undefined. */
30
- email?: string
31
- /** href del enlace de email (p. ej. mailto:). Por defecto undefined. */
32
- emailHref?: string
33
- }
34
-
35
- export interface FooterSection4SocialLink {
36
- label: string
37
- href?: string
38
- icon: FooterSection4SocialIcon
39
- }
40
-
41
- export interface FooterSection4Props {
42
- /** Nombre de la marca mostrado junto al logo. Requerido. */
43
- brandName: string
44
- /** Descripción corta de la marca bajo el logo. Requerida. */
45
- tagline: string
46
- /** Placeholder del input de email del newsletter. Por defecto 'Tu correo electrónico'. */
47
- newsletterPlaceholder?: string
48
- /** Texto del botón de suscripción. Por defecto 'Suscribirse'. */
49
- newsletterSubmitLabel?: string
50
- /** Datos de la columna de contacto (dirección, teléfono, email). Requerido. */
51
- contact: FooterSection4Contact
52
- /** Columnas de enlaces (Quick Links, Resources, Legal...). Requerido. */
53
- linkColumns: FooterSection4LinkColumn[]
54
- /** Texto de copyright de la barra inferior (incluye año). Requerido. */
55
- copyright: string
56
- /** Redes sociales con su icono serializable por nombre. Por defecto []. */
57
- socialLinks?: FooterSection4SocialLink[]
58
- /** Logo personalizado; si se omite se usa el SVG interno del bloque. Por defecto undefined. */
59
- logo?: ReactNode
60
- /** Callback al enviar el formulario newsletter. Recibe el email. */
61
- onNewsletterSubmit?: (email: string) => void
62
- /** Callback al hacer clic en un enlace de columna o contacto. Recibe el enlace. */
63
- onLinkClick?: (link: FooterSection4Link) => void
64
- /** Callback al hacer clic en una red social. Recibe el label. */
65
- onSocialClick?: (label: string) => void
66
- }
67
-
68
- const socialIconPaths: Record<
69
- FooterSection4SocialIcon,
70
- (props: SVGProps<SVGSVGElement>) => ReactElement
71
- > = {
72
- facebook: props => (
73
- <svg
74
- xmlns='http://www.w3.org/2000/svg'
75
- width='24'
76
- height='24'
77
- viewBox='0 0 24 24'
78
- fill='none'
79
- stroke='currentColor'
80
- strokeWidth='2'
81
- strokeLinecap='round'
82
- strokeLinejoin='round'
83
- {...props}
84
- >
85
- <path d='M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z' />
86
- </svg>
87
- ),
88
- twitter: props => (
89
- <svg
90
- xmlns='http://www.w3.org/2000/svg'
91
- width='24'
92
- height='24'
93
- viewBox='0 0 24 24'
94
- fill='none'
95
- stroke='currentColor'
96
- strokeWidth='2'
97
- strokeLinecap='round'
98
- strokeLinejoin='round'
99
- {...props}
100
- >
101
- <path d='M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z' />
102
- </svg>
103
- ),
104
- instagram: props => (
105
- <svg
106
- xmlns='http://www.w3.org/2000/svg'
107
- width='24'
108
- height='24'
109
- viewBox='0 0 24 24'
110
- fill='none'
111
- stroke='currentColor'
112
- strokeWidth='2'
113
- strokeLinecap='round'
114
- strokeLinejoin='round'
115
- {...props}
116
- >
117
- <rect width='20' height='20' x='2' y='2' rx='5' ry='5' />
118
- <path d='M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z' />
119
- <line x1='17.5' y1='6.5' x2='17.51' y2='6.5' />
120
- </svg>
121
- ),
122
- }
123
-
124
- const DefaultLogo = (
125
- <svg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'>
126
- <path
127
- 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'
128
- fill='currentColor'
129
- />
130
- <path
131
- 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'
132
- fill='currentColor'
133
- />
134
- <path
135
- 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'
136
- fill='currentColor'
137
- />
138
- <path
139
- 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'
140
- fill='currentColor'
141
- />
142
- <path
143
- 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'
144
- fill='currentColor'
145
- />
146
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' />
147
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' />
148
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' />
149
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' />
150
- </svg>
151
- )
152
-
153
- const contactItems = (contact: FooterSection4Contact, onLinkClick?: FooterSection4Props['onLinkClick']) => {
154
- const items: { key: string; icon: ReactElement; href?: string; text?: string }[] = []
155
- if (contact.address) {
156
- items.push({
157
- key: 'address',
158
- icon: <MapPin className='mt-0.5 size-4 shrink-0' aria-hidden />,
159
- href: contact.addressHref,
160
- text: contact.address,
161
- })
162
- }
163
- if (contact.phone) {
164
- items.push({
165
- key: 'phone',
166
- icon: <Phone className='size-4 shrink-0' aria-hidden />,
167
- href: contact.phoneHref,
168
- text: contact.phone,
169
- })
170
- }
171
- if (contact.email) {
172
- items.push({
173
- key: 'email',
174
- icon: <Mail className='size-4 shrink-0' aria-hidden />,
175
- href: contact.emailHref,
176
- text: contact.email,
177
- })
178
- }
179
- return items
180
- }
181
-
182
- export function FooterSection4({
183
- brandName,
184
- tagline,
185
- newsletterPlaceholder = 'Tu correo electrónico',
186
- newsletterSubmitLabel = 'Suscribirse',
187
- contact,
188
- linkColumns,
189
- copyright,
190
- socialLinks = [],
191
- logo,
192
- onNewsletterSubmit,
193
- onLinkClick,
194
- onSocialClick,
195
- }: FooterSection4Props) {
196
- const logoMark = logo ?? DefaultLogo
197
- return (
198
- <footer className='from-background to-muted/50 w-full bg-linear-to-b py-16'>
199
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
200
- {/* Top Section with Logo and Newsletter */}
201
- <section className='mb-16 flex flex-col items-start justify-between gap-8 md:flex-row md:items-center'>
202
- <div className='max-w-sm flex flex-col gap-4'>
203
- <div className='flex items-center gap-2'>
204
- {logoMark}
205
- <h2 className='text-2xl font-bold text-balance'>{brandName}</h2>
206
- </div>
207
- <p className='text-muted-foreground text-sm leading-relaxed text-balance'>{tagline}</p>
208
- </div>
209
-
210
- <div className='w-full md:w-auto'>
211
- <form
212
- className='relative'
213
- onSubmit={e => {
214
- e.preventDefault()
215
- if (onNewsletterSubmit) {
216
- const email = new FormData(e.currentTarget).get('email')
217
- if (typeof email === 'string') {
218
- onNewsletterSubmit(email)
219
- }
220
- }
221
- }}
222
- >
223
- <Input
224
- type='email'
225
- name='email'
226
- placeholder={newsletterPlaceholder}
227
- className='border-muted bg-background/50 h-12 w-full ps-4 pe-32 backdrop-blur-xs md:w-[400px]'
228
- aria-label={newsletterPlaceholder}
229
- />
230
- <Button
231
- type='submit'
232
- className='bg-primary hover:bg-primary/90 absolute end-1 top-1 h-10 cursor-pointer px-4'
233
- aria-label={newsletterSubmitLabel}
234
- >
235
- {newsletterSubmitLabel}
236
- <ArrowRight />
237
- </Button>
238
- </form>
239
- </div>
240
- </section>
241
-
242
- {/* Middle Section with Contact Info and Links */}
243
- <div className='mb-16 grid grid-cols-1 gap-12 md:grid-cols-2 lg:grid-cols-5'>
244
- {/* Contact Information */}
245
- <address className='not-italic lg:col-span-2'>
246
- <h3 className='mb-6 text-lg font-semibold text-balance'>{contact.heading}</h3>
247
- <ul className='flex flex-col gap-4'>
248
- {contactItems(contact, onLinkClick).map(item => (
249
- <li key={item.key} className='text-muted-foreground flex items-start gap-3 text-sm'>
250
- {item.icon}
251
- {item.href ? (
252
- <a
253
- href={item.href}
254
- onClick={e => {
255
- if (!onLinkClick) {
256
- e.preventDefault()
257
- } else {
258
- onLinkClick({ label: item.text ?? '' })
259
- }
260
- }}
261
- className='hover:text-foreground transition-colors'
262
- >
263
- {item.text}
264
- </a>
265
- ) : (
266
- <span>{item.text}</span>
267
- )}
268
- </li>
269
- ))}
270
- </ul>
271
- </address>
272
-
273
- {/* Link columns */}
274
- {linkColumns.map(column => (
275
- <nav key={column.title} aria-label={column.title}>
276
- <h3 className='mb-6 text-lg font-semibold text-balance'>{column.title}</h3>
277
- <ul className='grid grid-cols-1 gap-3'>
278
- {column.links.map(link => (
279
- <li key={link.label}>
280
- {link.href ? (
281
- <a
282
- href={link.href}
283
- onClick={e => {
284
- if (!onLinkClick) {
285
- e.preventDefault()
286
- } else {
287
- onLinkClick(link)
288
- }
289
- }}
290
- className='group text-muted-foreground hover:text-foreground focus-visible:ring-ring relative inline-block rounded-sm text-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden'
291
- >
292
- {link.label}
293
- </a>
294
- ) : (
295
- <span className='text-muted-foreground relative inline-block text-sm'>
296
- {link.label}
297
- </span>
298
- )}
299
- </li>
300
- ))}
301
- </ul>
302
- </nav>
303
- ))}
304
- </div>
305
-
306
- {/* Bottom Section */}
307
- <div className='border-muted-foreground/20 flex flex-col items-center justify-between border-t pt-8 md:flex-row'>
308
- <p className='text-muted-foreground mb-4 text-sm md:mb-0'>{copyright}</p>
309
-
310
- <nav aria-label='Social media'>
311
- <ul className='flex gap-4'>
312
- {socialLinks.map(({ label, href, icon }) => {
313
- const Icon = socialIconPaths[icon]
314
- const element = (
315
- <>
316
- <Icon className='size-5' aria-hidden />
317
- <span className='sr-only'>{label}</span>
318
- </>
319
- )
320
- const classes =
321
- 'bg-accent text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-10 items-center justify-center rounded-full p-2 transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden'
322
- return (
323
- <li key={label}>
324
- {href ? (
325
- <a
326
- href={href}
327
- onClick={e => {
328
- if (!onSocialClick) {
329
- e.preventDefault()
330
- } else {
331
- onSocialClick(label)
332
- }
333
- }}
334
- className={classes}
335
- aria-label={`Seguir en ${label}`}
336
- >
337
- {element}
338
- </a>
339
- ) : (
340
- <span className={classes} aria-hidden='true'>
341
- {element}
342
- </span>
343
- )}
344
- </li>
345
- )
346
- })}
347
- </ul>
348
- </nav>
349
- </div>
350
- </div>
351
- </footer>
352
- )
353
- }
354
-
355
- export default FooterSection4