Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Output/build/cache
.next
node_modules
coverage
build
dist

# Infra/large directories not needed for app linting
amplify
.amplify
public
template
scripts
lambda-edge-host-rewriter

# Optional: ignore plain JS assets under renderer engine
renderer-engine/liquid/**/*.js

35 changes: 34 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,38 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "always"
}
}
},
"files.watcherExclude": {
"**/.git/**": true,
"**/node_modules/**": true,
"**/.next/**": true,
"**/amplify/**": true,
"**/.amplify/**": true,
"**/public/**": true,
"**/template/**": true,
"**/coverage/**": true,
"**/dist/**": true,
"**/build/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/.next": true,
"**/amplify": true,
"**/.amplify": true,
"**/public": true,
"**/template": true,
"**/coverage": true,
"**/dist": true,
"**/build": true
},
"typescript.tsserver.maxTsServerMemory": 4096,
"typescript.tsserver.experimental.enableProjectDiagnostics": false,
"typescript.tsserver.watchOptions": {
"watchFile": "priorityPollingInterval",
"watchDirectory": "dynamicPriorityPolling",
"fallbackPolling": "dynamicPriorityPolling"
},
"eslint.workingDirectories": [
{ "mode": "auto" }
]
}
1 change: 0 additions & 1 deletion amplify/data/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ const schema = a
.authorization((allow) => [allow.authenticated()])
.handler(a.handler.function(createStoreTemplate)),

// Nueva mutación para procesar la configuración de pagos
processStorePaymentConfig: a
.mutation()
.arguments({ action: a.string(), input: PaymentConfigInput })
Expand Down
5 changes: 3 additions & 2 deletions app/api/stores/[storeId]/themes/confirm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,16 @@ async function processThemeInBackground(
settings: JSON.stringify(processedTheme.settings),
validation: JSON.stringify(themeData.validation),
analysis: JSON.stringify(themeData.analysis),
preview: themeData.theme.preview,
preview: storageResult.previewCdnUrl || themeData.theme.preview,
owner: username,
};

