@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime interno: construcción del <head>.
|
|
3
|
+
* Mezcla el SEO global de ecomiq.config.js con el `meta` de cada página
|
|
4
|
+
* (la página siempre gana) y genera los scripts de analytics declarados.
|
|
5
|
+
*/
|
|
6
|
+
import type { PageMeta, PublicEcomiqConfig } from '../index'
|
|
7
|
+
|
|
8
|
+
type MetaTag = Record<string, string>
|
|
9
|
+
type LinkTag = Record<string, string>
|
|
10
|
+
type ScriptTag = Record<string, string | boolean> & { children?: string }
|
|
11
|
+
|
|
12
|
+
interface HeadResult {
|
|
13
|
+
meta?: MetaTag[]
|
|
14
|
+
links?: LinkTag[]
|
|
15
|
+
scripts?: ScriptTag[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serializeJsonLd(value: Record<string, unknown>) {
|
|
19
|
+
return JSON.stringify(value)
|
|
20
|
+
.replace(/</g, '\\u003c')
|
|
21
|
+
.replace(/\u2028/g, '\\u2028')
|
|
22
|
+
.replace(/\u2029/g, '\\u2029')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Aplica titleTemplate ('%s · Tienda') o cae al patrón `Título · Tienda`. */
|
|
26
|
+
export function resolveTitle(pageTitle: string | undefined, config: PublicEcomiqConfig): string {
|
|
27
|
+
const seo = config.seo ?? {}
|
|
28
|
+
if (!pageTitle) return config.name
|
|
29
|
+
const template = seo.titleTemplate
|
|
30
|
+
return template ? template.replace('%s', pageTitle) : `${pageTitle} · ${config.name}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Head global: charset, viewport, favicon, metas/links extra y analytics. Va en el root. */
|
|
34
|
+
export function buildRootHead(config: PublicEcomiqConfig): HeadResult {
|
|
35
|
+
const seo = config.seo ?? {}
|
|
36
|
+
|
|
37
|
+
const meta: MetaTag[] = [
|
|
38
|
+
{ charSet: 'utf-8' },
|
|
39
|
+
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
|
40
|
+
]
|
|
41
|
+
if (seo.keywords?.length) meta.push({ name: 'keywords', content: seo.keywords.join(', ') })
|
|
42
|
+
for (const extra of seo.meta ?? []) meta.push(extra)
|
|
43
|
+
|
|
44
|
+
const links: LinkTag[] = [{ rel: 'icon', href: seo.favicon ?? '/favicon.ico' }]
|
|
45
|
+
for (const extra of seo.links ?? []) links.push(extra)
|
|
46
|
+
|
|
47
|
+
return { meta, links, scripts: buildAnalyticsScripts(config) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Head por página: title (con plantilla), description, OG/Twitter, robots, metas extra y JSON-LD. */
|
|
51
|
+
export function buildPageHead(
|
|
52
|
+
pageMeta: PageMeta | undefined,
|
|
53
|
+
config: PublicEcomiqConfig,
|
|
54
|
+
): HeadResult {
|
|
55
|
+
const seo = config.seo ?? {}
|
|
56
|
+
const title = resolveTitle(pageMeta?.title, config)
|
|
57
|
+
const description = pageMeta?.description ?? config.description
|
|
58
|
+
const ogImage = pageMeta?.ogImage ?? seo.ogImage
|
|
59
|
+
|
|
60
|
+
const meta: MetaTag[] = [{ title }]
|
|
61
|
+
if (description) meta.push({ name: 'description', content: description })
|
|
62
|
+
if (pageMeta?.noIndex) meta.push({ name: 'robots', content: 'noindex, nofollow' })
|
|
63
|
+
|
|
64
|
+
// Open Graph + Twitter con fallbacks razonables
|
|
65
|
+
meta.push({ property: 'og:title', content: title })
|
|
66
|
+
meta.push({ property: 'og:type', content: 'website' })
|
|
67
|
+
meta.push({ property: 'og:site_name', content: config.name })
|
|
68
|
+
if (description) meta.push({ property: 'og:description', content: description })
|
|
69
|
+
if (ogImage) {
|
|
70
|
+
meta.push({ property: 'og:image', content: ogImage })
|
|
71
|
+
meta.push({ name: 'twitter:card', content: seo.twitterCard ?? 'summary_large_image' })
|
|
72
|
+
meta.push({ name: 'twitter:image', content: ogImage })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const extra of pageMeta?.meta ?? []) meta.push(extra as MetaTag)
|
|
76
|
+
|
|
77
|
+
const scripts: ScriptTag[] = []
|
|
78
|
+
if (pageMeta?.jsonLd) {
|
|
79
|
+
scripts.push({ type: 'application/ld+json', children: serializeJsonLd(pageMeta.jsonLd) })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { meta, ...(scripts.length ? { scripts } : {}) }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Traduce el bloque `analytics` del config a etiquetas <script>. */
|
|
86
|
+
export function buildAnalyticsScripts(config: PublicEcomiqConfig): ScriptTag[] {
|
|
87
|
+
const analytics = config.analytics ?? {}
|
|
88
|
+
const scripts: ScriptTag[] = []
|
|
89
|
+
|
|
90
|
+
if (analytics.googleAnalytics) {
|
|
91
|
+
const id = analytics.googleAnalytics
|
|
92
|
+
scripts.push({ src: `https://www.googletagmanager.com/gtag/js?id=${id}`, async: true })
|
|
93
|
+
scripts.push({
|
|
94
|
+
children: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)};gtag('js',new Date());gtag('config','${id}');`,
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (analytics.plausible) {
|
|
99
|
+
scripts.push({
|
|
100
|
+
src: 'https://plausible.io/js/script.js',
|
|
101
|
+
defer: true,
|
|
102
|
+
'data-domain': analytics.plausible,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (analytics.metaPixel) {
|
|
107
|
+
const id = analytics.metaPixel
|
|
108
|
+
scripts.push({
|
|
109
|
+
children: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const custom of analytics.scripts ?? []) {
|
|
114
|
+
if (custom.src) {
|
|
115
|
+
scripts.push({
|
|
116
|
+
src: custom.src,
|
|
117
|
+
...(custom.defer ? { defer: true } : {}),
|
|
118
|
+
...(custom.async ? { async: true } : {}),
|
|
119
|
+
})
|
|
120
|
+
} else if (custom.inline) {
|
|
121
|
+
scripts.push({ children: custom.inline })
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return scripts
|
|
126
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime interno de Ecomiq (solo lo importa el código generado en .ecomiq).
|
|
3
|
+
* Traduce el contrato público `definePage` a las opciones del router interno.
|
|
4
|
+
*/
|
|
5
|
+
import type { EcomiqPage, PublicEcomiqConfig } from '../index'
|
|
6
|
+
import { StorefrontError } from '../ui/storefront-error'
|
|
7
|
+
import { buildPageHead } from './head'
|
|
8
|
+
|
|
9
|
+
export function pageToRouteOptions<
|
|
10
|
+
TData,
|
|
11
|
+
TParams extends Record<string, string>,
|
|
12
|
+
TSearch extends Record<string, unknown>,
|
|
13
|
+
>(
|
|
14
|
+
page: EcomiqPage<TData, TParams, TSearch>,
|
|
15
|
+
config: PublicEcomiqConfig,
|
|
16
|
+
) {
|
|
17
|
+
const readSearch = page.validateSearch ?? ((raw: Record<string, unknown>) => raw as TSearch)
|
|
18
|
+
const reloadOnSearch = page.reloadOnSearch !== false
|
|
19
|
+
const stableLoaderDeps: Record<string, never> = {}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
preload: page.preload,
|
|
23
|
+
shouldReload: reloadOnSearch ? undefined : false,
|
|
24
|
+
validateSearch: readSearch,
|
|
25
|
+
loaderDeps: ({ search }: { search: TSearch }) => reloadOnSearch ? search : stableLoaderDeps,
|
|
26
|
+
loader: page.loader
|
|
27
|
+
? ({ params, deps, location }: {
|
|
28
|
+
params: TParams
|
|
29
|
+
deps: TSearch | Record<string, never>
|
|
30
|
+
location?: { search?: Record<string, unknown> }
|
|
31
|
+
}) => {
|
|
32
|
+
let search = readSearch(location?.search ?? {})
|
|
33
|
+
if (reloadOnSearch) {
|
|
34
|
+
search = deps as TSearch
|
|
35
|
+
}
|
|
36
|
+
return page.loader!({
|
|
37
|
+
params,
|
|
38
|
+
search,
|
|
39
|
+
config,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
: undefined,
|
|
43
|
+
head: ({ loaderData, params, match }: {
|
|
44
|
+
loaderData: TData
|
|
45
|
+
params: TParams
|
|
46
|
+
match: { search: TSearch }
|
|
47
|
+
}) => buildPageHead(
|
|
48
|
+
loaderData === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: page.meta?.({
|
|
51
|
+
data: loaderData,
|
|
52
|
+
params,
|
|
53
|
+
search: match.search,
|
|
54
|
+
config,
|
|
55
|
+
}),
|
|
56
|
+
config,
|
|
57
|
+
),
|
|
58
|
+
pendingComponent: page.loading,
|
|
59
|
+
errorComponent: page.error
|
|
60
|
+
? ({ error, reset }: { error: Error; reset: () => void }) => page.error!({ error, reset })
|
|
61
|
+
: ({ reset }: { error: Error; reset: () => void }) => <StorefrontError onRetry={reset} />,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function PageBoundary<
|
|
66
|
+
TData,
|
|
67
|
+
TParams extends Record<string, string>,
|
|
68
|
+
TSearch extends Record<string, unknown>,
|
|
69
|
+
>({
|
|
70
|
+
page,
|
|
71
|
+
data,
|
|
72
|
+
params,
|
|
73
|
+
search,
|
|
74
|
+
}: {
|
|
75
|
+
page: EcomiqPage<TData, TParams, TSearch>
|
|
76
|
+
data: TData
|
|
77
|
+
params: TParams
|
|
78
|
+
search: TSearch
|
|
79
|
+
}) {
|
|
80
|
+
const Component = page.component
|
|
81
|
+
return <Component data={data} params={params} search={search} />
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CreateApiOptions } from '@ecomiq/core'
|
|
2
|
+
import { createServerOnlyFn } from '@tanstack/react-start'
|
|
3
|
+
|
|
4
|
+
export interface ServerEcomiqConfig {
|
|
5
|
+
slug: string
|
|
6
|
+
api?: CreateApiOptions
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const serverConfigKey = Symbol.for('@ecomiq/storefront/server-config')
|
|
10
|
+
const serverGlobal = globalThis as typeof globalThis & {
|
|
11
|
+
[key: symbol]: ServerEcomiqConfig | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const registerServerConfig = createServerOnlyFn((config: ServerEcomiqConfig): void => {
|
|
15
|
+
serverGlobal[serverConfigKey] = config
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export const getServerConfig = createServerOnlyFn((): ServerEcomiqConfig => {
|
|
19
|
+
const registered = serverGlobal[serverConfigKey]
|
|
20
|
+
if (!registered) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'Config server de Ecomiq no disponible. Ejecuta la app mediante `ecomiq dev|build`.',
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
return registered
|
|
26
|
+
})
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API storefront que solo corre en el servidor (TanStack Start RPC).
|
|
3
|
+
* En SSR ejecuta el handler localmente; en el cliente llama al Worker.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { definePage, serverApi } from '@ecomiq/storefront'
|
|
7
|
+
*
|
|
8
|
+
* export default definePage({
|
|
9
|
+
* loader: () => serverApi.getProducts({ page: 1, pageSize: 12 }),
|
|
10
|
+
* component: ({ data }) => …,
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Nota: cada `createServerFn` debe vivir en el top-level del módulo
|
|
15
|
+
* (el compilador de Start asigna IDs estables por call-site).
|
|
16
|
+
* El cliente storefront es singleton → el cache de settings se reutiliza.
|
|
17
|
+
*/
|
|
18
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
19
|
+
import {
|
|
20
|
+
AddCartItemParamsSchema,
|
|
21
|
+
CartAddressesParamsSchema,
|
|
22
|
+
CartBuyerParamsSchema,
|
|
23
|
+
CartCheckoutParamsSchema,
|
|
24
|
+
CartCheckoutReadParamsSchema,
|
|
25
|
+
CartPromotionCodeParamsSchema,
|
|
26
|
+
CreateProductReviewParamsSchema,
|
|
27
|
+
GetCartParamsSchema,
|
|
28
|
+
GetDepartmentsParamsSchema,
|
|
29
|
+
GetDistrictsParamsSchema,
|
|
30
|
+
GetProvincesParamsSchema,
|
|
31
|
+
GetShippingOptionsParamsSchema,
|
|
32
|
+
ListProductReviewsParamsSchema,
|
|
33
|
+
ProductAutocompleteParamsSchema,
|
|
34
|
+
ProductDetailParamsSchema,
|
|
35
|
+
ProductsParamsSchema,
|
|
36
|
+
PutShippingMethodParamsSchema,
|
|
37
|
+
RelatedProductsParamsSchema,
|
|
38
|
+
RemoveCartItemParamsSchema,
|
|
39
|
+
UpdateCartItemQuantityParamsSchema,
|
|
40
|
+
type AddCartItemParams,
|
|
41
|
+
type CartAddressesParams,
|
|
42
|
+
type CartBuyerParams,
|
|
43
|
+
type CartCheckoutParams,
|
|
44
|
+
type CartCheckoutReadParams,
|
|
45
|
+
type CartPromotionCodeParams,
|
|
46
|
+
type CreateProductReviewParams,
|
|
47
|
+
type GetCartParams,
|
|
48
|
+
type GetDepartmentsParams,
|
|
49
|
+
type GetDistrictsParams,
|
|
50
|
+
type GetProvincesParams,
|
|
51
|
+
type GetShippingOptionsParams,
|
|
52
|
+
type ListProductReviewsParams,
|
|
53
|
+
type ProductAutocompleteParams,
|
|
54
|
+
type ProductDetailParams,
|
|
55
|
+
type ProductsParamsInput,
|
|
56
|
+
type PutShippingMethodParams,
|
|
57
|
+
type RelatedProductsParams,
|
|
58
|
+
type RemoveCartItemParams,
|
|
59
|
+
type UpdateCartItemQuantityParams,
|
|
60
|
+
type UploadPaymentReceiptParams,
|
|
61
|
+
} from '@ecomiq/core'
|
|
62
|
+
import { z } from 'zod'
|
|
63
|
+
import { getStorefrontClient } from './client'
|
|
64
|
+
import {
|
|
65
|
+
loadActiveCart,
|
|
66
|
+
submitCartItemQuantity,
|
|
67
|
+
submitCartItemRemoval,
|
|
68
|
+
submitProductToCart,
|
|
69
|
+
type AddProductToCartInput,
|
|
70
|
+
} from './cart-session'
|
|
71
|
+
import { submitProductReview } from './review-submission'
|
|
72
|
+
|
|
73
|
+
const getSettingsFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
74
|
+
getStorefrontClient().getSettings(),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const getStoreIdFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
78
|
+
getStorefrontClient().getStoreId(),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const getCollectionsFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
82
|
+
getStorefrontClient().getCollections(),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const getCollectionBySlugFn = createServerFn({ method: 'GET' })
|
|
86
|
+
.validator(z.object({ slug: z.string().min(1) }))
|
|
87
|
+
.handler(({ data }) => getStorefrontClient().getCollectionBySlug(data.slug))
|
|
88
|
+
|
|
89
|
+
const getProductsFn = createServerFn({ method: 'GET' })
|
|
90
|
+
.validator(ProductsParamsSchema)
|
|
91
|
+
.handler(({ data }) => getStorefrontClient().getProducts(data))
|
|
92
|
+
|
|
93
|
+
const ProductBySlugServerParamsSchema = ProductDetailParamsSchema.extend({
|
|
94
|
+
slug: z.string().min(1),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const getProductBySlugFn = createServerFn({ method: 'GET' })
|
|
98
|
+
.validator(ProductBySlugServerParamsSchema)
|
|
99
|
+
.handler(({ data }) =>
|
|
100
|
+
getStorefrontClient().getProductBySlug(data.slug, { sellerId: data.sellerId }),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const getRelatedProductsFn = createServerFn({ method: 'GET' })
|
|
104
|
+
.validator(RelatedProductsParamsSchema)
|
|
105
|
+
.handler(({ data }) => getStorefrontClient().getRelatedProducts(data))
|
|
106
|
+
|
|
107
|
+
const autocompleteProductsFn = createServerFn({ method: 'GET' })
|
|
108
|
+
.validator(ProductAutocompleteParamsSchema)
|
|
109
|
+
.handler(({ data }) => getStorefrontClient().autocompleteProducts(data))
|
|
110
|
+
|
|
111
|
+
const listProductReviewsFn = createServerFn({ method: 'GET' })
|
|
112
|
+
.validator(ListProductReviewsParamsSchema)
|
|
113
|
+
.handler(({ data }) => getStorefrontClient().listProductReviews(data))
|
|
114
|
+
|
|
115
|
+
const submitProductReviewFn = createServerFn({ method: 'POST' })
|
|
116
|
+
.validator(CreateProductReviewParamsSchema)
|
|
117
|
+
.handler(({ data }) => submitProductReview(getStorefrontClient(), data))
|
|
118
|
+
|
|
119
|
+
const loadCartFn = createServerFn({ method: 'POST' })
|
|
120
|
+
.validator(GetCartParamsSchema)
|
|
121
|
+
.handler(({ data }) => loadActiveCart(getStorefrontClient(), data))
|
|
122
|
+
|
|
123
|
+
const setCartItemQuantityFn = createServerFn({ method: 'POST' })
|
|
124
|
+
.validator(UpdateCartItemQuantityParamsSchema)
|
|
125
|
+
.handler(({ data }) => submitCartItemQuantity(getStorefrontClient(), data))
|
|
126
|
+
|
|
127
|
+
const removeCartItemFn = createServerFn({ method: 'POST' })
|
|
128
|
+
.validator(RemoveCartItemParamsSchema)
|
|
129
|
+
.handler(({ data }) => submitCartItemRemoval(getStorefrontClient(), data))
|
|
130
|
+
|
|
131
|
+
const AddProductToCartServerSchema = z.object({
|
|
132
|
+
cartId: z.string().uuid().optional(),
|
|
133
|
+
currencyCode: z.string().length(3),
|
|
134
|
+
variantId: z.string().regex(/^[1-9]\d*$/),
|
|
135
|
+
quantity: z.number().int().positive(),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const addProductToCartFn = createServerFn({ method: 'POST' })
|
|
139
|
+
.validator(AddProductToCartServerSchema)
|
|
140
|
+
.handler(({ data }) => submitProductToCart(getStorefrontClient(), data))
|
|
141
|
+
|
|
142
|
+
const addCartItemFn = createServerFn({ method: 'POST' })
|
|
143
|
+
.validator(AddCartItemParamsSchema)
|
|
144
|
+
.handler(({ data }) => getStorefrontClient().addCartItem(data))
|
|
145
|
+
|
|
146
|
+
const getCartCheckoutFn = createServerFn({ method: 'GET' })
|
|
147
|
+
.validator(CartCheckoutReadParamsSchema)
|
|
148
|
+
.handler(({ data }) => getStorefrontClient().getCartCheckout(data))
|
|
149
|
+
|
|
150
|
+
const getShippingOptionsFn = createServerFn({ method: 'GET' })
|
|
151
|
+
.validator(GetShippingOptionsParamsSchema)
|
|
152
|
+
.handler(({ data }) => getStorefrontClient().getShippingOptions(data))
|
|
153
|
+
|
|
154
|
+
const putCartShippingMethodFn = createServerFn({ method: 'POST' })
|
|
155
|
+
.validator(PutShippingMethodParamsSchema)
|
|
156
|
+
.handler(({ data }) => getStorefrontClient().putCartShippingMethod(data))
|
|
157
|
+
|
|
158
|
+
const patchCartBuyerFn = createServerFn({ method: 'POST' })
|
|
159
|
+
.validator(CartBuyerParamsSchema)
|
|
160
|
+
.handler(({ data }) => getStorefrontClient().patchCartBuyer(data))
|
|
161
|
+
|
|
162
|
+
const patchCartAddressesFn = createServerFn({ method: 'POST' })
|
|
163
|
+
.validator(CartAddressesParamsSchema)
|
|
164
|
+
.handler(({ data }) => getStorefrontClient().patchCartAddresses(data))
|
|
165
|
+
|
|
166
|
+
const checkoutCartFn = createServerFn({ method: 'POST' })
|
|
167
|
+
.validator(CartCheckoutParamsSchema)
|
|
168
|
+
.handler(({ data }) => getStorefrontClient().checkoutCart(data))
|
|
169
|
+
|
|
170
|
+
const CartPaymentReceiptServerSchema = z.object({
|
|
171
|
+
cartId: z.string().uuid(),
|
|
172
|
+
fileBase64: z.string().min(1),
|
|
173
|
+
fileName: z.string().min(1),
|
|
174
|
+
mimeType: z.string().min(1).optional(),
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const uploadPaymentReceiptFn = createServerFn({ method: 'POST' })
|
|
178
|
+
.validator(CartPaymentReceiptServerSchema)
|
|
179
|
+
.handler(({ data }) => {
|
|
180
|
+
const binary = Uint8Array.from(atob(data.fileBase64), (c) => c.charCodeAt(0))
|
|
181
|
+
const file = new Blob([binary], { type: data.mimeType ?? 'application/octet-stream' })
|
|
182
|
+
return getStorefrontClient().uploadPaymentReceipt({
|
|
183
|
+
cartId: data.cartId,
|
|
184
|
+
file,
|
|
185
|
+
fileName: data.fileName,
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const addCartPromotionCodeFn = createServerFn({ method: 'POST' })
|
|
190
|
+
.validator(CartPromotionCodeParamsSchema)
|
|
191
|
+
.handler(({ data }) => getStorefrontClient().addCartPromotionCode(data))
|
|
192
|
+
|
|
193
|
+
const removeCartPromotionCodeFn = createServerFn({ method: 'POST' })
|
|
194
|
+
.validator(CartPromotionCodeParamsSchema)
|
|
195
|
+
.handler(({ data }) => getStorefrontClient().removeCartPromotionCode(data))
|
|
196
|
+
|
|
197
|
+
const getCountriesFn = createServerFn({ method: 'GET' }).handler(() =>
|
|
198
|
+
getStorefrontClient().getCountries(),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
const getDepartmentsFn = createServerFn({ method: 'GET' })
|
|
202
|
+
.validator(GetDepartmentsParamsSchema)
|
|
203
|
+
.handler(({ data }) => getStorefrontClient().getDepartments(data))
|
|
204
|
+
|
|
205
|
+
const getProvincesFn = createServerFn({ method: 'GET' })
|
|
206
|
+
.validator(GetProvincesParamsSchema)
|
|
207
|
+
.handler(({ data }) => getStorefrontClient().getProvinces(data))
|
|
208
|
+
|
|
209
|
+
const getDistrictsFn = createServerFn({ method: 'GET' })
|
|
210
|
+
.validator(GetDistrictsParamsSchema)
|
|
211
|
+
.handler(({ data }) => getStorefrontClient().getDistricts(data))
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Cliente storefront server-only. Misma superficie útil que `createClient()`,
|
|
215
|
+
* pero cada método es una server function reutilizable desde cualquier página.
|
|
216
|
+
*/
|
|
217
|
+
export const serverApi = {
|
|
218
|
+
getSettings: () => getSettingsFn(),
|
|
219
|
+
getStoreId: () => getStoreIdFn(),
|
|
220
|
+
getCollections: () => getCollectionsFn(),
|
|
221
|
+
getCollectionBySlug: (slug: string) => getCollectionBySlugFn({ data: { slug } }),
|
|
222
|
+
getProducts: (
|
|
223
|
+
params: ProductsParamsInput = {},
|
|
224
|
+
options: { signal?: AbortSignal } = {},
|
|
225
|
+
) => getProductsFn({ data: params, signal: options.signal }),
|
|
226
|
+
getProductBySlug: (slug: string, params: ProductDetailParams = {}) =>
|
|
227
|
+
getProductBySlugFn({ data: { slug, ...params } }),
|
|
228
|
+
getRelatedProducts: (params: RelatedProductsParams) =>
|
|
229
|
+
getRelatedProductsFn({ data: params }),
|
|
230
|
+
autocompleteProducts: (params: ProductAutocompleteParams) =>
|
|
231
|
+
autocompleteProductsFn({ data: params }),
|
|
232
|
+
listProductReviews: (params: ListProductReviewsParams) =>
|
|
233
|
+
listProductReviewsFn({ data: params }),
|
|
234
|
+
submitProductReview: (params: CreateProductReviewParams) =>
|
|
235
|
+
submitProductReviewFn({ data: params }),
|
|
236
|
+
loadCart: (params: GetCartParams) => loadCartFn({ data: params }),
|
|
237
|
+
setCartItemQuantity: (params: UpdateCartItemQuantityParams) =>
|
|
238
|
+
setCartItemQuantityFn({ data: params }),
|
|
239
|
+
removeCartItem: (params: RemoveCartItemParams) =>
|
|
240
|
+
removeCartItemFn({ data: params }),
|
|
241
|
+
addProductToCart: (params: AddProductToCartInput) =>
|
|
242
|
+
addProductToCartFn({ data: params }),
|
|
243
|
+
addCartItem: (params: AddCartItemParams) => addCartItemFn({ data: params }),
|
|
244
|
+
getCartCheckout: (params: CartCheckoutReadParams) =>
|
|
245
|
+
getCartCheckoutFn({ data: params }),
|
|
246
|
+
getShippingOptions: (params: GetShippingOptionsParams) =>
|
|
247
|
+
getShippingOptionsFn({ data: params }),
|
|
248
|
+
putCartShippingMethod: (params: PutShippingMethodParams) =>
|
|
249
|
+
putCartShippingMethodFn({ data: params }),
|
|
250
|
+
patchCartBuyer: (params: CartBuyerParams) => patchCartBuyerFn({ data: params }),
|
|
251
|
+
patchCartAddresses: (params: CartAddressesParams) =>
|
|
252
|
+
patchCartAddressesFn({ data: params }),
|
|
253
|
+
checkoutCart: (params: CartCheckoutParams) => checkoutCartFn({ data: params }),
|
|
254
|
+
uploadPaymentReceipt: async (params: UploadPaymentReceiptParams) => {
|
|
255
|
+
const buffer = await params.file.arrayBuffer()
|
|
256
|
+
const bytes = new Uint8Array(buffer)
|
|
257
|
+
let binary = ''
|
|
258
|
+
for (const byte of bytes) binary += String.fromCharCode(byte)
|
|
259
|
+
return uploadPaymentReceiptFn({
|
|
260
|
+
data: {
|
|
261
|
+
cartId: params.cartId,
|
|
262
|
+
fileBase64: btoa(binary),
|
|
263
|
+
fileName:
|
|
264
|
+
params.fileName ??
|
|
265
|
+
(params.file instanceof File ? params.file.name : 'receipt'),
|
|
266
|
+
mimeType: params.file.type || undefined,
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
},
|
|
270
|
+
addCartPromotionCode: (params: CartPromotionCodeParams) =>
|
|
271
|
+
addCartPromotionCodeFn({ data: params }),
|
|
272
|
+
removeCartPromotionCode: (params: CartPromotionCodeParams) =>
|
|
273
|
+
removeCartPromotionCodeFn({ data: params }),
|
|
274
|
+
getCountries: () => getCountriesFn(),
|
|
275
|
+
getDepartments: (params: GetDepartmentsParams) =>
|
|
276
|
+
getDepartmentsFn({ data: params }),
|
|
277
|
+
getProvinces: (params: GetProvincesParams) => getProvincesFn({ data: params }),
|
|
278
|
+
getDistricts: (params: GetDistrictsParams) => getDistrictsFn({ data: params }),
|
|
279
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Design system de Ecomiq — Tailwind v4 + tokens estilo shadcn.
|
|
2
|
+
* `source(...)` fija el scan para que client y SSR generen el mismo CSS/hash. */
|
|
3
|
+
@import 'tailwindcss' source('./');
|
|
4
|
+
|
|
5
|
+
/* Escanear los componentes UI del propio framework */
|
|
6
|
+
@source './ui';
|
|
7
|
+
@source './components';
|
|
8
|
+
@source './runtime';
|
|
9
|
+
@source './pages';
|
|
10
|
+
@source '../templates';
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--color-background: var(--background);
|
|
14
|
+
--color-foreground: var(--foreground);
|
|
15
|
+
--color-card: var(--card);
|
|
16
|
+
--color-card-foreground: var(--card-foreground);
|
|
17
|
+
--color-primary: var(--primary);
|
|
18
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
19
|
+
--color-secondary: var(--secondary);
|
|
20
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
21
|
+
--color-muted: var(--muted);
|
|
22
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
23
|
+
--color-accent: var(--accent);
|
|
24
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
25
|
+
--color-destructive: var(--destructive);
|
|
26
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
27
|
+
--color-border: var(--border);
|
|
28
|
+
--color-input: var(--input);
|
|
29
|
+
--color-ring: var(--ring);
|
|
30
|
+
--radius-lg: var(--radius);
|
|
31
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
32
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
:root {
|
|
36
|
+
--background: oklch(0.99 0.003 90);
|
|
37
|
+
--foreground: oklch(0.2 0.02 260);
|
|
38
|
+
--card: oklch(1 0 0);
|
|
39
|
+
--card-foreground: oklch(0.2 0.02 260);
|
|
40
|
+
--primary: oklch(0.45 0.15 165);
|
|
41
|
+
--primary-foreground: oklch(0.98 0.01 165);
|
|
42
|
+
--secondary: oklch(0.96 0.01 165);
|
|
43
|
+
--secondary-foreground: oklch(0.3 0.05 165);
|
|
44
|
+
--muted: oklch(0.96 0.005 90);
|
|
45
|
+
--muted-foreground: oklch(0.5 0.02 260);
|
|
46
|
+
--accent: oklch(0.94 0.03 165);
|
|
47
|
+
--accent-foreground: oklch(0.3 0.05 165);
|
|
48
|
+
--destructive: oklch(0.55 0.2 25);
|
|
49
|
+
--destructive-foreground: oklch(0.98 0.01 25);
|
|
50
|
+
--border: oklch(0.91 0.005 90);
|
|
51
|
+
--input: oklch(0.91 0.005 90);
|
|
52
|
+
--ring: oklch(0.45 0.15 165);
|
|
53
|
+
--radius: 0.625rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
* {
|
|
57
|
+
border-color: var(--border);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
::selection {
|
|
61
|
+
background: var(--accent);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
:focus-visible {
|
|
65
|
+
outline: 2px solid var(--ring);
|
|
66
|
+
outline-offset: 2px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (prefers-reduced-motion: reduce) {
|
|
70
|
+
*,
|
|
71
|
+
*::before,
|
|
72
|
+
*::after {
|
|
73
|
+
animation-duration: 0.01ms !important;
|
|
74
|
+
transition-duration: 0.01ms !important;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/ui/badge.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import { cn } from './cn'
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
11
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
12
|
+
outline: 'text-foreground',
|
|
13
|
+
destructive: 'border-transparent bg-destructive text-destructive-foreground',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
defaultVariants: { variant: 'default' },
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export interface BadgeProps
|
|
21
|
+
extends Omit<ComponentProps<'span'>, 'children'>,
|
|
22
|
+
VariantProps<typeof badgeVariants> {
|
|
23
|
+
children?: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Badge({ className, variant, children, ...props }: BadgeProps) {
|
|
27
|
+
return (
|
|
28
|
+
<span className={cn(badgeVariants({ variant }), className)} {...props}>
|
|
29
|
+
{children}
|
|
30
|
+
</span>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import { cn } from './cn'
|
|
4
|
+
|
|
5
|
+
export const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
11
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
12
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: 'h-9 px-4 py-2',
|
|
18
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
19
|
+
lg: 'h-10 rounded-md px-8',
|
|
20
|
+
icon: 'h-9 w-9',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: { variant: 'default', size: 'default' },
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export interface ButtonProps extends ComponentProps<'button'>, VariantProps<typeof buttonVariants> {}
|
|
28
|
+
|
|
29
|
+
export function Button({ className, variant, size, type = 'button', ...props }: ButtonProps) {
|
|
30
|
+
return <button type={type} className={cn(buttonVariants({ variant, size }), className)} {...props} />
|
|
31
|
+
}
|