@imansi/templates 0.1.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/LICENSE +21 -0
- package/README.md +557 -0
- package/package.json +35 -0
- package/src/auth/ForgotPasswordPage.jsx +176 -0
- package/src/auth/LoginPage.jsx +265 -0
- package/src/auth/RegisterPage.jsx +302 -0
- package/src/auth/ResetPasswordPage.jsx +158 -0
- package/src/auth/Verify2FAPage.jsx +175 -0
- package/src/dashboard/BillingPage.jsx +345 -0
- package/src/dashboard/DashboardHome.jsx +246 -0
- package/src/dashboard/NotificationsPage.jsx +230 -0
- package/src/dashboard/ProfilePage.jsx +272 -0
- package/src/dashboard/SettingsPage.jsx +415 -0
- package/src/dashboard/UsersTablePage.jsx +317 -0
- package/src/extra/CareersPage.jsx +166 -0
- package/src/extra/ChangelogPage.jsx +127 -0
- package/src/extra/CheckoutPage.jsx +383 -0
- package/src/extra/ComingSoonPage.jsx +147 -0
- package/src/extra/ErrorPage.jsx +91 -0
- package/src/extra/InvoicePage.jsx +241 -0
- package/src/extra/LegalPage.jsx +115 -0
- package/src/extra/MaintenancePage.jsx +172 -0
- package/src/extra/OnboardingPage.jsx +197 -0
- package/src/extra/StatusPage.jsx +181 -0
- package/src/index.js +53 -0
- package/src/layouts/AuthLayout.jsx +104 -0
- package/src/layouts/DashboardLayout.jsx +249 -0
- package/src/layouts/MarketingLayout.jsx +184 -0
- package/src/marketing/AboutPage.jsx +189 -0
- package/src/marketing/BlogPage.jsx +182 -0
- package/src/marketing/BlogPostPage.jsx +192 -0
- package/src/marketing/ContactPage.jsx +546 -0
- package/src/marketing/DocsPage.jsx +256 -0
- package/src/marketing/HomePage.jsx +702 -0
- package/src/marketing/LandingPage.jsx +755 -0
- package/src/marketing/PricingPage.jsx +205 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { Link } from 'react-router-dom';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
AvatarFallback,
|
|
5
|
+
Button,
|
|
6
|
+
cn,
|
|
7
|
+
} from '@imansi/ui';
|
|
8
|
+
import { MarketingLayout } from '../layouts/MarketingLayout.jsx';
|
|
9
|
+
|
|
10
|
+
const Icons = {
|
|
11
|
+
arrow: (
|
|
12
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
13
|
+
<path d="M5 12h14" />
|
|
14
|
+
<path d="M12 5l7 7-7 7" />
|
|
15
|
+
</svg>
|
|
16
|
+
),
|
|
17
|
+
mapPin: (
|
|
18
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
|
19
|
+
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
|
|
20
|
+
<circle cx="12" cy="10" r="3" />
|
|
21
|
+
</svg>
|
|
22
|
+
),
|
|
23
|
+
clock: (
|
|
24
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
|
25
|
+
<circle cx="12" cy="12" r="10" />
|
|
26
|
+
<polyline points="12 6 12 12 16 14" />
|
|
27
|
+
</svg>
|
|
28
|
+
),
|
|
29
|
+
phone: (
|
|
30
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
|
31
|
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
|
|
32
|
+
</svg>
|
|
33
|
+
),
|
|
34
|
+
mail: (
|
|
35
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
|
36
|
+
<rect x="2" y="4" width="20" height="16" rx="2" />
|
|
37
|
+
<path d="m22 6-10 7L2 6" />
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function HomePage({
|
|
43
|
+
brandName = 'El Trigal',
|
|
44
|
+
brandLogo,
|
|
45
|
+
navLinks = [],
|
|
46
|
+
headerCTA = { label: 'Reservar', href: '/reservar' },
|
|
47
|
+
headerSecondaryCTA = { label: 'Ver menú', href: '/menu' },
|
|
48
|
+
footerColumns = [],
|
|
49
|
+
|
|
50
|
+
/* Hero */
|
|
51
|
+
heroBadge = 'Panadería artesanal desde 1985',
|
|
52
|
+
heroTitle = 'El pan que tu mesa merece',
|
|
53
|
+
heroHighlight = 'tu mesa',
|
|
54
|
+
heroSubtitle = 'Horneamos cada mañana con masa madre, harinas orgánicas y tres generaciones de tradición.',
|
|
55
|
+
heroImage = '',
|
|
56
|
+
heroPrimaryCTA = { label: 'Ver menú', href: '/menu' },
|
|
57
|
+
heroSecondaryCTA = { label: 'Reservar mesa', href: '/reservar' },
|
|
58
|
+
heroCaption = 'Abierto de lunes a domingo · 7:00 a 21:00',
|
|
59
|
+
|
|
60
|
+
/* Logos / prensa */
|
|
61
|
+
pressLabel = 'Nuestro pan fue destacado en',
|
|
62
|
+
pressLogos = [],
|
|
63
|
+
|
|
64
|
+
/* Stats */
|
|
65
|
+
stats = [],
|
|
66
|
+
|
|
67
|
+
/* About */
|
|
68
|
+
aboutLabel = 'Nuestra historia',
|
|
69
|
+
aboutTitle = 'Tres generaciones horneando',
|
|
70
|
+
aboutText = [],
|
|
71
|
+
aboutImage = '',
|
|
72
|
+
aboutSign = '',
|
|
73
|
+
aboutCTA = { label: 'Conocer más', href: '/nosotros' },
|
|
74
|
+
|
|
75
|
+
/* Products */
|
|
76
|
+
productsLabel = 'Nuestro menú',
|
|
77
|
+
productsTitle = 'Frescos cada mañana',
|
|
78
|
+
productsSubtitle = 'Todo lo que sale del horno está hecho el mismo día con ingredientes locales.',
|
|
79
|
+
productsCTA = { label: 'Ver menú completo', href: '/menu' },
|
|
80
|
+
products = [],
|
|
81
|
+
|
|
82
|
+
/* Gallery */
|
|
83
|
+
galleryLabel = 'Galería',
|
|
84
|
+
galleryTitle = 'Un paseo por el local',
|
|
85
|
+
gallerySubtitle = 'Un espacio cálido para compartir.',
|
|
86
|
+
gallery = [],
|
|
87
|
+
|
|
88
|
+
/* Testimonials */
|
|
89
|
+
testimonialsLabel = 'Testimonios',
|
|
90
|
+
testimonialsTitle = 'Nuestros clientes opinan',
|
|
91
|
+
testimonialsSubtitle = 'Más de 4,200 reseñas con 4.9 estrellas.',
|
|
92
|
+
testimonials = [],
|
|
93
|
+
|
|
94
|
+
/* Visit */
|
|
95
|
+
visitLabel = 'Visitanos',
|
|
96
|
+
visitTitle = 'Te esperamos',
|
|
97
|
+
visitSubtitle = 'En el corazón del barrio, a metros del parque.',
|
|
98
|
+
visitAddress = '',
|
|
99
|
+
visitHours = [],
|
|
100
|
+
visitPhone = '',
|
|
101
|
+
visitEmail = '',
|
|
102
|
+
visitImage = '',
|
|
103
|
+
visitCTA = { label: 'Cómo llegar', href: '#' },
|
|
104
|
+
|
|
105
|
+
/* CTA final */
|
|
106
|
+
ctaTitle = 'Hacé tu pedido',
|
|
107
|
+
ctaSubtitle = 'Pedí online y pasá a retirar, o reservá una mesa para disfrutar en el local.',
|
|
108
|
+
ctaPrimary = { label: 'Pedir online', href: '#' },
|
|
109
|
+
ctaSecondary = { label: 'Llamar', href: 'tel:+54' },
|
|
110
|
+
}) {
|
|
111
|
+
function renderTitle(text, highlight) {
|
|
112
|
+
if (!highlight) return text;
|
|
113
|
+
const parts = text.split(highlight);
|
|
114
|
+
if (parts.length < 2) return text;
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
{parts[0]}
|
|
118
|
+
<span className="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
|
|
119
|
+
{highlight}
|
|
120
|
+
</span>
|
|
121
|
+
{parts[1]}
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<MarketingLayout
|
|
128
|
+
brandName={brandName}
|
|
129
|
+
brandLogo={brandLogo}
|
|
130
|
+
navLinks={navLinks}
|
|
131
|
+
headerCTA={headerCTA}
|
|
132
|
+
headerSecondaryCTA={headerSecondaryCTA}
|
|
133
|
+
footerColumns={footerColumns}
|
|
134
|
+
>
|
|
135
|
+
{/* ============================================================
|
|
136
|
+
HERO
|
|
137
|
+
============================================================ */}
|
|
138
|
+
<section className="relative overflow-hidden">
|
|
139
|
+
<div className="absolute inset-0 pointer-events-none" aria-hidden="true">
|
|
140
|
+
<div
|
|
141
|
+
className="absolute top-[-20%] left-1/2 -translate-x-1/2 w-[900px] sm:w-[1400px] h-[600px] sm:h-[900px]"
|
|
142
|
+
style={{
|
|
143
|
+
background: `radial-gradient(ellipse at center, var(--color-primary) 0%, transparent 55%)`,
|
|
144
|
+
opacity: 0.12,
|
|
145
|
+
filter: 'blur(100px)',
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div className="relative imansi-container pt-20 pb-16 sm:pt-28 sm:pb-20 lg:pt-32 lg:pb-24">
|
|
151
|
+
<div className="max-w-3xl mx-auto text-center">
|
|
152
|
+
{heroBadge && (
|
|
153
|
+
<div className="mb-6 sm:mb-8 flex justify-center">
|
|
154
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-card/60 backdrop-blur px-3 sm:px-3.5 py-1.5 text-[11px] sm:text-caption font-medium text-muted-foreground">
|
|
155
|
+
<span className="flex h-1.5 w-1.5 rounded-full bg-primary" />
|
|
156
|
+
{heroBadge}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
<h1 className="text-h1 sm:text-display font-bold tracking-[-0.035em] leading-[1.05] mb-5 sm:mb-6">
|
|
162
|
+
{renderTitle(heroTitle, heroHighlight)}
|
|
163
|
+
</h1>
|
|
164
|
+
|
|
165
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed max-w-2xl mx-auto mb-8 sm:mb-10">
|
|
166
|
+
{heroSubtitle}
|
|
167
|
+
</p>
|
|
168
|
+
|
|
169
|
+
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3 mb-4 sm:mb-5">
|
|
170
|
+
{heroPrimaryCTA && (
|
|
171
|
+
<Link to={heroPrimaryCTA.href} className="w-full sm:w-auto">
|
|
172
|
+
<Button size="lg" className="gap-2 w-full sm:w-auto">
|
|
173
|
+
{heroPrimaryCTA.label}
|
|
174
|
+
{Icons.arrow}
|
|
175
|
+
</Button>
|
|
176
|
+
</Link>
|
|
177
|
+
)}
|
|
178
|
+
{heroSecondaryCTA && (
|
|
179
|
+
<Link to={heroSecondaryCTA.href} className="w-full sm:w-auto">
|
|
180
|
+
<Button size="lg" variant="outline" className="w-full sm:w-auto">
|
|
181
|
+
{heroSecondaryCTA.label}
|
|
182
|
+
</Button>
|
|
183
|
+
</Link>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{heroCaption && (
|
|
188
|
+
<p className="text-[11px] sm:text-caption text-muted-foreground">
|
|
189
|
+
{heroCaption}
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Imagen del hero */}
|
|
195
|
+
{heroImage && (
|
|
196
|
+
<div className="relative mt-14 sm:mt-20 lg:mt-24 max-w-6xl mx-auto">
|
|
197
|
+
<div
|
|
198
|
+
className="absolute -inset-4 sm:-inset-8 pointer-events-none"
|
|
199
|
+
style={{
|
|
200
|
+
background: `radial-gradient(ellipse at center, var(--color-primary) 0%, transparent 60%)`,
|
|
201
|
+
opacity: 0.12,
|
|
202
|
+
filter: 'blur(60px)',
|
|
203
|
+
}}
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
/>
|
|
206
|
+
<div className="relative rounded-2xl overflow-hidden border border-border shadow-2xl shadow-black/20">
|
|
207
|
+
<img
|
|
208
|
+
src={heroImage}
|
|
209
|
+
alt={brandName}
|
|
210
|
+
className="w-full aspect-[16/10] sm:aspect-[21/9] object-cover"
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</section>
|
|
217
|
+
|
|
218
|
+
{/* ============================================================
|
|
219
|
+
PRENSA
|
|
220
|
+
============================================================ */}
|
|
221
|
+
{pressLogos.length > 0 && (
|
|
222
|
+
<section className="border-y border-border/60">
|
|
223
|
+
<div className="imansi-container py-10 sm:py-14">
|
|
224
|
+
<p className="text-center text-[10px] sm:text-[11px] uppercase tracking-[0.2em] text-muted-foreground/70 font-medium mb-6 sm:mb-10">
|
|
225
|
+
{pressLabel}
|
|
226
|
+
</p>
|
|
227
|
+
<div className="flex flex-wrap justify-center items-center gap-x-8 sm:gap-x-12 lg:gap-x-16 gap-y-4 sm:gap-y-6">
|
|
228
|
+
{pressLogos.map((logo) => (
|
|
229
|
+
<span
|
|
230
|
+
key={logo}
|
|
231
|
+
className="text-h4 font-bold tracking-tight text-muted-foreground/30 hover:text-muted-foreground/60 transition-colors"
|
|
232
|
+
>
|
|
233
|
+
{logo}
|
|
234
|
+
</span>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</section>
|
|
239
|
+
)}
|
|
240
|
+
|
|
241
|
+
{/* ============================================================
|
|
242
|
+
STATS
|
|
243
|
+
============================================================ */}
|
|
244
|
+
{stats.length > 0 && (
|
|
245
|
+
<section className="imansi-container py-14 sm:py-20">
|
|
246
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 sm:gap-10 max-w-5xl mx-auto">
|
|
247
|
+
{stats.map((stat, i) => (
|
|
248
|
+
<div key={i} className="text-center space-y-2">
|
|
249
|
+
<p className="text-h1 sm:text-display font-bold tracking-tight text-primary">
|
|
250
|
+
{stat.value}
|
|
251
|
+
</p>
|
|
252
|
+
<p className="text-small font-semibold">{stat.label}</p>
|
|
253
|
+
{stat.description && (
|
|
254
|
+
<p className="text-caption text-muted-foreground">
|
|
255
|
+
{stat.description}
|
|
256
|
+
</p>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
))}
|
|
260
|
+
</div>
|
|
261
|
+
</section>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{/* ============================================================
|
|
265
|
+
ABOUT — side by side en desktop, apilado en mobile
|
|
266
|
+
============================================================ */}
|
|
267
|
+
{aboutImage && (
|
|
268
|
+
<section className="border-y border-border/60 bg-muted/20">
|
|
269
|
+
<div className="imansi-container py-20 sm:py-24 lg:py-28">
|
|
270
|
+
<div className="grid gap-10 lg:gap-16 lg:grid-cols-2 items-center max-w-6xl mx-auto">
|
|
271
|
+
{/* Imagen */}
|
|
272
|
+
<div className="relative order-1 lg:order-1">
|
|
273
|
+
<div className="relative rounded-2xl overflow-hidden border border-border shadow-xl">
|
|
274
|
+
<img
|
|
275
|
+
src={aboutImage}
|
|
276
|
+
alt={aboutTitle}
|
|
277
|
+
className="w-full aspect-[4/3] object-cover"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Texto */}
|
|
283
|
+
<div className="order-2 lg:order-2 space-y-5 sm:space-y-6">
|
|
284
|
+
<div className="space-y-3 sm:space-y-4">
|
|
285
|
+
{aboutLabel && (
|
|
286
|
+
<p className="text-caption uppercase tracking-[0.2em] text-primary font-semibold">
|
|
287
|
+
{aboutLabel}
|
|
288
|
+
</p>
|
|
289
|
+
)}
|
|
290
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
291
|
+
{aboutTitle}
|
|
292
|
+
</h2>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{aboutText.length > 0 && (
|
|
296
|
+
<div className="space-y-4">
|
|
297
|
+
{aboutText.map((paragraph, i) => (
|
|
298
|
+
<p
|
|
299
|
+
key={i}
|
|
300
|
+
className="text-small sm:text-body text-muted-foreground leading-relaxed"
|
|
301
|
+
>
|
|
302
|
+
{paragraph}
|
|
303
|
+
</p>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{aboutSign && (
|
|
309
|
+
<p className="text-small italic text-muted-foreground border-l-2 border-primary/30 pl-4">
|
|
310
|
+
{aboutSign}
|
|
311
|
+
</p>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{aboutCTA && (
|
|
315
|
+
<div className="pt-2">
|
|
316
|
+
<Link to={aboutCTA.href}>
|
|
317
|
+
<Button variant="outline" className="gap-2">
|
|
318
|
+
{aboutCTA.label}
|
|
319
|
+
{Icons.arrow}
|
|
320
|
+
</Button>
|
|
321
|
+
</Link>
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</section>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* ============================================================
|
|
331
|
+
PRODUCTS / MENU
|
|
332
|
+
============================================================ */}
|
|
333
|
+
{products.length > 0 && (
|
|
334
|
+
<section className="imansi-container py-20 sm:py-24 lg:py-28">
|
|
335
|
+
<div className="max-w-2xl mx-auto text-center mb-14 sm:mb-16 lg:mb-20 space-y-3 sm:space-y-4">
|
|
336
|
+
{productsLabel && (
|
|
337
|
+
<p className="text-caption uppercase tracking-[0.2em] text-primary font-semibold">
|
|
338
|
+
{productsLabel}
|
|
339
|
+
</p>
|
|
340
|
+
)}
|
|
341
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
342
|
+
{productsTitle}
|
|
343
|
+
</h2>
|
|
344
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed">
|
|
345
|
+
{productsSubtitle}
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 max-w-6xl mx-auto">
|
|
350
|
+
{products.map((product, i) => (
|
|
351
|
+
<div
|
|
352
|
+
key={i}
|
|
353
|
+
className="group rounded-2xl border border-border bg-card overflow-hidden transition-all hover:shadow-xl hover:-translate-y-0.5"
|
|
354
|
+
>
|
|
355
|
+
{product.image && (
|
|
356
|
+
<div className="aspect-[4/3] overflow-hidden bg-muted">
|
|
357
|
+
<img
|
|
358
|
+
src={product.image}
|
|
359
|
+
alt={product.name}
|
|
360
|
+
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
<div className="p-5 sm:p-6 space-y-3">
|
|
365
|
+
<div className="flex items-start justify-between gap-3">
|
|
366
|
+
<h3 className="text-h4 font-semibold tracking-tight">
|
|
367
|
+
{product.name}
|
|
368
|
+
</h3>
|
|
369
|
+
{product.price && (
|
|
370
|
+
<span className="text-small font-semibold text-primary whitespace-nowrap">
|
|
371
|
+
{product.price}
|
|
372
|
+
</span>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
{product.description && (
|
|
376
|
+
<p className="text-small text-muted-foreground leading-relaxed">
|
|
377
|
+
{product.description}
|
|
378
|
+
</p>
|
|
379
|
+
)}
|
|
380
|
+
{product.tags?.length > 0 && (
|
|
381
|
+
<div className="flex flex-wrap gap-1.5 pt-1">
|
|
382
|
+
{product.tags.map((tag, j) => (
|
|
383
|
+
<span
|
|
384
|
+
key={j}
|
|
385
|
+
className="inline-flex items-center rounded-full border border-border bg-muted/50 px-2.5 py-0.5 text-[10px] font-medium text-muted-foreground"
|
|
386
|
+
>
|
|
387
|
+
{tag}
|
|
388
|
+
</span>
|
|
389
|
+
))}
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{productsCTA && (
|
|
398
|
+
<div className="text-center mt-12 sm:mt-14">
|
|
399
|
+
<Link to={productsCTA.href}>
|
|
400
|
+
<Button size="lg" variant="outline" className="gap-2">
|
|
401
|
+
{productsCTA.label}
|
|
402
|
+
{Icons.arrow}
|
|
403
|
+
</Button>
|
|
404
|
+
</Link>
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
</section>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{/* ============================================================
|
|
411
|
+
GALLERY
|
|
412
|
+
============================================================ */}
|
|
413
|
+
{gallery.length > 0 && (
|
|
414
|
+
<section className="border-y border-border/60 bg-muted/20">
|
|
415
|
+
<div className="imansi-container py-20 sm:py-24 lg:py-28">
|
|
416
|
+
<div className="max-w-2xl mx-auto text-center mb-14 sm:mb-16 space-y-3 sm:space-y-4">
|
|
417
|
+
{galleryLabel && (
|
|
418
|
+
<p className="text-caption uppercase tracking-[0.2em] text-primary font-semibold">
|
|
419
|
+
{galleryLabel}
|
|
420
|
+
</p>
|
|
421
|
+
)}
|
|
422
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
423
|
+
{galleryTitle}
|
|
424
|
+
</h2>
|
|
425
|
+
{gallerySubtitle && (
|
|
426
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed">
|
|
427
|
+
{gallerySubtitle}
|
|
428
|
+
</p>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4 max-w-5xl mx-auto">
|
|
433
|
+
{gallery.map((item, i) => (
|
|
434
|
+
<div
|
|
435
|
+
key={i}
|
|
436
|
+
className={cn(
|
|
437
|
+
'group rounded-xl overflow-hidden border border-border bg-muted transition-all hover:shadow-lg',
|
|
438
|
+
// La primera imagen ocupa 2x2 en desktop
|
|
439
|
+
i === 0 && 'col-span-2 md:row-span-2 aspect-square md:aspect-auto',
|
|
440
|
+
i > 0 && 'aspect-square'
|
|
441
|
+
)}
|
|
442
|
+
>
|
|
443
|
+
<img
|
|
444
|
+
src={item.image}
|
|
445
|
+
alt={item.alt || ''}
|
|
446
|
+
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
447
|
+
/>
|
|
448
|
+
</div>
|
|
449
|
+
))}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
</section>
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{/* ============================================================
|
|
456
|
+
TESTIMONIALS
|
|
457
|
+
============================================================ */}
|
|
458
|
+
{testimonials.length > 0 && (
|
|
459
|
+
<section className="imansi-container py-20 sm:py-24 lg:py-28">
|
|
460
|
+
<div className="max-w-2xl mx-auto text-center mb-14 sm:mb-16 lg:mb-20 space-y-3 sm:space-y-4">
|
|
461
|
+
{testimonialsLabel && (
|
|
462
|
+
<p className="text-caption uppercase tracking-[0.2em] text-primary font-semibold">
|
|
463
|
+
{testimonialsLabel}
|
|
464
|
+
</p>
|
|
465
|
+
)}
|
|
466
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
467
|
+
{testimonialsTitle}
|
|
468
|
+
</h2>
|
|
469
|
+
{testimonialsSubtitle && (
|
|
470
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed">
|
|
471
|
+
{testimonialsSubtitle}
|
|
472
|
+
</p>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<div className="grid gap-5 sm:gap-6 md:grid-cols-3 max-w-5xl mx-auto">
|
|
477
|
+
{testimonials.map((t, i) => (
|
|
478
|
+
<div
|
|
479
|
+
key={i}
|
|
480
|
+
className="rounded-xl border border-border bg-card p-5 sm:p-6 space-y-5 sm:space-y-6 flex flex-col text-center"
|
|
481
|
+
>
|
|
482
|
+
{/* Estrellas */}
|
|
483
|
+
<div className="flex justify-center gap-0.5 text-warning">
|
|
484
|
+
{Array.from({ length: 5 }).map((_, j) => (
|
|
485
|
+
<svg
|
|
486
|
+
key={j}
|
|
487
|
+
width="14"
|
|
488
|
+
height="14"
|
|
489
|
+
viewBox="0 0 24 24"
|
|
490
|
+
fill="currentColor"
|
|
491
|
+
>
|
|
492
|
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
|
493
|
+
</svg>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<p className="text-small leading-relaxed text-foreground flex-1">
|
|
498
|
+
"{t.quote}"
|
|
499
|
+
</p>
|
|
500
|
+
|
|
501
|
+
<div className="flex items-center justify-center gap-3 pt-4 sm:pt-5 border-t border-border">
|
|
502
|
+
<Avatar size="sm">
|
|
503
|
+
<AvatarFallback>{t.author?.charAt(0)}</AvatarFallback>
|
|
504
|
+
</Avatar>
|
|
505
|
+
<div className="text-left">
|
|
506
|
+
<p className="text-caption font-semibold">{t.author}</p>
|
|
507
|
+
<p className="text-[10px] text-muted-foreground">{t.role}</p>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
))}
|
|
512
|
+
</div>
|
|
513
|
+
</section>
|
|
514
|
+
)}
|
|
515
|
+
|
|
516
|
+
{/* ============================================================
|
|
517
|
+
VISIT — Info del local
|
|
518
|
+
============================================================ */}
|
|
519
|
+
{visitAddress && (
|
|
520
|
+
<section className="border-y border-border/60 bg-muted/20">
|
|
521
|
+
<div className="imansi-container py-20 sm:py-24 lg:py-28">
|
|
522
|
+
<div className="grid gap-10 lg:gap-16 lg:grid-cols-2 items-center max-w-6xl mx-auto">
|
|
523
|
+
{/* Info */}
|
|
524
|
+
<div className="space-y-6 sm:space-y-8">
|
|
525
|
+
<div className="space-y-3 sm:space-y-4">
|
|
526
|
+
{visitLabel && (
|
|
527
|
+
<p className="text-caption uppercase tracking-[0.2em] text-primary font-semibold">
|
|
528
|
+
{visitLabel}
|
|
529
|
+
</p>
|
|
530
|
+
)}
|
|
531
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
532
|
+
{visitTitle}
|
|
533
|
+
</h2>
|
|
534
|
+
{visitSubtitle && (
|
|
535
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed">
|
|
536
|
+
{visitSubtitle}
|
|
537
|
+
</p>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
{/* Info items */}
|
|
542
|
+
<div className="space-y-5">
|
|
543
|
+
<div className="flex items-start gap-4">
|
|
544
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-primary">
|
|
545
|
+
{Icons.mapPin}
|
|
546
|
+
</div>
|
|
547
|
+
<div className="space-y-1 pt-1">
|
|
548
|
+
<p className="text-caption font-semibold uppercase tracking-wider text-muted-foreground">
|
|
549
|
+
Dirección
|
|
550
|
+
</p>
|
|
551
|
+
<p className="text-small leading-relaxed whitespace-pre-line">
|
|
552
|
+
{visitAddress}
|
|
553
|
+
</p>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{visitHours.length > 0 && (
|
|
558
|
+
<div className="flex items-start gap-4">
|
|
559
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-primary">
|
|
560
|
+
{Icons.clock}
|
|
561
|
+
</div>
|
|
562
|
+
<div className="space-y-2 pt-1 flex-1">
|
|
563
|
+
<p className="text-caption font-semibold uppercase tracking-wider text-muted-foreground">
|
|
564
|
+
Horarios
|
|
565
|
+
</p>
|
|
566
|
+
<ul className="space-y-1.5">
|
|
567
|
+
{visitHours.map((h, i) => (
|
|
568
|
+
<li
|
|
569
|
+
key={i}
|
|
570
|
+
className="flex justify-between gap-4 text-small"
|
|
571
|
+
>
|
|
572
|
+
<span className="text-muted-foreground">
|
|
573
|
+
{h.day}
|
|
574
|
+
</span>
|
|
575
|
+
<span className="font-medium">{h.hours}</span>
|
|
576
|
+
</li>
|
|
577
|
+
))}
|
|
578
|
+
</ul>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
{(visitPhone || visitEmail) && (
|
|
584
|
+
<div className="flex items-start gap-4">
|
|
585
|
+
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-primary">
|
|
586
|
+
{visitPhone ? Icons.phone : Icons.mail}
|
|
587
|
+
</div>
|
|
588
|
+
<div className="space-y-2 pt-1 flex-1">
|
|
589
|
+
<p className="text-caption font-semibold uppercase tracking-wider text-muted-foreground">
|
|
590
|
+
Contacto
|
|
591
|
+
</p>
|
|
592
|
+
<div className="space-y-1">
|
|
593
|
+
{visitPhone && (
|
|
594
|
+
<a
|
|
595
|
+
href={`tel:${visitPhone.replace(/\s/g, '')}`}
|
|
596
|
+
className="block text-small hover:text-primary transition-colors"
|
|
597
|
+
>
|
|
598
|
+
{visitPhone}
|
|
599
|
+
</a>
|
|
600
|
+
)}
|
|
601
|
+
{visitEmail && (
|
|
602
|
+
<a
|
|
603
|
+
href={`mailto:${visitEmail}`}
|
|
604
|
+
className="block text-small hover:text-primary transition-colors"
|
|
605
|
+
>
|
|
606
|
+
{visitEmail}
|
|
607
|
+
</a>
|
|
608
|
+
)}
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{visitCTA && (
|
|
616
|
+
<div className="pt-2">
|
|
617
|
+
<Link to={visitCTA.href}>
|
|
618
|
+
<Button className="gap-2">
|
|
619
|
+
{visitCTA.label}
|
|
620
|
+
{Icons.arrow}
|
|
621
|
+
</Button>
|
|
622
|
+
</Link>
|
|
623
|
+
</div>
|
|
624
|
+
)}
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Mapa o imagen */}
|
|
628
|
+
{visitImage && (
|
|
629
|
+
<div className="relative">
|
|
630
|
+
<div className="relative rounded-2xl overflow-hidden border border-border shadow-xl">
|
|
631
|
+
<img
|
|
632
|
+
src={visitImage}
|
|
633
|
+
alt="Ubicación"
|
|
634
|
+
className="w-full aspect-[4/3] object-cover"
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
)}
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
</section>
|
|
642
|
+
)}
|
|
643
|
+
|
|
644
|
+
{/* ============================================================
|
|
645
|
+
CTA FINAL
|
|
646
|
+
============================================================ */}
|
|
647
|
+
<section className="imansi-container py-20 sm:py-24 lg:pb-28 lg:pt-28">
|
|
648
|
+
<div className="relative overflow-hidden rounded-2xl border border-border bg-card p-8 sm:p-14 lg:p-24">
|
|
649
|
+
<div
|
|
650
|
+
className="absolute inset-0 pointer-events-none"
|
|
651
|
+
style={{
|
|
652
|
+
backgroundImage: `
|
|
653
|
+
radial-gradient(circle at 15% 15%, var(--color-primary) 0%, transparent 40%),
|
|
654
|
+
radial-gradient(circle at 85% 85%, var(--color-accent) 0%, transparent 40%)
|
|
655
|
+
`,
|
|
656
|
+
opacity: 0.14,
|
|
657
|
+
}}
|
|
658
|
+
aria-hidden="true"
|
|
659
|
+
/>
|
|
660
|
+
<div
|
|
661
|
+
className="absolute inset-0 pointer-events-none"
|
|
662
|
+
style={{
|
|
663
|
+
backgroundImage: `
|
|
664
|
+
linear-gradient(currentColor 1px, transparent 1px),
|
|
665
|
+
linear-gradient(90deg, currentColor 1px, transparent 1px)
|
|
666
|
+
`,
|
|
667
|
+
backgroundSize: '40px 40px',
|
|
668
|
+
opacity: 0.03,
|
|
669
|
+
}}
|
|
670
|
+
aria-hidden="true"
|
|
671
|
+
/>
|
|
672
|
+
|
|
673
|
+
<div className="relative max-w-2xl mx-auto text-center space-y-5 sm:space-y-6">
|
|
674
|
+
<h2 className="text-h1 sm:text-display font-bold tracking-[-0.025em] leading-[1.1]">
|
|
675
|
+
{ctaTitle}
|
|
676
|
+
</h2>
|
|
677
|
+
<p className="text-small sm:text-body text-muted-foreground leading-relaxed max-w-xl mx-auto">
|
|
678
|
+
{ctaSubtitle}
|
|
679
|
+
</p>
|
|
680
|
+
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3 pt-3 sm:pt-4">
|
|
681
|
+
{ctaPrimary && (
|
|
682
|
+
<Link to={ctaPrimary.href} className="w-full sm:w-auto">
|
|
683
|
+
<Button size="lg" className="gap-2 w-full sm:w-auto">
|
|
684
|
+
{ctaPrimary.label}
|
|
685
|
+
{Icons.arrow}
|
|
686
|
+
</Button>
|
|
687
|
+
</Link>
|
|
688
|
+
)}
|
|
689
|
+
{ctaSecondary && (
|
|
690
|
+
<Link to={ctaSecondary.href} className="w-full sm:w-auto">
|
|
691
|
+
<Button size="lg" variant="outline" className="w-full sm:w-auto">
|
|
692
|
+
{ctaSecondary.label}
|
|
693
|
+
</Button>
|
|
694
|
+
</Link>
|
|
695
|
+
)}
|
|
696
|
+
</div>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</section>
|
|
700
|
+
</MarketingLayout>
|
|
701
|
+
);
|
|
702
|
+
}
|