let savedThemeId: string | undefined = themeId;
if (themeId) {
const { owner: _omitOwner, ...updatePayload } = themeRecord as any;
const { data: updated, errors: updateErrors } = await cookiesClient.models.UserTheme.update({
id: themeId,
...themeRecord,
...updatePayload,
} as any);
if (updateErrors) {
logger.error('Failed to update placeholder theme', { processId, errors: updateErrors }, 'ThemeConfirmAPI');
Expand Down
1 change: 1 addition & 0 deletions app/api/stores/[storeId]/themes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
version: theme.version,
author: theme.author,
description: theme.description,
previewUrl: theme.preview,
fileCount: theme.fileCount,
totalSize: theme.totalSize,
isActive: theme.isActive,
Expand Down
5 changes: 3 additions & 2 deletions app/api/stores/[storeId]/themes/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
processedTheme.validation = validation;
processedTheme.analysis = validation.analysis as TemplateAnalysis;

// 7. Generar preview si la validación es exitosa
if (validation.isValid) {
// 7. Generar preview solo si no existe y la validación es exitosa
if (validation.isValid && !processedTheme.preview) {
processedTheme.preview = await processor.generatePreview(processedTheme);
}

Expand Down Expand Up @@ -123,6 +123,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
settings: processedTheme.settings,
validation: processedTheme.validation,
analysis: processedTheme.analysis,
previewUrl: processedTheme.preview,
},
validation: {
isValid: validation.isValid,
Expand Down
37 changes: 36 additions & 1 deletion app/api/stores/template/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ export async function POST(request: NextRequest) {
}
}

// Resolver preview URL buscando un asset conocido o usando la del schema
const previewUrl = findPreviewUrlFromTemplateUrls(templateUrls) || themeInfo.previewUrl || null

// 7. Crear registro del tema en la DB con la información validada
try {
const s3FolderKey = `templates/${storeId}`
Expand All @@ -166,7 +169,7 @@ export async function POST(request: NextRequest) {
}),
validation: JSON.stringify(validation),
analysis: JSON.stringify(validation.analysis || {}),
preview: templateUrls['layout/theme.liquid'] || null,
preview: previewUrl,
owner: user.username,
}

Expand Down Expand Up @@ -298,3 +301,35 @@ function generateTemplateUrls(
function sanitizeMetadataValue(value: string): string {
return value.replace(/[^\x00-\x7F]/g, '')
}

// Busca una URL de preview dentro de los archivos copiados del template
function findPreviewUrlFromTemplateUrls(urls: Record<string, string>): string | undefined {
const candidates = [
'assets/preview.png',
'assets/preview.jpg',
'assets/preview.webp',
'assets/screenshot.png',
'assets/screenshot.jpg',
'assets/screenshot.webp',
'preview.png',
'preview.jpg',
'preview.webp',
'screenshot.png',
'screenshot.jpg',
'screenshot.webp',
]

// Intentar coincidencia exacta por clave
for (const name of candidates) {
if (urls[name]) return urls[name]
}

// Si no está exacto, buscar por sufijo en claves (por si vienen en subcarpetas)
const keys = Object.keys(urls)
for (const key of keys) {
if (candidates.some((c) => key.endsWith('/' + c) || key.toLowerCase().endsWith('/' + c))) {
return urls[key]
}
}
return undefined
}
59 changes: 50 additions & 9 deletions app/store/components/store-config/components/ThemePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { LogoUploader } from '@/app/store/components/store-config/components/LogoUploader';
import { ThemeUploader } from '@/app/store/components/store-config/components/ThemeUploader';
import { ThemeList } from '@/app/store/components/theme-management/components/ThemeList';
import { useThemeList } from '@/app/store/components/theme-management/hooks/useThemeList';
import useStoreDataStore from '@/context/core/storeDataStore';
import { Badge, BlockStack, Button, ButtonGroup, Card, Layout, MediaCard, Page, Tabs, Text } from '@shopify/polaris';
import {
Badge,
BlockStack,
Button,
ButtonGroup,
Card,
Layout,
MediaCard,
Page,
Spinner,
Tabs,
Text,
} from '@shopify/polaris';
import { MoneyFilledIcon } from '@shopify/polaris-icons';
import Image from 'next/image';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';

export function ThemePreview() {
const { currentStore } = useStoreDataStore();
const customDomain = currentStore?.defaultDomain || '';
const [selectedTab, setSelectedTab] = useState(0);
const storeId = currentStore?.storeId || '';
const { themes, loading } = useThemeList(storeId);

// Encontrar el tema activo y su preview URL (HTTPS o data URI si aún no se propaga)
const activeTheme = useMemo(() => themes.find((t: any) => t.isActive) || themes[0], [themes]);
const activePreviewUrl = activeTheme?.previewUrl;
const isLoadingPreview = !storeId || loading || themes.length === 0;

const viewStore = `https://${customDomain}`;

Expand Down Expand Up @@ -40,19 +60,40 @@ export function ThemePreview() {
Tema Actual
</Text>
<div style={{ height: '280px', position: 'relative', overflow: 'hidden' }}>
<Image
src="https://images.unsplash.com/photo-1741482529153-a98d81235d06?q=80&w=2070&auto=format&fit=crop"
alt="Amazonas theme preview"
fill
style={{ objectFit: 'cover' }}
/>
{isLoadingPreview ? (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--p-color-bg-subdued)',
}}>
<Spinner accessibilityLabel="Cargando vista previa del tema" size="large" />
</div>
) : activePreviewUrl ? (
<Image
src={activePreviewUrl}
alt={`Vista previa de ${activeTheme?.name || 'tema'}`}
fill
style={{ objectFit: 'cover' }}
/>
) : (
<Image
src="https://images.unsplash.com/photo-1741482529153-a98d81235d06?q=80&w=2070&auto=format&fit=crop"
alt="Theme preview placeholder"
fill
style={{ objectFit: 'cover' }}
/>
)}
</div>
<BlockStack gap="200">
<Layout>
<Layout.Section>
<BlockStack align="center">
<Text variant="headingMd" as="h2">
Amazonas
{activeTheme?.name || 'Tema'}
</Text>
</BlockStack>
<Badge tone="success" size="small">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface Theme {
totalSize: number;
createdAt: string;
updatedAt: string;
previewUrl?: string;
}

interface UseThemeListReturn {
Expand Down
84 changes: 84 additions & 0 deletions docs/engine/theme-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Guía: Cómo definir la vista previa (preview) de tu tema

Esta guía explica cómo los desarrolladores de temas deben proveer la imagen de vista previa para que Fasttify la detecte automáticamente, la publique en CDN y se muestre en el panel de administración.

## Opción recomendada: archivo dentro del ZIP del tema

Coloca una imagen de preview dentro de tu tema antes de comprimirlo. Rutas soportadas (se detectan en cualquier subcarpeta del tema):

- assets/preview.png
- assets/preview.jpg
- assets/preview.webp
- assets/screenshot.png
- assets/screenshot.jpg
- assets/screenshot.webp
- preview.png / preview.jpg / preview.webp
- screenshot.png / screenshot.jpg / screenshot.webp

Ejemplo de estructura:

```
my-theme/
layout/theme.liquid
templates/index.json
sections/header.liquid
assets/preview.png ← recomendado
config/settings_schema.json
...
```

Al subir el ZIP:

- El sistema sube tus archivos a `templates/{storeId}/...` en S3/CloudFront.
- Genera `metadata.json` con `previewUrl` apuntando al archivo (CDN).
- El panel mostrará esa URL en la lista de temas y en la vista de “Diseño”.

## Opción alternativa: URL en settings_schema.json

Si no incluyes archivo, puedes declarar una URL en `config/settings_schema.json`. Se lee desde la sección `theme_info`:

```json
[
{
"name": "theme_info",
"theme_name": "Mi Tema",
"theme_author": "Mi Estudio",
"theme_version": "1.0.0",
"preview_url": "https://cdn.mi-cuenta.com/previews/mi-tema.png"
}
]
```

Notas:

- Se admiten `preview_url` o `previewUrl`.
- Si existe un archivo de preview en el ZIP, ese tiene prioridad para la URL de CDN.

## Recomendaciones de tamaño y formato

- Formato: PNG o WEBP (recomendado WEBP por peso).
- Dimensiones sugeridas: 1600×900 o 1920×1080.
- Peso recomendado: < 500 KB (el validador marca warning si las imágenes son muy pesadas).
- Evita metadatos EXIF innecesarios y usa compresión.

## Comportamiento del sistema

- Upload de tema: se detecta el preview y se publica en CDN.
- Confirmación/almacenamiento: se escribe `metadata.json` con `previewUrl`.
- Panel (lista de temas y “Diseño”): consume `previewUrl` y muestra un loader mientras carga.
- Si inicialmente ves un data URI, espera 30–60 s mientras finaliza la escritura de metadata/propagación CDN.

## Troubleshooting

- No veo la preview:
- Verifica que la ruta del archivo sea una de las soportadas y que el archivo exista en el ZIP.
- Asegura extensiones .png/.jpg/.webp.
- Revisa `templates/{storeId}/metadata.json` y confirma que tenga `previewUrl`.
- Espera ~1–2 minutos por propagación de CDN si es la primera carga.
- El validador advierte por tamaño de imagen:
- Optimiza la imagen (usa WEBP, reduce dimensiones o compresión) para mejorar rendimiento.

## Preguntas frecuentes

- ¿Puedo usar una URL externa? Sí, con `preview_url` en `settings_schema.json`. Sin embargo, recomendamos empaquetar el preview en `assets/` para un flujo totalmente integrado con nuestro CDN.
- ¿La preview es obligatoria? No, pero es altamente recomendada para una buena UX en el panel.
27 changes: 13 additions & 14 deletions renderer-engine/liquid/filters.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// Re-exportar todos los filtros desde sus módulos específicos
export { baseFilters } from '@/renderer-engine/liquid/filters/base-filters';
export { cartFilters } from '@/renderer-engine/liquid/filters/cart-filters';
export { dataAccessFilters } from '@/renderer-engine/liquid/filters/data-access-filters';
export { ecommerceFilters } from '@/renderer-engine/liquid/filters/ecommerce-filters';
export { htmlFilters } from '@/renderer-engine/liquid/filters/html-filters';
export { moneyFilters } from '@/renderer-engine/liquid/filters/money-filters';
export { baseFilters } from './filters/base-filters';
export { cartFilters } from './filters/cart-filters';
export { dataAccessFilters } from './filters/data-access-filters';
export { ecommerceFilters } from './filters/ecommerce-filters';
export { htmlFilters } from './filters/html-filters';
export { moneyFilters } from './filters/money-filters';

// Importar todos los filtros para el array principal
import { baseFilters } from '@/renderer-engine/liquid/filters/base-filters';
import { cartFilters } from '@/renderer-engine/liquid/filters/cart-filters';
import { dataAccessFilters } from '@/renderer-engine/liquid/filters/data-access-filters';
import { ecommerceFilters } from '@/renderer-engine/liquid/filters/ecommerce-filters';
import { htmlFilters } from '@/renderer-engine/liquid/filters/html-filters';
import { moneyFilters } from '@/renderer-engine/liquid/filters/money-filters';
import { baseFilters } from './filters/base-filters';
import { cartFilters } from './filters/cart-filters';
import { dataAccessFilters } from './filters/data-access-filters';
import { ecommerceFilters } from './filters/ecommerce-filters';
import { htmlFilters } from './filters/html-filters';
import { moneyFilters } from './filters/money-filters';

import type { LiquidFilter } from '@/renderer-engine/types';
import type { LiquidFilter } from '../types';

/**
* Array con todos los filtros para registrar en el motor Liquid
Expand All @@ -28,5 +28,4 @@ export const allFilters: LiquidFilter[] = [
...dataAccessFilters,
];

// Mantener compatibilidad hacia atrás
export const ecommerceFiltersLegacy = allFilters;
1 change: 1 addition & 0 deletions renderer-engine/services/themes/core/theme-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export class ThemeProcessor {
license: themeInfo.license,
settings_schema: themeInfo.settings_schema || [],
settings_defaults: themeInfo.settings_defaults || {},
previewUrl: themeInfo.previewUrl,
};
} catch (error) {
throw new Error(`Failed to parse theme settings: ${error instanceof Error ? error.message : 'Unknown error'}`);
Expand Down
Loading