@ecomiq/storefront 1.1.9 → 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/README.md +26 -418
- package/bin/ecomiq.mjs +154 -0
- package/package.json +72 -94
- package/src/api-proxy.ts +194 -0
- package/src/auth/create-storefront-auth.ts +35 -0
- package/src/auth/ecomiq-oauth-password-client.ts +26 -0
- package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
- package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
- package/src/auth/ecomiq-oauth-password-types.ts +25 -0
- package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
- package/src/auth/env.ts +7 -0
- package/src/cart-session.ts +241 -0
- package/src/client.ts +48 -0
- package/src/codegen.mjs +603 -0
- package/src/components/checkout/cart-sync.ts +143 -0
- package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
- package/src/components/checkout/checkout-form-controls.tsx +139 -0
- package/src/components/checkout/checkout-line-items.tsx +43 -0
- package/src/components/checkout/checkout-order-summary.tsx +156 -0
- package/src/components/checkout/checkout-payment-section.tsx +215 -0
- package/src/components/checkout/checkout-session-view.tsx +240 -0
- package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
- package/src/components/checkout/checkout-shipping-section.tsx +90 -0
- package/src/components/checkout/checkout-states.tsx +73 -0
- package/src/components/checkout/form-schema.ts +229 -0
- package/src/components/checkout/format.ts +27 -0
- package/src/components/checkout/index.ts +51 -0
- package/src/components/checkout/types.ts +43 -0
- package/src/components/checkout/use-checkout-geo.ts +50 -0
- package/src/components/checkout/use-checkout-session.ts +583 -0
- package/src/components/hero.tsx +291 -0
- package/src/config.d.ts +69 -0
- package/src/config.mjs +165 -0
- package/src/index.ts +274 -0
- package/src/link.tsx +2 -0
- package/src/pages/checkout.$checkoutId.tsx +60 -0
- package/src/pages/checkout.tsx +31 -0
- package/src/pages/thank-you.tsx +53 -0
- package/src/providers/auth.tsx +69 -0
- package/src/providers/cart.tsx +275 -0
- package/src/providers/ecomiq.ts +137 -0
- package/src/providers/index.ts +26 -0
- package/src/public-config.ts +44 -0
- package/src/review-submission.ts +89 -0
- package/src/runtime/context.ts +1 -0
- package/src/runtime/default-layout.tsx +80 -0
- package/src/runtime/head.ts +126 -0
- package/src/runtime/page.tsx +82 -0
- package/src/runtime/postcss-browser-stub.mjs +5 -0
- package/src/server-config-store.ts +26 -0
- package/src/server.ts +279 -0
- package/src/styles.css +76 -0
- package/src/ui/badge.tsx +32 -0
- package/src/ui/button.tsx +31 -0
- package/src/ui/card.tsx +31 -0
- package/src/ui/carousel.tsx +218 -0
- package/src/ui/cn.ts +6 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/separator.tsx +6 -0
- package/src/ui/skeleton.tsx +6 -0
- package/src/ui/storefront-error.tsx +49 -0
- package/src/utils.ts +36 -0
- package/src/vite-inline.mjs +95 -0
- package/templates/__root.tsx +77 -0
- package/templates/router.tsx +19 -0
- package/dist/chunk-64GLOWMJ.js +0 -216
- package/dist/chunk-64GLOWMJ.js.map +0 -1
- package/dist/chunk-AKD5IDEB.js +0 -177
- package/dist/chunk-AKD5IDEB.js.map +0 -1
- package/dist/chunk-EFEVJJWF.js +0 -1345
- package/dist/chunk-EFEVJJWF.js.map +0 -1
- package/dist/chunk-NAJQB5DN.js +0 -155
- package/dist/chunk-NAJQB5DN.js.map +0 -1
- package/dist/chunk-OABDGTBI.js +0 -253
- package/dist/chunk-OABDGTBI.js.map +0 -1
- package/dist/chunk-Q4U5OEUT.js +0 -21
- package/dist/chunk-Q4U5OEUT.js.map +0 -1
- package/dist/chunk-S7CPZ4UE.js +0 -6
- package/dist/chunk-S7CPZ4UE.js.map +0 -1
- package/dist/chunks/client.css +0 -984
- package/dist/chunks/client.css.map +0 -1
- package/dist/chunks/client.d.ts +0 -10759
- package/dist/chunks/client.js +0 -5398
- package/dist/chunks/client.js.map +0 -1
- package/dist/chunks/config.d.ts +0 -31
- package/dist/chunks/config.js +0 -5
- package/dist/chunks/config.js.map +0 -1
- package/dist/chunks/core-public.d.ts +0 -130
- package/dist/chunks/core-public.js +0 -92
- package/dist/chunks/core-public.js.map +0 -1
- package/dist/chunks/server-nextjs.d.ts +0 -33
- package/dist/chunks/server-nextjs.js +0 -59
- package/dist/chunks/server-nextjs.js.map +0 -1
- package/dist/chunks/server-tanstack.d.ts +0 -22
- package/dist/chunks/server-tanstack.js +0 -40
- package/dist/chunks/server-tanstack.js.map +0 -1
- package/dist/collection-DU7gREXS.d.ts +0 -4062
- package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
- package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
- package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.server.d.ts +0 -9
- package/dist/index.server.js +0 -8
- package/dist/index.server.js.map +0 -1
- package/dist/nextjs/api.d.ts +0 -45
- package/dist/nextjs/api.js +0 -32
- package/dist/nextjs/api.js.map +0 -1
- package/dist/nextjs/globals.css +0 -13
- package/dist/nextjs.d.ts +0 -2
- package/dist/nextjs.js +0 -2
- package/dist/nextjs.js.map +0 -1
- package/dist/nextjs.server.d.ts +0 -18
- package/dist/nextjs.server.js +0 -1
- package/dist/nextjs.server.js.map +0 -1
- package/dist/postcss.config.mjs +0 -6
- package/dist/react/checkout-page.css +0 -855
- package/dist/react/client.css +0 -984
- package/dist/react/payment-provider.css +0 -91
- package/dist/react/thank-you-page.css +0 -105
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -2
- package/dist/react.js.map +0 -1
- package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
- package/dist/tanstack/api.d.ts +0 -41
- package/dist/tanstack/api.js +0 -30
- package/dist/tanstack/api.js.map +0 -1
- package/dist/tanstack.d.ts +0 -1
- package/dist/tanstack.js +0 -2
- package/dist/tanstack.js.map +0 -1
- package/dist/tanstack.server.d.ts +0 -18
- package/dist/tanstack.server.js +0 -1
- package/dist/tanstack.server.js.map +0 -1
package/src/ui/card.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react'
|
|
2
|
+
import { cn } from './cn'
|
|
3
|
+
|
|
4
|
+
export function Card({ className, ...props }: ComponentProps<'div'>) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CardHeader({ className, ...props }: ComponentProps<'div'>) {
|
|
14
|
+
return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CardTitle({ className, ...props }: ComponentProps<'h3'>) {
|
|
18
|
+
return <h3 className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CardDescription({ className, ...props }: ComponentProps<'p'>) {
|
|
22
|
+
return <p className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function CardContent({ className, ...props }: ComponentProps<'div'>) {
|
|
26
|
+
return <div className={cn('p-6 pt-0', className)} {...props} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CardFooter({ className, ...props }: ComponentProps<'div'>) {
|
|
30
|
+
return <div className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
|
31
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
useCallback,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type HTMLAttributes,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import useEmblaCarousel, {
|
|
13
|
+
type UseEmblaCarouselType,
|
|
14
|
+
} from 'embla-carousel-react'
|
|
15
|
+
import Autoplay from 'embla-carousel-autoplay'
|
|
16
|
+
import Fade from 'embla-carousel-fade'
|
|
17
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
18
|
+
import { cn } from './cn'
|
|
19
|
+
import { Button } from './button'
|
|
20
|
+
|
|
21
|
+
type EmblaCarouselApi = UseEmblaCarouselType[1]
|
|
22
|
+
|
|
23
|
+
type CarouselContextValue = {
|
|
24
|
+
api: EmblaCarouselApi | undefined
|
|
25
|
+
selectedIndex: number
|
|
26
|
+
scrollPrev: () => void
|
|
27
|
+
scrollNext: () => void
|
|
28
|
+
scrollTo: (index: number) => void
|
|
29
|
+
slideCount: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CarouselContext = createContext<CarouselContextValue | null>(null)
|
|
33
|
+
|
|
34
|
+
function useCarousel() {
|
|
35
|
+
const ctx = useContext(CarouselContext)
|
|
36
|
+
if (!ctx) throw new Error('Carousel components must be used within <Carousel>')
|
|
37
|
+
return ctx
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CarouselProps = {
|
|
41
|
+
children: ReactNode
|
|
42
|
+
className?: string
|
|
43
|
+
/** Loop infinito. Default: true si hay más de un slide. */
|
|
44
|
+
loop?: boolean
|
|
45
|
+
/** Transición fade (Embla Fade). Default: false. */
|
|
46
|
+
fade?: boolean
|
|
47
|
+
/** Autoplay: delay en ms, o false para desactivar. Default: false. */
|
|
48
|
+
autoplay?: number | false
|
|
49
|
+
/** Flechas prev/next. Default: true si hay más de un slide. */
|
|
50
|
+
showArrows?: boolean
|
|
51
|
+
/** Dots de navegación. Default: true si hay más de un slide. */
|
|
52
|
+
showDots?: boolean
|
|
53
|
+
/** Etiqueta accesible del region. */
|
|
54
|
+
'aria-label'?: string
|
|
55
|
+
} & Omit<HTMLAttributes<HTMLElement>, 'children'>
|
|
56
|
+
|
|
57
|
+
export function Carousel({
|
|
58
|
+
children,
|
|
59
|
+
className,
|
|
60
|
+
loop,
|
|
61
|
+
fade = false,
|
|
62
|
+
autoplay = false,
|
|
63
|
+
showArrows,
|
|
64
|
+
showDots,
|
|
65
|
+
'aria-label': ariaLabel = 'Carrusel',
|
|
66
|
+
...rest
|
|
67
|
+
}: CarouselProps) {
|
|
68
|
+
const slides = Children.toArray(children).filter(Boolean)
|
|
69
|
+
const slideCount = slides.length
|
|
70
|
+
const multi = slideCount > 1
|
|
71
|
+
const shouldLoop = loop ?? multi
|
|
72
|
+
const arrows = showArrows ?? multi
|
|
73
|
+
const dots = showDots ?? multi
|
|
74
|
+
|
|
75
|
+
const plugins = useMemo(() => {
|
|
76
|
+
if (!multi) return []
|
|
77
|
+
const list = []
|
|
78
|
+
if (fade) list.push(Fade())
|
|
79
|
+
if (autoplay !== false) {
|
|
80
|
+
list.push(
|
|
81
|
+
Autoplay({
|
|
82
|
+
delay: typeof autoplay === 'number' ? autoplay : 6000,
|
|
83
|
+
stopOnInteraction: false,
|
|
84
|
+
stopOnMouseEnter: true,
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
return list
|
|
89
|
+
}, [multi, fade, autoplay])
|
|
90
|
+
|
|
91
|
+
const [emblaRef, emblaApi] = useEmblaCarousel(
|
|
92
|
+
{ loop: shouldLoop, align: 'start', duration: 35 },
|
|
93
|
+
plugins,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
97
|
+
|
|
98
|
+
const resetAutoplay = useCallback(() => {
|
|
99
|
+
const plugin = emblaApi?.plugins()?.autoplay
|
|
100
|
+
if (plugin && 'reset' in plugin) plugin.reset()
|
|
101
|
+
}, [emblaApi])
|
|
102
|
+
|
|
103
|
+
const scrollPrev = useCallback(() => {
|
|
104
|
+
emblaApi?.scrollPrev()
|
|
105
|
+
resetAutoplay()
|
|
106
|
+
}, [emblaApi, resetAutoplay])
|
|
107
|
+
|
|
108
|
+
const scrollNext = useCallback(() => {
|
|
109
|
+
emblaApi?.scrollNext()
|
|
110
|
+
resetAutoplay()
|
|
111
|
+
}, [emblaApi, resetAutoplay])
|
|
112
|
+
|
|
113
|
+
const scrollTo = useCallback(
|
|
114
|
+
(index: number) => {
|
|
115
|
+
emblaApi?.scrollTo(index)
|
|
116
|
+
resetAutoplay()
|
|
117
|
+
},
|
|
118
|
+
[emblaApi, resetAutoplay],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!emblaApi) return
|
|
123
|
+
const onSelect = () => setSelectedIndex(emblaApi.selectedScrollSnap())
|
|
124
|
+
onSelect()
|
|
125
|
+
emblaApi.on('select', onSelect)
|
|
126
|
+
emblaApi.on('reInit', onSelect)
|
|
127
|
+
return () => {
|
|
128
|
+
emblaApi.off('select', onSelect)
|
|
129
|
+
emblaApi.off('reInit', onSelect)
|
|
130
|
+
}
|
|
131
|
+
}, [emblaApi])
|
|
132
|
+
|
|
133
|
+
const value = useMemo(
|
|
134
|
+
() => ({
|
|
135
|
+
api: emblaApi,
|
|
136
|
+
selectedIndex,
|
|
137
|
+
scrollPrev,
|
|
138
|
+
scrollNext,
|
|
139
|
+
scrollTo,
|
|
140
|
+
slideCount,
|
|
141
|
+
}),
|
|
142
|
+
[emblaApi, selectedIndex, scrollPrev, scrollNext, scrollTo, slideCount],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if (slideCount === 0) return null
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<CarouselContext.Provider value={value}>
|
|
149
|
+
<section
|
|
150
|
+
className={cn('group relative overflow-hidden', className)}
|
|
151
|
+
aria-label={ariaLabel}
|
|
152
|
+
role="region"
|
|
153
|
+
aria-roledescription="carrusel"
|
|
154
|
+
{...rest}
|
|
155
|
+
>
|
|
156
|
+
<div className="overflow-hidden" ref={emblaRef}>
|
|
157
|
+
<div className="flex touch-pan-y">
|
|
158
|
+
{slides.map((slide, index) => (
|
|
159
|
+
<div key={index} className="relative min-w-0 flex-[0_0_100%]">
|
|
160
|
+
{slide}
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{arrows ? (
|
|
167
|
+
<div className="pointer-events-none absolute inset-x-0 top-1/2 z-20 flex -translate-y-1/2 justify-between px-4 md:px-8">
|
|
168
|
+
<Button
|
|
169
|
+
type="button"
|
|
170
|
+
variant="outline"
|
|
171
|
+
size="icon"
|
|
172
|
+
onClick={scrollPrev}
|
|
173
|
+
className="pointer-events-auto rounded-full border-white/20 bg-black/40 text-white shadow-sm backdrop-blur-sm hover:bg-black/60 hover:text-white"
|
|
174
|
+
aria-label="Slide anterior"
|
|
175
|
+
>
|
|
176
|
+
<ChevronLeft />
|
|
177
|
+
</Button>
|
|
178
|
+
<Button
|
|
179
|
+
type="button"
|
|
180
|
+
variant="outline"
|
|
181
|
+
size="icon"
|
|
182
|
+
onClick={scrollNext}
|
|
183
|
+
className="pointer-events-auto rounded-full border-white/20 bg-black/40 text-white shadow-sm backdrop-blur-sm hover:bg-black/60 hover:text-white"
|
|
184
|
+
aria-label="Slide siguiente"
|
|
185
|
+
>
|
|
186
|
+
<ChevronRight />
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
) : null}
|
|
190
|
+
|
|
191
|
+
{dots ? (
|
|
192
|
+
<div
|
|
193
|
+
className="absolute bottom-6 left-1/2 z-20 flex -translate-x-1/2 gap-2 md:bottom-8"
|
|
194
|
+
role="tablist"
|
|
195
|
+
aria-label="Slides"
|
|
196
|
+
>
|
|
197
|
+
{slides.map((_, i) => (
|
|
198
|
+
<button
|
|
199
|
+
key={i}
|
|
200
|
+
type="button"
|
|
201
|
+
role="tab"
|
|
202
|
+
aria-selected={i === selectedIndex}
|
|
203
|
+
aria-label={`Slide ${i + 1}`}
|
|
204
|
+
onClick={() => scrollTo(i)}
|
|
205
|
+
className={cn(
|
|
206
|
+
'h-2 w-2 rounded-full border border-white/30 transition md:h-2.5 md:w-2.5',
|
|
207
|
+
i === selectedIndex ? 'scale-110 bg-white' : 'bg-white/45 hover:bg-white/70',
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
) : null}
|
|
213
|
+
</section>
|
|
214
|
+
</CarouselContext.Provider>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { useCarousel }
|
package/src/ui/cn.ts
ADDED
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Button, buttonVariants, type ButtonProps } from './button'
|
|
2
|
+
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card'
|
|
3
|
+
export { Badge, type BadgeProps } from './badge'
|
|
4
|
+
export { Skeleton } from './skeleton'
|
|
5
|
+
export { Separator } from './separator'
|
|
6
|
+
export { Carousel, useCarousel, type CarouselProps } from './carousel'
|
|
7
|
+
export { StorefrontError, type StorefrontErrorProps } from './storefront-error'
|
|
8
|
+
export { cn } from './cn'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { House, RefreshCw, TriangleAlert } from 'lucide-react'
|
|
2
|
+
import { Button, buttonVariants } from './button'
|
|
3
|
+
|
|
4
|
+
export interface StorefrontErrorProps {
|
|
5
|
+
title?: string
|
|
6
|
+
description?: string
|
|
7
|
+
onRetry?: () => void
|
|
8
|
+
returnTo?: string
|
|
9
|
+
returnLabel?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StorefrontError({
|
|
13
|
+
title = 'No pudimos cargar esta página',
|
|
14
|
+
description = 'La tienda no respondió como esperábamos. Inténtalo nuevamente en unos segundos.',
|
|
15
|
+
onRetry,
|
|
16
|
+
returnTo = '/',
|
|
17
|
+
returnLabel = 'Volver al inicio',
|
|
18
|
+
}: StorefrontErrorProps) {
|
|
19
|
+
return (
|
|
20
|
+
<section className="mx-auto flex min-h-[55vh] w-full max-w-6xl items-center px-4 py-16" role="alert">
|
|
21
|
+
<div className="w-full max-w-xl border-l-2 border-primary pl-5 sm:pl-7">
|
|
22
|
+
<div className="mb-5 flex size-11 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
23
|
+
<TriangleAlert className="size-5" aria-hidden="true" />
|
|
24
|
+
</div>
|
|
25
|
+
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
26
|
+
Interrupción temporal
|
|
27
|
+
</p>
|
|
28
|
+
<h1 className="text-balance text-2xl font-semibold tracking-[-0.02em] sm:text-3xl">
|
|
29
|
+
{title}
|
|
30
|
+
</h1>
|
|
31
|
+
<p className="mt-3 max-w-md text-sm leading-6 text-muted-foreground">
|
|
32
|
+
{description}
|
|
33
|
+
</p>
|
|
34
|
+
<div className="mt-6 flex flex-wrap gap-2">
|
|
35
|
+
{onRetry ? (
|
|
36
|
+
<Button onClick={onRetry} className="min-h-10">
|
|
37
|
+
<RefreshCw aria-hidden="true" />
|
|
38
|
+
Intentar otra vez
|
|
39
|
+
</Button>
|
|
40
|
+
) : null}
|
|
41
|
+
<a href={returnTo} className={buttonVariants({ variant: 'outline' })}>
|
|
42
|
+
<House aria-hidden="true" />
|
|
43
|
+
{returnLabel}
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
)
|
|
49
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { StorefrontSettings } from "@ecomiq/core";
|
|
2
|
+
|
|
3
|
+
type StorefrontCmsPage = StorefrontSettings['cms']['pages'][number]
|
|
4
|
+
|
|
5
|
+
function isRenderableCmsPage(p: StorefrontCmsPage): boolean {
|
|
6
|
+
return p.status === "published";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function findStorefrontCmsPageBySlug(
|
|
10
|
+
settings: StorefrontSettings | null | undefined,
|
|
11
|
+
slug: string
|
|
12
|
+
) {
|
|
13
|
+
const pages = settings?.cms?.pages;
|
|
14
|
+
if (!pages?.length) return null;
|
|
15
|
+
const raw = pages.find(
|
|
16
|
+
(p) => p.slug === slug && isRenderableCmsPage(p)
|
|
17
|
+
);
|
|
18
|
+
if (!raw) return null;
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function cmsBranding(settings: StorefrontSettings | null | undefined) {
|
|
23
|
+
return settings?.cms?.settings?.branding;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cmsSeo(settings: StorefrontSettings | null | undefined) {
|
|
27
|
+
return settings?.cms?.settings?.seo;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function storeDisplayName(settings: StorefrontSettings | null | undefined) {
|
|
31
|
+
return settings?.store?.name ?? cmsSeo(settings)?.siteName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function storeSeoDescription(settings: StorefrontSettings | null | undefined) {
|
|
35
|
+
return cmsSeo(settings)?.metaDescription ?? settings?.store?.description;
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config interna de Vite, construida en memoria por la CLI.
|
|
3
|
+
* No existe vite.config.ts en el proyecto del usuario ni en .ecomiq:
|
|
4
|
+
* los plugins se importan aquí (contexto del framework) y se pasan como
|
|
5
|
+
* instancias, así la resolución de módulos nunca depende del proyecto.
|
|
6
|
+
*/
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
|
10
|
+
import viteReact from '@vitejs/plugin-react'
|
|
11
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
12
|
+
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
13
|
+
|
|
14
|
+
const postcssBrowserStub = fileURLToPath(
|
|
15
|
+
new URL('./runtime/postcss-browser-stub.mjs', import.meta.url),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
function resolveSanitizeHtmlPostcss(source, importer) {
|
|
19
|
+
if (source !== 'postcss') return null
|
|
20
|
+
if (!importer?.includes('/sanitize-html/')) return null
|
|
21
|
+
return postcssBrowserStub
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sanitizeHtmlClientBoundary() {
|
|
25
|
+
return {
|
|
26
|
+
name: 'ecomiq:sanitize-html-client-boundary',
|
|
27
|
+
enforce: 'pre',
|
|
28
|
+
applyToEnvironment(environment) {
|
|
29
|
+
return environment.name === 'client'
|
|
30
|
+
},
|
|
31
|
+
configEnvironment(name, config) {
|
|
32
|
+
if (name !== 'client') return
|
|
33
|
+
const rolldownOptions = config.optimizeDeps?.rolldownOptions ?? {}
|
|
34
|
+
return {
|
|
35
|
+
optimizeDeps: {
|
|
36
|
+
rolldownOptions: {
|
|
37
|
+
...rolldownOptions,
|
|
38
|
+
plugins: [
|
|
39
|
+
...(rolldownOptions.plugins ?? []),
|
|
40
|
+
{
|
|
41
|
+
name: 'ecomiq:sanitize-html-optimizer-boundary',
|
|
42
|
+
resolveId: resolveSanitizeHtmlPostcss,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
resolveId: resolveSanitizeHtmlPostcss,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createViteConfig({ appDir, config }) {
|
|
54
|
+
return {
|
|
55
|
+
root: appDir,
|
|
56
|
+
configFile: false,
|
|
57
|
+
publicDir: resolve(appDir, '../public'),
|
|
58
|
+
plugins: [
|
|
59
|
+
tailwindcss(),
|
|
60
|
+
// Emula el runtime de Workers en dev y empaqueta para Cloudflare en build.
|
|
61
|
+
cloudflare({
|
|
62
|
+
configPath: resolve(appDir, 'wrangler.jsonc'),
|
|
63
|
+
viteEnvironment: { name: 'ssr' },
|
|
64
|
+
}),
|
|
65
|
+
tanstackStart({
|
|
66
|
+
srcDirectory: 'src',
|
|
67
|
+
router: {
|
|
68
|
+
routesDirectory: 'routes',
|
|
69
|
+
generatedRouteTree: 'routeTree.gen.ts',
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
sanitizeHtmlClientBoundary(),
|
|
73
|
+
viteReact(),
|
|
74
|
+
],
|
|
75
|
+
build: {
|
|
76
|
+
// Un solo CSS; nombre estable → SSR y client apuntan al mismo asset en Workers.
|
|
77
|
+
cssCodeSplit: false,
|
|
78
|
+
rollupOptions: {
|
|
79
|
+
output: {
|
|
80
|
+
assetFileNames: (assetInfo) => {
|
|
81
|
+
const name = assetInfo.names?.join('') ?? assetInfo.name ?? ''
|
|
82
|
+
if (name.endsWith('.css')) return 'assets/styles[extname]'
|
|
83
|
+
return 'assets/[name]-[hash][extname]'
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
server: {
|
|
89
|
+
fs: {
|
|
90
|
+
// pages/, lib/ y public/ del usuario viven un nivel arriba de .ecomiq
|
|
91
|
+
allow: ['..'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Generado por Ecomiq — no editar. */
|
|
2
|
+
import { useState, type ReactNode } from 'react'
|
|
3
|
+
import { createRootRoute, HeadContent, Scripts, Outlet, Link } from '@tanstack/react-router'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { AppEcomiqProvider } from '@ecomiq/storefront/internal/context'
|
|
6
|
+
import { buildRootHead } from '@ecomiq/storefront/internal/head'
|
|
7
|
+
import { serverApi } from '@ecomiq/storefront'
|
|
8
|
+
import { StorefrontError } from '@ecomiq/storefront/ui'
|
|
9
|
+
import Layout from '../user-layout'
|
|
10
|
+
import publicConfig from '../app-config'
|
|
11
|
+
// Side-effect: Start lo trata como asset del manifest (mismo URL en SSR y client).
|
|
12
|
+
// Evita el FOUC por hashes distintos con `?url` en head().
|
|
13
|
+
import '../styles.css'
|
|
14
|
+
|
|
15
|
+
export const Route = createRootRoute({
|
|
16
|
+
loader: () => serverApi.getSettings(),
|
|
17
|
+
head: () => buildRootHead(publicConfig),
|
|
18
|
+
component: RootComponent,
|
|
19
|
+
errorComponent: RootError,
|
|
20
|
+
notFoundComponent: NotFound,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function RootComponent() {
|
|
24
|
+
const settings = Route.useLoaderData()
|
|
25
|
+
const [queryClient] = useState(
|
|
26
|
+
() =>
|
|
27
|
+
new QueryClient({
|
|
28
|
+
defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
|
|
29
|
+
}),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<RootDocument language={settings.store.language}>
|
|
34
|
+
<QueryClientProvider client={queryClient}>
|
|
35
|
+
<AppEcomiqProvider config={publicConfig} settings={settings}>
|
|
36
|
+
<Layout config={publicConfig} settings={settings}>
|
|
37
|
+
<Outlet />
|
|
38
|
+
</Layout>
|
|
39
|
+
</AppEcomiqProvider>
|
|
40
|
+
</QueryClientProvider>
|
|
41
|
+
</RootDocument>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function RootDocument({ children, language }: { children: ReactNode; language: string }) {
|
|
46
|
+
return (
|
|
47
|
+
<html lang={language} suppressHydrationWarning>
|
|
48
|
+
<head>
|
|
49
|
+
<HeadContent />
|
|
50
|
+
</head>
|
|
51
|
+
<body className="bg-background text-foreground antialiased">
|
|
52
|
+
{children}
|
|
53
|
+
<Scripts />
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function RootError({ reset }: { reset: () => void }) {
|
|
60
|
+
return (
|
|
61
|
+
<RootDocument language="es">
|
|
62
|
+
<StorefrontError onRetry={reset} />
|
|
63
|
+
</RootDocument>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function NotFound() {
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col items-center gap-3 py-24 text-center">
|
|
70
|
+
<p className="text-6xl font-bold tracking-tight">404</p>
|
|
71
|
+
<p className="text-muted-foreground">Esta página no existe.</p>
|
|
72
|
+
<Link to="/" className="text-sm underline underline-offset-4">
|
|
73
|
+
Volver al inicio
|
|
74
|
+
</Link>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Generado por Ecomiq — no editar. */
|
|
2
|
+
import { createRouter } from '@tanstack/react-router'
|
|
3
|
+
import { routeTree } from './routeTree.gen'
|
|
4
|
+
|
|
5
|
+
export function getRouter() {
|
|
6
|
+
return createRouter({
|
|
7
|
+
routeTree,
|
|
8
|
+
defaultPreload: 'intent',
|
|
9
|
+
defaultPreloadDelay: 200,
|
|
10
|
+
scrollRestoration: true,
|
|
11
|
+
defaultStaleTime: 30_000,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare module '@tanstack/react-router' {
|
|
16
|
+
interface Register {
|
|
17
|
+
router: ReturnType<typeof getRouter>
|
|
18
|
+
}
|
|
19
|
+
}
|