From adff71c7e821c74ae43f6bcb4d3e1b774b9d0765 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 29 Oct 2025 11:11:03 -0500 Subject: [PATCH 01/49] Refactor image handling in content components to use ContentThumbnail This commit replaces the usage of the Thumbnail component with the new ContentThumbnail component across multiple content-related components, including ContentDetailsModal, ContentCardMobile, ContentTableRow, and ImageCard. This change enhances consistency in image rendering and improves maintainability by centralizing image display logic. --- .../details/ContentDetailsModal.tsx | 5 +- .../components/listing/ContentCardMobile.tsx | 5 +- .../components/listing/ContentTableRow.tsx | 6 +- .../components/media/ContentThumbnail.tsx | 161 ++++++++++++++++++ .../images-selector/components/ImageCard.tsx | 15 +- 5 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 app/store/components/content/components/media/ContentThumbnail.tsx diff --git a/app/store/components/content/components/details/ContentDetailsModal.tsx b/app/store/components/content/components/details/ContentDetailsModal.tsx index 89db92b4..f6e0c3e6 100644 --- a/app/store/components/content/components/details/ContentDetailsModal.tsx +++ b/app/store/components/content/components/details/ContentDetailsModal.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Modal, Thumbnail, Text } from '@shopify/polaris'; +import { Modal, Text } from '@shopify/polaris'; import type { S3Image } from '@/app/store/components/images-selector/types/s3-types'; import { formatFileSize, formatDate, getFileName } from '@/app/store/components/content/utils/content-utils'; +import { ContentThumbnail } from '@/app/store/components/content/components/media/ContentThumbnail'; interface ContentDetailsModalProps { image: S3Image | null; @@ -40,7 +41,7 @@ export function ContentDetailsModal({ image, open, onClose, onDelete }: ContentD
- +
{getFileName(image.filename)} diff --git a/app/store/components/content/components/listing/ContentCardMobile.tsx b/app/store/components/content/components/listing/ContentCardMobile.tsx index b7a0ddec..a946cdad 100644 --- a/app/store/components/content/components/listing/ContentCardMobile.tsx +++ b/app/store/components/content/components/listing/ContentCardMobile.tsx @@ -1,10 +1,11 @@ 'use client'; -import { Card, Thumbnail, Text, Button } from '@shopify/polaris'; +import { Card, Text, Button } from '@shopify/polaris'; import { DeleteIcon, ViewIcon } from '@shopify/polaris-icons'; import type { S3Image } from '@/app/store/components/images-selector/types/s3-types'; import { formatFileSize, formatDate, getFileName } from '@/app/store/components/content/utils/content-utils'; import type { VisibleColumns } from '@/app/store/components/content/types/content-types'; +import { ContentThumbnail } from '../media/ContentThumbnail'; interface ContentCardMobileProps { images: S3Image[]; @@ -29,7 +30,7 @@ export function ContentCardMobile({ {images.map((image) => (
- +
diff --git a/app/store/components/content/components/listing/ContentTableRow.tsx b/app/store/components/content/components/listing/ContentTableRow.tsx index 86d4fd24..f4943daa 100644 --- a/app/store/components/content/components/listing/ContentTableRow.tsx +++ b/app/store/components/content/components/listing/ContentTableRow.tsx @@ -5,7 +5,7 @@ import { ViewIcon, LinkIcon } from '@shopify/polaris-icons'; import type { S3Image } from '@/app/store/components/images-selector/types/s3-types'; import { formatFileSize, formatDate } from '@/app/store/components/content/utils/content-utils'; import type { VisibleColumns } from '@/app/store/components/content/types/content-types'; -import Image from 'next/image'; +import { ContentThumbnail } from '@/app/store/components/content/components/media/ContentThumbnail'; interface ContentTableRowProps { image: S3Image; @@ -31,9 +31,7 @@ export function ContentTableRow({ position={index}>
-
- {image.filename} -
+
{image.filename.split('.')[0] || image.filename} diff --git a/app/store/components/content/components/media/ContentThumbnail.tsx b/app/store/components/content/components/media/ContentThumbnail.tsx new file mode 100644 index 00000000..b6bafbbf --- /dev/null +++ b/app/store/components/content/components/media/ContentThumbnail.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { Thumbnail, Icon } from '@shopify/polaris'; +import { PlayCircleIcon } from '@shopify/polaris-icons'; +import { useMemo, useState, useEffect, useRef } from 'react'; +import { isVideoFile, isImageFile } from '@/lib/utils/file-utils'; +import Image from 'next/image'; + +interface ContentThumbnailProps { + src: string; + alt: string; + size?: 'small' | 'medium' | 'large'; + className?: string; + style?: React.CSSProperties; +} + +export function ContentThumbnail({ src, alt, size = 'small', className, style }: ContentThumbnailProps) { + const isVideo = useMemo(() => isVideoFile(src), [src]); + const isImage = useMemo(() => isImageFile(src), [src]); + const [videoThumbnail, setVideoThumbnail] = useState(null); + const videoRef = useRef(null); + + useEffect(() => { + if (!isVideo || !src) return; + + const video = document.createElement('video'); + video.crossOrigin = 'anonymous'; + video.src = src; + video.muted = true; + videoRef.current = video; + + const captureFrame = () => { + try { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) return; + + canvas.width = video.videoWidth || 800; + canvas.height = video.videoHeight || 600; + + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnail = canvas.toDataURL('image/jpeg', 0.8); + setVideoThumbnail(thumbnail); + } catch (error) { + console.error('Error getting video thumbnail:', error); + } + }; + + const handleLoadedMetadata = () => { + video.currentTime = Math.min(1, video.duration / 2); + }; + + video.addEventListener('loadedmetadata', handleLoadedMetadata); + video.addEventListener('seeked', captureFrame); + + return () => { + video.removeEventListener('loadedmetadata', handleLoadedMetadata); + video.removeEventListener('seeked', captureFrame); + video.remove(); + videoRef.current = null; + }; + }, [isVideo, src]); + + if (isImage) { + const dimensionMap = { + small: 40, + medium: 80, + large: 120, + }; + + const dimensions = dimensionMap[size]; + + return ( +
+ {alt} +
+ ); + } + + if (isVideo) { + const dimensionMap = { + small: 40, + medium: 80, + large: 120, + }; + + const dimensions = dimensionMap[size]; + + return ( +
+ {/* Thumbnail del video si está disponible */} + {videoThumbnail ? ( + {alt} + ) : ( + /* Icono de video centrado mientras carga */ +
+ +
+ )} + + {/* Overlay de play centrado si hay thumbnail */} + {videoThumbnail && ( +
+ +
+ )} +
+ ); + } + + return ; +} diff --git a/app/store/components/images-selector/components/ImageCard.tsx b/app/store/components/images-selector/components/ImageCard.tsx index 83d47fa5..8ff3206c 100644 --- a/app/store/components/images-selector/components/ImageCard.tsx +++ b/app/store/components/images-selector/components/ImageCard.tsx @@ -2,7 +2,7 @@ import { useCallback, memo, useState } from 'react'; import type { S3Image } from '@/app/store/hooks/storage/useS3Images'; import { Checkbox } from '@shopify/polaris'; import { DeleteIcon } from '@shopify/polaris-icons'; -import Image from 'next/image'; +import { ContentThumbnail } from '@/app/store/components/content/components/media/ContentThumbnail'; const CARD_BG_COLOR = '#FFFFFF'; const HOVER_OVERLAY_COLOR = 'rgba(0, 0, 0, 0.05)'; @@ -168,18 +168,7 @@ const ImageCard = memo(function ImageCard({
)} - {image.filename} +
{/* Información del archivo */} From 2fc08a5a8ae73e7cd9ebb5c886d2ac3d02645880 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 29 Oct 2025 14:53:55 -0500 Subject: [PATCH 02/49] Update package.json and pnpm configuration to include theme-studio package and upgrade pnpm version This commit adds the new theme-studio package to the workspace in package.json, pnpm-lock.yaml, and pnpm-workspace.yaml. It also updates the pnpm version from 10.18.0 to 10.20.0. Additionally, it modifies tsconfig.base.json to include path mappings for the theme-studio package and updates StoreLayoutClient.tsx to account for new routing requirements. --- app/store/[slug]/studio/page.tsx | 12 ++ app/store/[slug]/studio/studioClient.tsx | 13 ++ app/store/layout/StoreLayoutClient.tsx | 5 +- package.json | 6 +- packages/theme-studio/README.md | 73 ++++++++++ packages/theme-studio/package.json | 37 +++++ .../src/application/use-cases/index.ts | 18 +++ .../use-cases/load-template.use-case.ts | 29 ++++ .../use-cases/render-preview.use-case.ts | 33 +++++ .../theme-studio/src/domain/entities/index.ts | 24 ++++ .../src/domain/entities/section.entity.ts | 99 +++++++++++++ .../src/domain/entities/template.entity.ts | 53 +++++++ .../src/domain/entities/theme.entity.ts | 31 +++++ .../theme-studio/src/domain/ports/index.ts | 20 +++ .../src/domain/ports/preview-renderer.port.ts | 40 ++++++ .../domain/ports/section-repository.port.ts | 32 +++++ .../domain/ports/template-repository.port.ts | 47 +++++++ .../src/domain/ports/theme-repository.port.ts | 29 ++++ packages/theme-studio/src/index.ts | 10 ++ .../adapters/preview-renderer.adapter.ts | 54 ++++++++ .../adapters/section-repository.adapter.ts | 56 ++++++++ .../adapters/template-repository.adapter.ts | 82 +++++++++++ .../src/infrastructure/services/.gitkeep | 0 .../presentation/components/ThemeStudio.tsx | 51 +++++++ .../components/header/EditorHeader.tsx | 131 ++++++++++++++++++ .../components/preview/PreviewPane.tsx | 59 ++++++++ .../components/settings/SettingsPane.tsx | 35 +++++ .../components/sidebar/Sidebar.tsx | 36 +++++ .../src/presentation/hooks/useThemeStudio.ts | 13 ++ .../src/presentation/stores/studioStore.ts | 14 ++ packages/theme-studio/tsconfig.json | 17 +++ pnpm-lock.yaml | 37 +++++ pnpm-workspace.yaml | 1 + tsconfig.base.json | 2 + 34 files changed, 1196 insertions(+), 3 deletions(-) create mode 100644 app/store/[slug]/studio/page.tsx create mode 100644 app/store/[slug]/studio/studioClient.tsx create mode 100644 packages/theme-studio/README.md create mode 100644 packages/theme-studio/package.json create mode 100644 packages/theme-studio/src/application/use-cases/index.ts create mode 100644 packages/theme-studio/src/application/use-cases/load-template.use-case.ts create mode 100644 packages/theme-studio/src/application/use-cases/render-preview.use-case.ts create mode 100644 packages/theme-studio/src/domain/entities/index.ts create mode 100644 packages/theme-studio/src/domain/entities/section.entity.ts create mode 100644 packages/theme-studio/src/domain/entities/template.entity.ts create mode 100644 packages/theme-studio/src/domain/entities/theme.entity.ts create mode 100644 packages/theme-studio/src/domain/ports/index.ts create mode 100644 packages/theme-studio/src/domain/ports/preview-renderer.port.ts create mode 100644 packages/theme-studio/src/domain/ports/section-repository.port.ts create mode 100644 packages/theme-studio/src/domain/ports/template-repository.port.ts create mode 100644 packages/theme-studio/src/domain/ports/theme-repository.port.ts create mode 100644 packages/theme-studio/src/index.ts create mode 100644 packages/theme-studio/src/infrastructure/adapters/preview-renderer.adapter.ts create mode 100644 packages/theme-studio/src/infrastructure/adapters/section-repository.adapter.ts create mode 100644 packages/theme-studio/src/infrastructure/adapters/template-repository.adapter.ts create mode 100644 packages/theme-studio/src/infrastructure/services/.gitkeep create mode 100644 packages/theme-studio/src/presentation/components/ThemeStudio.tsx create mode 100644 packages/theme-studio/src/presentation/components/header/EditorHeader.tsx create mode 100644 packages/theme-studio/src/presentation/components/preview/PreviewPane.tsx create mode 100644 packages/theme-studio/src/presentation/components/settings/SettingsPane.tsx create mode 100644 packages/theme-studio/src/presentation/components/sidebar/Sidebar.tsx create mode 100644 packages/theme-studio/src/presentation/hooks/useThemeStudio.ts create mode 100644 packages/theme-studio/src/presentation/stores/studioStore.ts create mode 100644 packages/theme-studio/tsconfig.json diff --git a/app/store/[slug]/studio/page.tsx b/app/store/[slug]/studio/page.tsx new file mode 100644 index 00000000..dc35801d --- /dev/null +++ b/app/store/[slug]/studio/page.tsx @@ -0,0 +1,12 @@ +import StudioClient from './studioClient'; + +export async function generateMetadata({ params: _params }: { params: Promise<{ slug: string }> }) { + return { + title: 'Theme Studio', + description: 'Editor visual de temas', + }; +} + +export default async function Page({ params: _params }: { params: Promise<{ slug: string }> }) { + return ; +} diff --git a/app/store/[slug]/studio/studioClient.tsx b/app/store/[slug]/studio/studioClient.tsx new file mode 100644 index 00000000..82d55824 --- /dev/null +++ b/app/store/[slug]/studio/studioClient.tsx @@ -0,0 +1,13 @@ +'use client'; + +import { useParams, usePathname } from 'next/navigation'; +import { getStoreId } from '@/utils/client/store-utils'; +import { ThemeStudio } from '@fasttify/theme-studio'; + +export default function StudioClient() { + const pathname = usePathname(); + const params = useParams(); + const storeId = getStoreId(params, pathname); + + return ; +} diff --git a/app/store/layout/StoreLayoutClient.tsx b/app/store/layout/StoreLayoutClient.tsx index 5e5e9d4c..c7f0a7a0 100644 --- a/app/store/layout/StoreLayoutClient.tsx +++ b/app/store/layout/StoreLayoutClient.tsx @@ -34,7 +34,10 @@ export const StoreLayoutClient = ({ children }: { children: React.ReactNode }) = useStore(storeId); const hideSidebar = - pathname.includes('/editor') || pathname.includes('/profile') || pathname.includes('/suscribe/select-plan'); + pathname.includes('/editor') || + pathname.includes('/profile') || + pathname.includes('/suscribe/select-plan') || + pathname.includes('/studio'); const isCheckoutPage = pathname.includes('/access_account/checkout'); const isSelectPlanPage = pathname.includes('/suscribe/select-plan'); diff --git a/package.json b/package.json index cba8ffb6..167e9dff 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "packages/liquid-forge", "packages/orders-app", "packages/theme-editor", - "packages/tenant-domains" + "packages/tenant-domains", + "packages/theme-studio" ], - "packageManager": "pnpm@10.18.0", + "packageManager": "pnpm@10.20.0", "engines": { "node": ">=20.18.3", "pnpm": ">=10.18.0" @@ -73,6 +74,7 @@ "@fasttify/orders-app": "workspace:*", "@fasttify/tenant-domains": "workspace:*", "@fasttify/theme-editor": "workspace:*", + "@fasttify/theme-studio": "workspace:*", "@hookform/resolvers": "3.3.4", "@next/bundle-analyzer": "16.0.0", "@polar-sh/nextjs": "^0.4.9", diff --git a/packages/theme-studio/README.md b/packages/theme-studio/README.md new file mode 100644 index 00000000..18580daa --- /dev/null +++ b/packages/theme-studio/README.md @@ -0,0 +1,73 @@ +# Theme Studio + +Editor visual de temas para Fasttify siguiendo Clean Architecture. + +## Estructura + +Este paquete sigue los principios de Clean Architecture: + +``` +theme-studio/ +├── domain/ # Capa de Dominio (más interna) +│ ├── entities/ # Entidades de negocio puras +│ └── ports/ # Interfaces (puertos) - contratos +├── application/ # Capa de Aplicación +│ └── use-cases/ # Casos de uso - lógica de negocio +├── infrastructure/ # Capa de Infraestructura +│ ├── adapters/ # Implementaciones concretas +│ └── services/ # Servicios externos (API, storage) +├── presentation/ # Capa de Presentación (más externa) +│ ├── components/ # Componentes React +│ ├── hooks/ # React hooks +│ └── stores/ # Estado global (Zustand) +└── shared/ # Código compartido entre capas + └── types/ # Tipos TypeScript compartidos +``` + +## Flujo de Dependencias + +Las dependencias fluyen **hacia adentro**: + +- **presentation** → **application** → **domain** +- **infrastructure** → **application** → **domain** + +**Regla de oro**: Ninguna capa interna conoce las capas externas. + +## Responsabilidades por Capa + +### Domain (Entidades y Puertos) + +- Define **qué** se necesita hacer +- Interfaces y contratos (puertos) +- Entidades de negocio puras (sin lógica de framework) +- **Sin dependencias externas** (solo TypeScript puro) + +### Application (Casos de Uso) + +- Define **cómo** se hacen las cosas +- Orquesta la lógica de negocio +- Depende solo de **puertos** (interfaces) del dominio +- Independiente de implementaciones concretas + +### Infrastructure (Adaptadores y Servicios) + +- **Implementa** las interfaces del dominio +- Acceso a datos (API, S3, etc.) +- Servicios externos +- Depende de puertos, no de casos de uso directamente + +### Presentation (Componentes y UI) + +- Expone la funcionalidad al usuario +- Componentes React con Polaris +- Hooks personalizados +- Estado global (Zustand) +- Depende de casos de uso + +## Tecnologías + +- **UI**: Shopify Polaris +- **Drag & Drop**: @shopify/draggable +- **Estado**: Zustand +- **Requests**: TanStack Query +- **Framework**: React + Next.js diff --git a/packages/theme-studio/package.json b/packages/theme-studio/package.json new file mode 100644 index 00000000..05582db9 --- /dev/null +++ b/packages/theme-studio/package.json @@ -0,0 +1,37 @@ +{ + "name": "@fasttify/theme-studio", + "version": "1.0.0", + "description": "Visual theme editor for Fasttify", + "main": "src/index.ts", + "types": "src/index.ts", + "private": true, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@shopify/draggable": "^1.2.1", + "@shopify/polaris": "^13.9.5", + "@shopify/polaris-icons": "^9.3.1", + "@tanstack/react-query": "^5.82.0", + "zustand": "^5.0.6" + }, + "peerDependencies": { + "react": "^18.3.1 || ^19.0.0", + "react-dom": "^18.3.1 || ^19.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.1 || ^19.2.2", + "@types/react-dom": "^18.3.1 || ^19.2.2", + "typescript": "^5.8.3" + }, + "keywords": ["theme", "editor", "visual", "polaris", "fasttify"], + "author": "Fasttify Team", + "license": "Apache-2.0" +} + + diff --git a/packages/theme-studio/src/application/use-cases/index.ts b/packages/theme-studio/src/application/use-cases/index.ts new file mode 100644 index 00000000..a0a166c0 --- /dev/null +++ b/packages/theme-studio/src/application/use-cases/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { LoadTemplateUseCase } from './load-template.use-case'; +export { RenderPreviewUseCase } from './render-preview.use-case'; diff --git a/packages/theme-studio/src/application/use-cases/load-template.use-case.ts b/packages/theme-studio/src/application/use-cases/load-template.use-case.ts new file mode 100644 index 00000000..2f0d8823 --- /dev/null +++ b/packages/theme-studio/src/application/use-cases/load-template.use-case.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2025 Fasttify LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Template, TemplateType } from '../../domain/entities/template.entity'; +import type { ITemplateRepository } from '../../domain/ports/template-repository.port'; + +/** + * Caso de uso: Cargar Template por tipo + */ +export class LoadTemplateUseCase { + constructor(private readonly templateRepo: ITemplateRepository) {} + + async execute(storeId: string, templateType: TemplateType): Promise