diff --git a/amplify/data/models/user-store.ts b/amplify/data/models/user-store.ts index 56097b9f..7a99a40a 100644 --- a/amplify/data/models/user-store.ts +++ b/amplify/data/models/user-store.ts @@ -7,7 +7,6 @@ export const userStoreModel = a .required() .authorization((allow) => [ allow.ownerDefinedIn('userId').to(['create', 'read', 'delete']), - allow.authenticated().to(['create', 'read']), allow.publicApiKey().to(['read']), ]), storeId: a @@ -15,7 +14,6 @@ export const userStoreModel = a .required() .authorization((allow) => [ allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']), - allow.authenticated().to(['create', 'read']), allow.publicApiKey().to(['read']), ]), storeName: a @@ -23,7 +21,6 @@ export const userStoreModel = a .required() .authorization((allow) => [ allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']), - allow.authenticated().to(['create', 'read', 'update']), allow.publicApiKey().to(['read']), ]), storeDescription: a.string(), @@ -35,7 +32,12 @@ export const userStoreModel = a currencyLocale: a.string(), currencyDecimalPlaces: a.integer(), storeType: a.string(), - storeStatus: a.boolean(), + storeStatus: a + .boolean() + .authorization((allow) => [ + allow.ownerDefinedIn('userId').to(['create', 'read']), + allow.publicApiKey().to(['read', 'update']), + ]), storeAdress: a.string(), contactEmail: a.string(), contactPhone: a.string(), @@ -45,7 +47,6 @@ export const userStoreModel = a .required() .authorization((allow) => [ allow.ownerDefinedIn('userId').to(['create', 'read', 'update', 'delete']), - allow.authenticated().to(['create', 'read', 'update']), allow.publicApiKey().to(['read']), ]), onboardingData: a.json(), @@ -67,7 +68,6 @@ export const userStoreModel = a .identifier(['storeId']) .secondaryIndexes((index) => [index('userId'), index('storeName'), index('defaultDomain')]) .authorization((allow) => [ - allow.authenticated().to(['read', 'update', 'delete', 'create']), allow.publicApiKey().to(['read']), allow.ownerDefinedIn('userId').to(['read', 'update', 'delete', 'create']), ]); diff --git a/app/(setup)/layout.tsx b/app/(setup)/layout.tsx index 32b8cc09..c6150b99 100644 --- a/app/(setup)/layout.tsx +++ b/app/(setup)/layout.tsx @@ -2,6 +2,8 @@ import { inter } from '@/lib/fonts'; import { useAuthInitializer } from '@/hooks/auth/useAuthInitializer'; +import { AppProvider } from '@shopify/polaris'; +import '@shopify/polaris/build/esm/styles.css'; import '@/app/global.css'; export default function WithoutNavbarLayout({ children }: { children: React.ReactNode }) { @@ -9,7 +11,33 @@ export default function WithoutNavbarLayout({ children }: { children: React.Reac return ( - {children} + + + {children} + + ); } diff --git a/app/(setup)/my-store/components/EmptyStoreState.tsx b/app/(setup)/my-store/components/EmptyStoreState.tsx new file mode 100644 index 00000000..290b50a7 --- /dev/null +++ b/app/(setup)/my-store/components/EmptyStoreState.tsx @@ -0,0 +1,43 @@ +/* + * 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. + */ + +'use client'; + +import { EmptyState } from '@shopify/polaris'; +import { PlusIcon } from '@shopify/polaris-icons'; +import type { EmptyStoreStateProps } from '../types/store.types'; + +export function EmptyStoreState({ canCreateStore, onCreateStore }: EmptyStoreStateProps) { + return ( + +

+ Crea tu primera tienda y comienza a vender online en minutos. Configura tu catálogo, personaliza tu diseño y + lanza tu negocio. +

+
+ ); +} diff --git a/app/(setup)/my-store/components/StoreCard.tsx b/app/(setup)/my-store/components/StoreCard.tsx new file mode 100644 index 00000000..99b58e93 --- /dev/null +++ b/app/(setup)/my-store/components/StoreCard.tsx @@ -0,0 +1,94 @@ +/* + * 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. + */ + +'use client'; + +import { Text, BlockStack, InlineStack } from '@shopify/polaris'; +import type { StoreCardProps } from '../types/store.types'; + +function getInitialsFromName(name: string): string { + return name + .split(' ') + .filter(Boolean) + .map((word) => word[0]) + .join('') + .toUpperCase() + .slice(0, 2); +} + +export function StoreCard({ store, onClick }: StoreCardProps) { + const initials = getInitialsFromName(store.storeName); + const domain = store.defaultDomain || `${store.storeId}.fasttify.com`; + + return ( +
onClick(store.storeId)} + style={{ + cursor: 'pointer', + backgroundColor: 'white', + border: '1px solid #e5e7eb', + borderRadius: '8px', + padding: '16px 24px', + marginBottom: '6px', + transition: 'all 0.2s ease', + }} + onMouseEnter={(e) => { + e.currentTarget.style.borderColor = '#d1d5db'; + e.currentTarget.style.boxShadow = '0 1px 3px 0 rgba(0, 0, 0, 0.1)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.borderColor = '#e5e7eb'; + e.currentTarget.style.boxShadow = 'none'; + }} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + onClick(store.storeId); + } + }} + aria-label={`Seleccionar tienda ${store.storeName}`}> + + +
+ {initials} +
+ + + {store.storeName} + + + {domain} + + +
+
+
+
+ ); +} diff --git a/app/(setup)/my-store/components/StoreFilters.tsx b/app/(setup)/my-store/components/StoreFilters.tsx new file mode 100644 index 00000000..ff20ffd7 --- /dev/null +++ b/app/(setup)/my-store/components/StoreFilters.tsx @@ -0,0 +1,37 @@ +/* + * 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. + */ + +'use client'; + +import { Tabs } from '@shopify/polaris'; +import type { StoreFiltersProps } from '../types/store.types'; + +export function StoreFilters({ selected, onSelect }: StoreFiltersProps) { + const tabs = [ + { + id: 'active', + content: 'Activas', + panelID: 'active-panel', + }, + { + id: 'inactive', + content: 'Inactivas', + panelID: 'inactive-panel', + }, + ]; + + return ; +} diff --git a/app/(setup)/my-store/components/StoreList.tsx b/app/(setup)/my-store/components/StoreList.tsx new file mode 100644 index 00000000..dab94aec --- /dev/null +++ b/app/(setup)/my-store/components/StoreList.tsx @@ -0,0 +1,36 @@ +/* + * 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. + */ + +'use client'; + +import { BlockStack } from '@shopify/polaris'; +import { StoreCard } from './StoreCard'; +import { EmptyStoreState } from './EmptyStoreState'; +import type { StoreListProps } from '../types/store.types'; + +export function StoreList({ stores, onStoreSelect, canCreateStore, onCreateStore }: StoreListProps) { + if (stores.length === 0) { + return ; + } + + return ( + + {stores.map((store) => ( + + ))} + + ); +} diff --git a/app/(setup)/my-store/components/StoreListHeader.tsx b/app/(setup)/my-store/components/StoreListHeader.tsx new file mode 100644 index 00000000..cc96799d --- /dev/null +++ b/app/(setup)/my-store/components/StoreListHeader.tsx @@ -0,0 +1,53 @@ +/* + * 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. + */ + +'use client'; + +import { Button } from '@shopify/polaris'; +import { PlusIcon } from '@shopify/polaris-icons'; +import type { StoreListHeaderProps } from '../types/store.types'; + +export function StoreListHeader({ canCreateStore, onCreateStore }: StoreListHeaderProps) { + const primaryAction = canCreateStore + ? { + content: 'Crear tienda', + onAction: onCreateStore, + icon: PlusIcon, + } + : undefined; + + return ( +
+
+

+ Bienvenido de nuevo +

+ {primaryAction && ( + + )} +
+
+ ); +} diff --git a/app/(setup)/my-store/components/StoreSelector.tsx b/app/(setup)/my-store/components/StoreSelector.tsx index 440d0186..a6c91a0c 100644 --- a/app/(setup)/my-store/components/StoreSelector.tsx +++ b/app/(setup)/my-store/components/StoreSelector.tsx @@ -1,103 +1,78 @@ 'use client'; +import { useState, Suspense, useEffect } from 'react'; +import { Page, Layout, Spinner } from '@shopify/polaris'; +import Image from 'next/image'; import { useUserStores } from '@/app/(setup)/my-store/hooks/useUserStores'; -import { StoreAvatar } from '@/app/(setup)/my-store/components/StoreAvatar'; -import { Button } from '@/components/ui/button'; -import { Loader } from '@/components/ui/loader'; import { routes } from '@/utils/client/routes'; -import { AnimatePresence, motion } from 'framer-motion'; -import { PlusCircle } from 'lucide-react'; -import { Suspense } from 'react'; import useAuthStore from '@/context/core/userStore'; -import Link from 'next/link'; -import Image from 'next/image'; +import { UserMenu } from './UserMenu'; +import { StoreListHeader } from './StoreListHeader'; +import { StoreFilters } from './StoreFilters'; +import { StoreList } from './StoreList'; function StoreError({ message }: { message: string }) { - return
{message}
; -} - -// Componente para mostrar la lista de tiendas -function StoreList({ stores, canCreateStore }: { stores: any[]; canCreateStore: boolean }) { return ( - <> - - - {stores.length > 0 ? ( - // Stores list - stores.map((store, index) => ( - - - - )) - ) : ( - // No stores state -
-
- -

No tienes tiendas configuradas aún

-
-
- )} -
-
- - - {canCreateStore ? ( - - - - ) : ( - stores.length > 0 && ( -
- Has alcanzado el límite máximo de tiendas para tu plan actual -
- ) - )} -
- +
{message}
); } // Componente que carga los datos con Suspense -function StoreData({ userId, userPlan }: { userId: string | null; userPlan?: string }) { - // Obtenemos los datos directamente +function StoreData({ + userId, + userPlan, + isLoading, + selectedTab, + setSelectedTab, +}: { + userId: string | null; + userPlan?: string; + isLoading: boolean; + selectedTab: number; + setSelectedTab: (tab: number) => void; +}) { const result = useUserStores(userId, userPlan); - const { - stores = [], - canCreateStore = false, - error, - } = result as { stores: unknown[]; canCreateStore: boolean; error?: string }; + const { activeStores, inactiveStores, canCreateStore, error } = result; + + const currentStores = selectedTab === 0 ? activeStores : inactiveStores; + + const handleStoreSelect = (storeId: string) => { + window.location.href = routes.store.dashboard.main(storeId); + }; + + const handleCreateStore = () => { + window.location.href = '/first-steps'; + }; + + // Si está cargando la autenticación, mostrar spinner + if (isLoading) { + return ( +
+ +
+ ); + } if (error) { return ; } - return ; + return ( + + + + + + + + + + ); } // Componente principal @@ -105,64 +80,60 @@ export function StoreSelector() { const { user, loading: isLoading } = useAuthStore(); const cognitoUsername = user?.userId; const userPlan = user?.plan; + const [selectedTab, setSelectedTab] = useState(0); + const [isClient, setIsClient] = useState(false); - if (isLoading) { - return ( - - ); - } + useEffect(() => { + setIsClient(true); + }, []); return ( - -
- Logo -

Selecciona una tienda

-

para continuar a tu dashboard

-
- - - }> - - +
+ {/* Card principal con todo el contenido - responsive */} +
+ {/* Header con logo y UserMenu dentro de la card */} +
+
+ Fasttify + Fasttify +
+ useAuthStore.getState().logout()} /> +
- - - Ayuda - - - Privacidad - - - Términos - - - + {/* Contenido principal con un solo Suspense */} + {!isClient ? ( +
+ +
+ ) : ( + + +
+ }> + + + )} +
+ ); } diff --git a/app/(setup)/my-store/components/UserMenu.tsx b/app/(setup)/my-store/components/UserMenu.tsx new file mode 100644 index 00000000..e783d373 --- /dev/null +++ b/app/(setup)/my-store/components/UserMenu.tsx @@ -0,0 +1,113 @@ +/* + * 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. + */ + +'use client'; + +import { Popover, ActionList, Text, BlockStack, InlineStack } from '@shopify/polaris'; +import { ExitIcon, SettingsIcon, ReplaceIcon } from '@shopify/polaris-icons'; +import { useState } from 'react'; +import type { UserMenuProps } from '../types/store.types'; + +function getInitialsFromEmail(email: string): string { + return email.split('@')[0].slice(0, 2).toUpperCase(); +} + +export function UserMenu({ user, onLogout }: UserMenuProps) { + const [popoverActive, setPopoverActive] = useState(false); + + if (!user) { + return null; + } + + const initials = getInitialsFromEmail(user.email); + + const togglePopoverActive = () => setPopoverActive((prev) => !prev); + + const handleLogout = () => { + setPopoverActive(false); + onLogout(); + }; + + const actions = [ + { + content: 'Gestionar cuenta', + icon: SettingsIcon, + onAction: () => { + setPopoverActive(false); + // TODO: Implementar navegación a configuración de cuenta + }, + }, + { + content: 'Cambiar de cuenta', + icon: ReplaceIcon, + onAction: () => { + setPopoverActive(false); + // TODO: Implementar cambio de cuenta + }, + }, + { + content: 'Cerrar sesión', + icon: ExitIcon, + destructive: true, + onAction: handleLogout, + }, + ]; + + return ( + + {initials} + + } + onClose={() => setPopoverActive(false)} + preferredAlignment="right"> +
+ + + + + {user.email.length > 20 ? `${user.email.slice(0, 20)}...` : user.email} + + + {user.email} + + + + setPopoverActive(false)} /> + +
+
+ ); +} diff --git a/app/(setup)/my-store/hooks/useUserStores.ts b/app/(setup)/my-store/hooks/useUserStores.ts index 2d5ff3f4..f1a61180 100644 --- a/app/(setup)/my-store/hooks/useUserStores.ts +++ b/app/(setup)/my-store/hooks/useUserStores.ts @@ -1,5 +1,6 @@ import { use } from 'react'; import { client } from '@/lib/amplify-client'; +import type { Store, UseUserStoresResult } from '../types/store.types'; const STORE_LIMITS = { Imperial: 5, @@ -13,12 +14,13 @@ const storeCache = new Map(); * Hook para obtener las tiendas de un usuario * Esta función es compatible con Suspense */ -export function useUserStores(userId: string | null, userPlan?: string) { +export function useUserStores(userId: string | null, userPlan?: string): UseUserStoresResult { // Si no hay userId, devolver datos vacíos if (!userId) { return { stores: [], - allStores: [], + activeStores: [], + inactiveStores: [], canCreateStore: false, error: null, storeCount: 0, @@ -40,7 +42,7 @@ export function useUserStores(userId: string | null, userPlan?: string) { /** * Función que realiza la consulta a la base de datos */ -async function fetchUserStores(userId: string, userPlan?: string) { +async function fetchUserStores(userId: string, userPlan?: string): Promise { try { // Obtener todas las tiendas del usuario (para verificar límites) const { data: allUserStores } = await client.models.UserStore.listUserStoreByUserId( @@ -48,11 +50,15 @@ async function fetchUserStores(userId: string, userPlan?: string) { userId: userId, }, { - selectionSet: ['storeId', 'storeName', 'storeType', 'onboardingCompleted'], + selectionSet: ['storeId', 'storeName', 'storeType', 'defaultDomain', 'storeStatus', 'onboardingCompleted'], } ); - const completedStores = allUserStores || []; + const completedStores = (allUserStores || []) as Store[]; + + // Separar tiendas activas e inactivas + const activeStores = completedStores.filter((store) => store.storeStatus !== false); + const inactiveStores = completedStores.filter((store) => store.storeStatus === false); // Verificar límite de tiendas según el plan const currentCount = allUserStores?.length || 0; @@ -60,7 +66,8 @@ async function fetchUserStores(userId: string, userPlan?: string) { return { stores: completedStores, - allStores: allUserStores || [], + activeStores, + inactiveStores, canCreateStore: currentCount < limit, error: null, storeCount: allUserStores?.length || 0, @@ -69,9 +76,10 @@ async function fetchUserStores(userId: string, userPlan?: string) { console.error('getUserStores: Error fetching stores:', err); return { stores: [], - allStores: [], + activeStores: [], + inactiveStores: [], canCreateStore: false, - error: err, + error: err instanceof Error ? err.message : 'Error desconocido', storeCount: 0, }; } diff --git a/app/(setup)/my-store/types/store.types.ts b/app/(setup)/my-store/types/store.types.ts new file mode 100644 index 00000000..b8dde7c1 --- /dev/null +++ b/app/(setup)/my-store/types/store.types.ts @@ -0,0 +1,83 @@ +/* + * 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 type StoreStatus = 'active' | 'inactive'; + +export interface Store { + storeId: string; + storeName: string; + defaultDomain?: string; + storeType: string; + storeStatus?: boolean; + storeDescription?: string; + storeLogo?: string; + storeFavicon?: string; + storeTheme?: string; + storeCurrency?: string; + currencyFormat?: string; + currencyLocale?: string; + currencyDecimalPlaces?: number; + storeAdress?: string; + contactEmail?: string; + contactPhone?: string; + onboardingCompleted: boolean; + onboardingData?: any; +} + +export interface UserMenuProps { + user: { + email: string; + nickName?: string; + picture?: string; + } | null; + onLogout: () => void; +} + +export interface StoreCardProps { + store: Store; + onClick: (storeId: string) => void; +} + +export interface StoreFiltersProps { + selected: number; + onSelect: (selectedTabIndex: number) => void; +} + +export interface StoreListHeaderProps { + canCreateStore: boolean; + onCreateStore: () => void; +} + +export interface StoreListProps { + stores: Store[]; + onStoreSelect: (storeId: string) => void; + canCreateStore: boolean; + onCreateStore: () => void; +} + +export interface EmptyStoreStateProps { + canCreateStore: boolean; + onCreateStore: () => void; +} + +export interface UseUserStoresResult { + stores: Store[]; + activeStores: Store[]; + inactiveStores: Store[]; + canCreateStore: boolean; + error: string | null; + storeCount: number; +} diff --git a/app/api/stores/_lib/assets/controllers/get-asset-controller.ts b/app/api/stores/_lib/assets/controllers/get-asset-controller.ts index 8a6f749f..3406823b 100644 --- a/app/api/stores/_lib/assets/controllers/get-asset-controller.ts +++ b/app/api/stores/_lib/assets/controllers/get-asset-controller.ts @@ -23,7 +23,7 @@ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; const s3Client = new S3Client({ region: process.env.REGION_BUCKET }); const bucketName = process.env.BUCKET_NAME || ''; const cloudFrontDomain = process.env.CLOUDFRONT_DOMAIN_NAME || ''; -const appEnv = process.env.NODE_ENV || 'development'; +const appEnv = process.env.APP_ENV || 'development'; export async function getAsset(request: NextRequest, storeId: string, assetPath: string): Promise { const corsHeaders = await getNextCorsHeaders(request); diff --git a/app/api/stores/_lib/themes/controllers/activate-controller.ts b/app/api/stores/_lib/themes/controllers/activate-controller.ts index 4da2edd5..9ef036a1 100644 --- a/app/api/stores/_lib/themes/controllers/activate-controller.ts +++ b/app/api/stores/_lib/themes/controllers/activate-controller.ts @@ -76,12 +76,6 @@ export async function activateTheme(request: NextRequest, storeId: string): Prom ); } - logger.info( - 'Theme activation updated successfully', - { storeId, themeId, isActive: updatedTheme?.isActive }, - 'ThemeActivationAPI' - ); - return NextResponse.json( { success: true, diff --git a/app/api/themes/_lib/services/s3-theme-files.service.ts b/app/api/themes/_lib/services/s3-theme-files.service.ts index ca676276..27962216 100644 --- a/app/api/themes/_lib/services/s3-theme-files.service.ts +++ b/app/api/themes/_lib/services/s3-theme-files.service.ts @@ -42,7 +42,7 @@ export class ThemeS3Service { private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutos private constructor() { - this.isProduction = process.env.NODE_ENV === 'production'; + this.isProduction = process.env.APP_ENV === 'production'; this.bucket = process.env.BUCKET_NAME || ''; this.s3 = new S3Client({ region: process.env.REGION_BUCKET, diff --git a/app/store/components/store-config/components/LogoUploader.tsx b/app/store/components/store-config/components/LogoUploader.tsx index 23be1f35..41741966 100644 --- a/app/store/components/store-config/components/LogoUploader.tsx +++ b/app/store/components/store-config/components/LogoUploader.tsx @@ -1,17 +1,6 @@ import { useState, useCallback } from 'react'; -import { - Modal, - Tabs, - Button, - DropZone, - BlockStack, - Text, - Thumbnail, - Banner, - Toast, - Tooltip, - InlineStack, -} from '@shopify/polaris'; +import { Modal, Tabs, Button, DropZone, BlockStack, Text, Banner, Toast, Tooltip, InlineStack } from '@shopify/polaris'; +import Image from 'next/image'; import { useLogoUpload } from '@/app/store/hooks/storage/useLogoUpload'; import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreData'; import useStoreDataStore from '@/context/core/storeDataStore'; @@ -157,7 +146,16 @@ export function LogoUploader() { {logoUrl ? ( - +
+ Logo preview +
@@ -177,7 +175,16 @@ export function LogoUploader() { {faviconUrl ? ( - +
+ Favicon preview +
diff --git a/app/store/components/store-config/components/ThemePreview.tsx b/app/store/components/store-config/components/ThemePreview.tsx index df0cb80d..096f0a91 100644 --- a/app/store/components/store-config/components/ThemePreview.tsx +++ b/app/store/components/store-config/components/ThemePreview.tsx @@ -1,61 +1,32 @@ 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 { openStoreUrl } from '@/lib/utils/store-url'; -import { - Badge, - BlockStack, - Button, - ButtonGroup, - Card, - Layout, - MediaCard, - Page, - Spinner, - Tabs, - Text, -} from '@shopify/polaris'; -import { MoneyFilledIcon } from '@shopify/polaris-icons'; +import { BlockStack, Card, Layout, MediaCard, Page, Spinner, Tabs, Text } from '@shopify/polaris'; import Image from 'next/image'; 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 handleTabChange = useCallback((index: number) => setSelectedTab(index), []); - const handleViewStore = useCallback(() => { - openStoreUrl({ - storeId: storeId, - customDomain: customDomain, - }); - }, [storeId, customDomain]); - const tabs = [ { id: 'preview', content: 'Vista previa' }, { id: 'themes', content: 'Gestionar temas' }, ]; return ( - {}, - }}> + - + {selectedTab === 0 && ( @@ -74,7 +45,6 @@ export function ThemePreview() { display: 'flex', alignItems: 'center', justifyContent: 'center', - background: 'var(--p-color-bg-subdued)', }}> @@ -102,16 +72,8 @@ export function ThemePreview() { {activeTheme?.name || 'Tema'}
- - Diseño actual - - - - - - + } + onClose={() => setActivePopover(false)} + preferredPosition="below" + preferredAlignment="right"> + + + ); +} diff --git a/app/store/components/theme-management/components/InactiveThemesList.tsx b/app/store/components/theme-management/components/InactiveThemesList.tsx new file mode 100644 index 00000000..8c91cd2f --- /dev/null +++ b/app/store/components/theme-management/components/InactiveThemesList.tsx @@ -0,0 +1,122 @@ +'use client'; + +import { ActionList, BlockStack, Button, Card, InlineStack, Popover, Text } from '@shopify/polaris'; +import { DeleteIcon, EditIcon, MenuHorizontalIcon, StatusActiveIcon, ViewIcon } from '@shopify/polaris-icons'; +import { useState } from 'react'; +import Image from 'next/image'; +import type { InactiveThemesListProps } from '@/app/store/components/theme-management/types/theme-types'; + +const THEME_PREVIEW_PLACEHOLDER = + 'https://images.unsplash.com/photo-1741482529153-a98d81235d06?q=80&w=2070&auto=format&fit=crop'; + +export function InactiveThemesList({ themes, onAction }: InactiveThemesListProps) { + const [activePopover, setActivePopover] = useState(null); + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('es-ES', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + const handleAction = (action: string, themeId: string): void => { + const theme = themes.find((t) => t.id === themeId); + if (theme) { + onAction(action, theme); + } + setActivePopover(null); + }; + + const getThemeActions = (themeId: string) => [ + { + content: 'Activar', + icon: StatusActiveIcon, + onAction: () => handleAction('activate', themeId), + }, + { + content: 'Vista previa', + icon: ViewIcon, + onAction: () => handleAction('preview', themeId), + }, + { + content: 'Renombrar', + icon: EditIcon, + onAction: () => handleAction('rename', themeId), + }, + { + content: 'Editar código', + icon: EditIcon, + onAction: () => handleAction('edit', themeId), + }, + { + content: 'Eliminar', + icon: DeleteIcon, + destructive: true, + onAction: () => handleAction('delete', themeId), + }, + ]; + + if (themes.length === 0) { + return null; + } + + return ( + + + + Otros temas + + + + {themes.map((theme) => ( + + + +
+ {theme.name} +
+ + + {theme.name} + + + v{theme.version} • {formatDate(theme.updatedAt)} + + +
+ + setActivePopover(activePopover === theme.id ? null : theme.id)} + /> + } + onClose={() => setActivePopover(null)} + preferredPosition="below" + preferredAlignment="right"> + + +
+
+ ))} +
+
+
+ ); +} diff --git a/app/store/components/theme-management/components/ThemeList.tsx b/app/store/components/theme-management/components/ThemeList.tsx index 89821ef3..0fa24305 100644 --- a/app/store/components/theme-management/components/ThemeList.tsx +++ b/app/store/components/theme-management/components/ThemeList.tsx @@ -1,13 +1,14 @@ 'use client'; -import { Banner, BlockStack, Button, Card, EmptyState, InlineStack, Modal, Spinner, Text } from '@shopify/polaris'; -import { AlertCircleIcon, PlusIcon } from '@shopify/polaris-icons'; +import { Banner, BlockStack, Button, Card, Spinner, Text } from '@shopify/polaris'; +import { AlertCircleIcon } from '@shopify/polaris-icons'; import { useState } from 'react'; -import { useThemeList } from '../hooks/useThemeList'; -import { useThemeActions } from '../hooks/useThemeActions'; -import { ThemeTable } from './ThemeTable'; -import { ThemeModals } from './ThemeModals'; -import { ThemeUploadForm } from './ThemeUploadForm'; +import { useThemeList } from '@/app/store/components/theme-management/hooks/useThemeList'; +import { useThemeActions } from '@/app/store/components/theme-management/hooks/useThemeActions'; +import { ThemeModals } from '@/app/store/components/theme-management/components/ThemeModals'; +import { ThemeUploadModal } from '@/app/store/components/theme-management/components/ThemeUploadModal'; +import { ThemePreviewCard } from '@/app/store/components/theme-management/components/ThemePreviewCard'; +import { InactiveThemesList } from '@/app/store/components/theme-management/components/InactiveThemesList'; export function ThemeList({ storeId }: { storeId: string }) { const [showUploadModal, setShowUploadModal] = useState(false); @@ -19,11 +20,9 @@ export function ThemeList({ storeId }: { storeId: string }) { showDeleteModal, isActivating, isDeleting, - openActivateModal, - openDeleteModal, - handleEditTheme, handleActivateTheme, handleDeleteTheme, + handleThemeAction, closeActivateModal, closeDeleteModal, } = useThemeActions({ storeId, activateTheme, deleteTheme, refreshThemes }); @@ -60,100 +59,112 @@ export function ThemeList({ storeId }: { storeId: string }) { ); } + const activeTheme = themes.find((theme) => theme.isActive); + const inactiveThemes = themes.filter((theme) => !theme.isActive); + + const handleCustomize = () => { + if (activeTheme) { + handleThemeAction('edit', activeTheme); + } + }; + + const handleUploadTheme = () => { + setShowUploadModal(true); + }; + return ( <> - -
- - - Gestión de Temas - - - - - -
- - {themes.length === 0 ? ( - -

Sube tu primer tema para personalizar la apariencia de tu tienda.

- -
- ) : ( - + {activeTheme ? ( + + ) : ( + +
+ + + No hay tema activo. Sube un tema para comenzar. + + + +
+
)} -
+ + {inactiveThemes.length > 0 && } + + {themes.length === 0 && ( + +
+ + No hay temas subidos. Sube tu primer tema para personalizar la apariencia de tu tienda. + +
+
+ )} +
{/* Modal de subida de tema */} - {showUploadModal && ( - setShowUploadModal(false)} title="Subir Tema Personalizado"> - - { - const formData = new FormData(); - formData.append('theme', file); - - const response = await fetch(`/api/stores/${storeId}/themes/upload`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || 'Error al subir el tema'); - } - - const result = await response.json(); - return result.details || result; - }} - onConfirm={async (result, file) => { - try { - const formData = new FormData(); - formData.append('theme', file); - formData.append('themeData', JSON.stringify(result)); - - const response = await fetch(`/api/stores/${storeId}/themes/confirm`, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || 'Error al confirmar el tema'); - } - - const confirmResult = await response.json(); - - if (confirmResult.success && confirmResult.processId) { - setTimeout(() => refreshThemes(), 2000); - return { ok: true, processId: confirmResult.processId as string }; - } - return { ok: false }; - } catch (error) { - console.error('Error confirming theme:', error); - return { ok: false }; - } - }} - onCancel={() => { - setShowUploadModal(false); - refreshThemes(); - }} - /> - - - )} + setShowUploadModal(false)} + storeId={storeId} + onUpload={async (file: File) => { + const formData = new FormData(); + formData.append('theme', file); + + const response = await fetch(`/api/stores/${storeId}/themes/upload`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Error al subir el tema'); + } + + const result = await response.json(); + return result.details || result; + }} + onConfirm={async (result, file) => { + try { + const formData = new FormData(); + formData.append('theme', file); + formData.append('themeData', JSON.stringify(result)); + + const response = await fetch(`/api/stores/${storeId}/themes/confirm`, { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Error al confirmar el tema'); + } + + const confirmResult = await response.json(); + + if (confirmResult.success && confirmResult.processId) { + setTimeout(() => refreshThemes(), 2000); + return { ok: true, processId: confirmResult.processId as string }; + } + return { ok: false }; + } catch (error) { + console.error('Error confirming theme:', error); + return { ok: false }; + } + }} + onCancel={() => { + setShowUploadModal(false); + refreshThemes(); + }} + /> {/* Modal de confirmación de activación */} {showActivateModal && selectedTheme && ( - + @@ -71,30 +54,28 @@ export function ThemeModals({ + + + + + + )} {/* Modal de confirmación de eliminación */} {showDeleteModal && selectedTheme && ( - + @@ -105,6 +86,23 @@ export function ThemeModals({ + + + + + + )} diff --git a/app/store/components/theme-management/components/ThemePreviewCard.tsx b/app/store/components/theme-management/components/ThemePreviewCard.tsx new file mode 100644 index 00000000..7e32bf6c --- /dev/null +++ b/app/store/components/theme-management/components/ThemePreviewCard.tsx @@ -0,0 +1,137 @@ +'use client'; + +import { ActionList, Badge, BlockStack, Button, Card, InlineStack, Popover, Text } from '@shopify/polaris'; +import { DeleteIcon, EditIcon, MenuHorizontalIcon, ViewIcon } from '@shopify/polaris-icons'; +import { useState } from 'react'; +import Image from 'next/image'; +import type { ThemePreviewCardProps } from '@/app/store/components/theme-management/types/theme-types'; +import { ImportThemeDropdown } from '@/app/store/components/theme-management/components/ImportThemeDropdown'; + +const THEME_PREVIEW_PLACEHOLDER = + 'https://images.unsplash.com/photo-1741482529153-a98d81235d06?q=80&w=2070&auto=format&fit=crop'; + +interface ThemePreviewCardPropsWithUpload extends ThemePreviewCardProps { + onUploadTheme: () => void; +} + +export function ThemePreviewCard({ theme, onCustomize, onAction, onUploadTheme }: ThemePreviewCardPropsWithUpload) { + const [activePopover, setActivePopover] = useState(false); + + const formatDate = (dateString: string): string => { + return new Date(dateString).toLocaleDateString('es-ES', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + }; + + const handleAction = (action: string): void => { + onAction(action, theme); + setActivePopover(false); + }; + + const themeActions = [ + { + content: 'Vista previa', + icon: ViewIcon, + onAction: () => handleAction('preview'), + }, + { + content: 'Editar código', + icon: EditIcon, + onAction: () => handleAction('edit'), + }, + { + content: 'Eliminar', + icon: DeleteIcon, + destructive: true, + onAction: () => handleAction('delete'), + }, + ]; + + return ( + + + {/* Header con título y acciones */} + + + Temas + + + + + + + {/* Preview del tema */} +
+ {`Preview +
+ + {/* Información del tema */} + + + +
+ {theme.name} +
+ + + + {theme.name} + + Tema actual + + + Agregado: {formatDate(theme.createdAt)} + + + Versión {theme.version} + + +
+
+ + setActivePopover(!activePopover)} />} + onClose={() => setActivePopover(false)} + preferredPosition="below" + preferredAlignment="right"> + + +
+ +
+
+
+
+
+ ); +} diff --git a/app/store/components/theme-management/components/ThemeUploadForm.tsx b/app/store/components/theme-management/components/ThemeUploadForm.tsx index 3b116564..1cff0e49 100644 --- a/app/store/components/theme-management/components/ThemeUploadForm.tsx +++ b/app/store/components/theme-management/components/ThemeUploadForm.tsx @@ -1,37 +1,26 @@ -import { - Banner, - BlockStack, - Button, - Card, - Icon, - InlineStack, - ProgressBar, - Spinner, - Text, - Thumbnail, -} from '@shopify/polaris'; +import { Banner, BlockStack, Icon, InlineStack, ProgressBar, Spinner, Text } from '@shopify/polaris'; import { AlertCircleIcon, CheckCircleIcon, UploadIcon } from '@shopify/polaris-icons'; import { useCallback } from 'react'; -import { useThemeUpload } from '../hooks/useThemeUpload'; -import type { ThemeUploadFormProps } from '../types/theme-types'; -import { formatFileSize, getValidationTone } from '../utils/theme-utils'; - -export function ThemeUploadForm({ storeId, onUpload, onConfirm, onCancel: _onCancel }: ThemeUploadFormProps) { +import { useThemeUpload } from '@/app/store/components/theme-management/hooks/useThemeUpload'; +import type { ThemeUploadFormProps } from '@/app/store/components/theme-management/types/theme-types'; +import { formatFileSize } from '@/app/store/components/theme-management/utils/theme-utils'; + +export function ThemeUploadForm({ + storeId, + onUpload, + onConfirm, + onCancel: _onCancel, + onFileSelect, +}: ThemeUploadFormProps) { const { selectedFile, - uploadResult, isUploading, - isConfirming, isProcessing, processingStatus, processingError, uploadProgress, error, handleFileSelect, - handleUpload, - handleConfirm, - handleCancel, - handleClearFile, } = useThemeUpload({ storeId, onUpload, @@ -48,11 +37,10 @@ export function ThemeUploadForm({ storeId, onUpload, onConfirm, onCancel: _onCan const file = event.dataTransfer.files[0]; if (file) { handleFileSelect(file); - // Automáticamente iniciar el upload cuando se arrastra un archivo - setTimeout(() => handleUpload(), 100); + onFileSelect?.(file); } }, - [handleFileSelect, handleUpload] + [handleFileSelect, onFileSelect] ); const handleFileInputChange = useCallback( @@ -60,220 +48,104 @@ export function ThemeUploadForm({ storeId, onUpload, onConfirm, onCancel: _onCan const file = event.target.files?.[0]; if (file) { handleFileSelect(file); - setTimeout(() => handleUpload(), 100); + onFileSelect?.(file); } }, - [handleFileSelect, handleUpload] + [handleFileSelect, onFileSelect] ); return ( - - {/* Área de subida */} - - - - Seleccionar Archivo - + + {/* Instrucciones */} + + Sube un archivo ZIP de tu tema de Fasttify. Los temas subidos se publicarán por defecto. + + + Tamaño máximo del archivo: 50 MB + + + {/* Área de drop */} +
document.getElementById('theme-file-input')?.click()}> + + + {selectedFile ? ( + + + {selectedFile.name} + + + {formatFileSize(selectedFile.size)} + + + ) : ( + + + + Arrastra tu archivo ZIP aquí o haz clic para seleccionar + + + )} +
+ + {/* Error */} + {error && ( + +

{error}

+
+ )} - - Sube tu tema en formato ZIP. El archivo debe contener la estructura completa del tema. + {/* Progreso */} + {isUploading && ( + + + Procesando tema... - - {/* Área de drop */} -
document.getElementById('theme-file-input')?.click()}> - - - - - - {selectedFile ? selectedFile.name : 'Arrastra tu archivo ZIP aquí o haz clic para seleccionar'} - - - Máximo 50MB - - -
- - {/* Archivo seleccionado */} - {selectedFile && ( - - - - - - - {selectedFile.name} - - - {formatFileSize(selectedFile.size)} - - - - - {!uploadResult && !isUploading && ( - - )} - - - - - )} - - {/* Error */} - {error && ( - -

{error}

-
- )} - - {/* Progreso */} - {isUploading && ( - - - Procesando tema... - - - - )} +
-
- - {/* Resultado de la validación */} - {uploadResult && uploadResult.validation && ( - - - - - Resultado de la Validación - - - {uploadResult.validation?.isValid ? 'Tema válido' : 'Tema con problemas'} - - - - {/* Información del tema */} - - - {uploadResult.theme?.name || 'Tema sin nombre'} v{uploadResult.theme?.version || '1.0.0'} - - {uploadResult.theme?.author && ( - - Por {uploadResult.theme.author} - - )} - {uploadResult.theme?.description && ( - - {uploadResult.theme.description} - - )} - + )} - {/* Estadísticas */} - - - {uploadResult.theme?.fileCount || 0} archivos - - - {uploadResult.theme?.assetCount || 0} assets - - - {uploadResult.theme?.sectionCount || 0} secciones - - - {uploadResult.theme?.templateCount || 0} templates + {/* Estado de procesamiento */} + {isProcessing || processingStatus === 'processing' ? ( + + + + + + Procesando tema... Esto puede tardar unos segundos. - - {/* Errores y advertencias */} - {uploadResult.validation?.errorCount > 0 && ( - - - Errores ({uploadResult.validation.errorCount}) - - {uploadResult.validation.errors?.slice(0, 3).map((error, index) => ( - - • {error.message} - - ))} - - )} - - {uploadResult.validation?.warningCount > 0 && ( - - - Advertencias ({uploadResult.validation.warningCount}) - - {uploadResult.validation.warnings?.slice(0, 3).map((warning, index) => ( - - • {warning.message} - - ))} - - )} - - {/* Estado de confirmación y procesamiento */} - {processingStatus === 'completed' ? ( - -

¡Tema confirmado y almacenado correctamente! Cerrando…

-
- ) : isProcessing || processingStatus === 'processing' ? ( - - - - - - Confirmando y almacenando el tema... Esto puede tardar 30–60 segundos. - - - - Puedes mantener esta ventana abierta; se cerrará automáticamente al finalizar. - - - - ) : processingStatus === 'error' ? ( - -

{processingError || 'Ocurrió un error al confirmar el tema'}

-
- ) : ( - - - - - )}
-
- )} + + ) : processingStatus === 'completed' ? ( + +

¡Tema subido correctamente! Cerrando…

+
+ ) : processingStatus === 'error' ? ( + +

{processingError || 'Ocurrió un error al procesar el tema'}

+
+ ) : null}
); } diff --git a/app/store/components/theme-management/components/ThemeUploadModal.tsx b/app/store/components/theme-management/components/ThemeUploadModal.tsx new file mode 100644 index 00000000..36b5d132 --- /dev/null +++ b/app/store/components/theme-management/components/ThemeUploadModal.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { Modal } from '@shopify/polaris'; +import { useState } from 'react'; +import { ThemeUploadForm } from '@/app/store/components/theme-management/components/ThemeUploadForm'; +import type { ThemeUploadResult } from '@/app/store/components/theme-management/types/theme-types'; + +interface ThemeUploadModalProps { + isOpen: boolean; + onClose: () => void; + storeId: string; + onUpload: (file: File) => Promise; + onConfirm: (result: ThemeUploadResult, originalFile: File) => Promise<{ ok: boolean; processId?: string }>; + onCancel: () => void; +} + +export function ThemeUploadModal({ + isOpen, + onClose, + storeId, + onUpload, + onConfirm, + onCancel: _onCancel, +}: ThemeUploadModalProps) { + const [isProcessing, setIsProcessing] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + + const handleCompleteUpload = async () => { + if (!selectedFile) return; + + setIsProcessing(true); + try { + // 1. Upload del archivo + const uploadResult = await onUpload(selectedFile); + + // 2. Si es válido, confirmar automáticamente + if (uploadResult.validation?.isValid) { + const confirmResult = await onConfirm(uploadResult, selectedFile); + if (confirmResult.ok) { + setTimeout(() => onClose(), 2000); + } + } + } catch (error) { + console.error('Upload error:', error); + } finally { + setIsProcessing(false); + } + }; + + return ( + + + + + + ); +} diff --git a/app/store/components/theme-management/hooks/useThemeActions.ts b/app/store/components/theme-management/hooks/useThemeActions.ts index a69987aa..d8fe3ffd 100644 --- a/app/store/components/theme-management/hooks/useThemeActions.ts +++ b/app/store/components/theme-management/hooks/useThemeActions.ts @@ -1,17 +1,5 @@ import { useState } from 'react'; - -interface Theme { - id: string; - name: string; - version: string; - author: string; - description: string; - isActive: boolean; - fileCount: number; - totalSize: number; - createdAt: string; - updatedAt: string; -} +import type { Theme } from '@/app/store/components/theme-management/types/theme-types'; interface UseThemeActionsProps { storeId: string; @@ -81,6 +69,64 @@ export function useThemeActions({ } }; + const handlePreviewTheme = (theme: Theme) => { + const previewUrl = `/store/${_storeId}/${theme.name.toLowerCase().replace(/\s+/g, '-')}`; + const newWindow = window.open(previewUrl, '_blank', 'noopener,noreferrer'); + + if (!newWindow) { + throw new Error('could not open the preview window'); + } + }; + + const handleRenameTheme = async (theme: Theme, newName: string) => { + try { + const response = await fetch(`/api/stores/${_storeId}/themes/${theme.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: newName, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'Error al renombrar el tema'); + } + + _refreshThemes(); + } catch (error) { + console.error('Error renaming theme:', error); + throw error; + } + }; + + const handleThemeAction = (action: string, theme: Theme) => { + switch (action) { + case 'preview': + handlePreviewTheme(theme); + break; + case 'edit': + handleEditTheme(); + break; + case 'activate': + openActivateModal(theme); + break; + case 'delete': + openDeleteModal(theme); + break; + case 'rename': + const newName = prompt('Nuevo nombre del tema:', theme.name); + if (newName && newName.trim() !== theme.name) { + handleRenameTheme(theme, newName.trim()); + } + break; + default: + console.warn('Acción no reconocida:', action); + } + }; + return { selectedTheme, showActivateModal, @@ -92,6 +138,7 @@ export function useThemeActions({ handleEditTheme, handleActivateTheme, handleDeleteTheme, + handleThemeAction, closeActivateModal: () => setShowActivateModal(false), closeDeleteModal: () => setShowDeleteModal(false), }; diff --git a/app/store/components/theme-management/hooks/useThemeUpload.ts b/app/store/components/theme-management/hooks/useThemeUpload.ts index 1a19ef14..579ecfa7 100644 --- a/app/store/components/theme-management/hooks/useThemeUpload.ts +++ b/app/store/components/theme-management/hooks/useThemeUpload.ts @@ -1,5 +1,5 @@ -import { useCallback, useState } from 'react'; -import type { ThemeUploadResult } from '../types/theme-types'; +import { useCallback, useRef, useState } from 'react'; +import type { ThemeUploadResult } from '@/app/store/components/theme-management/types/theme-types'; interface UseThemeUploadProps { storeId: string; @@ -81,6 +81,12 @@ export function useThemeUpload({ storeId, onUpload, onConfirm }: UseThemeUploadP if (!result.success) { setError('Error al procesar el tema'); + return; + } + + // Si el tema es válido, automáticamente confirmarlo + if (result.validation?.isValid) { + setTimeout(() => handleConfirmRef.current?.(), 500); } } catch (err) { setError('Error al subir el tema. Por favor intenta de nuevo.'); @@ -90,6 +96,8 @@ export function useThemeUpload({ storeId, onUpload, onConfirm }: UseThemeUploadP } }, [selectedFile, onUpload]); + const handleConfirmRef = useRef<() => void>(); + const handleConfirm = useCallback(async () => { if (!uploadResult || !selectedFile) return; @@ -171,6 +179,8 @@ export function useThemeUpload({ storeId, onUpload, onConfirm }: UseThemeUploadP } }, [uploadResult, selectedFile, onConfirm, storeId]); + handleConfirmRef.current = handleConfirm; + const handleCancel = useCallback(() => { setSelectedFile(null); setUploadResult(null); diff --git a/app/store/components/theme-management/types/theme-types.ts b/app/store/components/theme-management/types/theme-types.ts index ad1bb34d..29369a23 100644 --- a/app/store/components/theme-management/types/theme-types.ts +++ b/app/store/components/theme-management/types/theme-types.ts @@ -34,4 +34,42 @@ export interface ThemeUploadFormProps { onUpload: (file: File) => Promise; onConfirm: (result: ThemeUploadResult, originalFile: File) => Promise<{ ok: boolean; processId?: string }>; onCancel: () => void; + onFileSelect?: (file: File | null) => void; +} + +export interface Theme { + id: string; + name: string; + version: string; + author: string; + description: string; + isActive: boolean; + fileCount: number; + totalSize: number; + createdAt: string; + updatedAt: string; + previewUrl?: string; +} + +export interface ThemeAction { + content: string; + icon?: React.ComponentType; + onAction: () => void; + destructive?: boolean; + disabled?: boolean; +} + +export interface ThemePreviewCardProps { + theme: Theme; + onCustomize: () => void; + onAction: (action: string, theme: Theme) => void; +} + +export interface ImportThemeDropdownProps { + onUploadTheme: () => void; +} + +export interface InactiveThemesListProps { + themes: Theme[]; + onAction: (action: string, theme: Theme) => void; } diff --git a/jest.setup.ts b/jest.setup.ts index c2a72bc7..191e0416 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -5,3 +5,23 @@ process.env.APP_ENV = 'test'; process.env.DEV_CACHE_ENABLED = 'true'; global.console.warn = jest.fn(); + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); diff --git a/packages/liquid-forge/services/templates/template-loader.ts b/packages/liquid-forge/services/templates/template-loader.ts index 70183006..2c6595ec 100644 --- a/packages/liquid-forge/services/templates/template-loader.ts +++ b/packages/liquid-forge/services/templates/template-loader.ts @@ -43,7 +43,7 @@ class TemplateLoader { private constructor() { this.bucketName = process.env.BUCKET_NAME || ''; - this.isProduction = process.env.NODE_ENV === 'production'; + this.isProduction = process.env.APP_ENV === 'production'; if (this.bucketName) { this.s3Client = new S3Client({ diff --git a/test/unit/components/StoreSelector.test.tsx b/test/unit/components/StoreSelector.test.tsx index 06188ce3..5be17b1c 100644 --- a/test/unit/components/StoreSelector.test.tsx +++ b/test/unit/components/StoreSelector.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { AppProvider } from '@shopify/polaris'; import { StoreSelector } from '@/app/(setup)/my-store/components/StoreSelector'; import { useAuth } from '@/context/hooks/useAuth'; import { useUserStores } from '@/app/(setup)/my-store/hooks/useUserStores'; @@ -38,6 +39,9 @@ jest.mock('framer-motion', () => ({ AnimatePresence: ({ children }: any) => <>{children}, })); +// Wrapper con AppProvider para los tests de Polaris +const TestWrapper = ({ children }: { children: React.ReactNode }) => {children}; + describe('StoreSelector', () => { beforeEach(() => { // Configuración por defecto para los mocks @@ -57,43 +61,73 @@ describe('StoreSelector', () => { storeName: 'Tienda de Prueba', storeType: 'Ropa', onboardingCompleted: true, + storeStatus: true, + }, + ], + activeStores: [ + { + storeId: 'store-1', + storeName: 'Tienda de Prueba', + storeType: 'Ropa', + onboardingCompleted: true, + storeStatus: true, }, ], + inactiveStores: [], canCreateStore: true, error: null, + storeCount: 1, }); }); it('renderiza el título correctamente', () => { - render(); - expect(screen.getByText('Selecciona una tienda')).toBeInTheDocument(); - expect(screen.getByText('para continuar a tu dashboard')).toBeInTheDocument(); + render( + + + + ); + expect(screen.getByText('Bienvenido de nuevo')).toBeInTheDocument(); }); it('muestra la lista de tiendas cuando hay tiendas disponibles', () => { - render(); + render( + + + + ); expect(screen.getByText('Tienda de Prueba')).toBeInTheDocument(); - expect(screen.getByText('Ropa')).toBeInTheDocument(); + expect(screen.getByText('store-1.fasttify.com')).toBeInTheDocument(); }); it('muestra el mensaje de no tiendas cuando no hay tiendas', () => { // Cambiamos el mock para simular que no hay tiendas (useUserStores as jest.Mock).mockReturnValue({ stores: [], + activeStores: [], + inactiveStores: [], canCreateStore: true, error: null, + storeCount: 0, }); - render(); + render( + + + + ); expect(screen.getByText('No tienes tiendas configuradas aún')).toBeInTheDocument(); }); it('muestra el botón para crear tienda cuando está permitido', () => { - render(); - expect(screen.getByText('Crear nueva tienda')).toBeInTheDocument(); + render( + + + + ); + expect(screen.getByText('Crear tienda')).toBeInTheDocument(); }); - it('muestra el mensaje de límite alcanzado cuando no se pueden crear más tiendas', () => { + it('no muestra el botón de crear tienda cuando se alcanza el límite', () => { // Cambiamos el mock para simular que no se pueden crear más tiendas (useUserStores as jest.Mock).mockReturnValue({ stores: [ @@ -102,25 +136,48 @@ describe('StoreSelector', () => { storeName: 'Tienda de Prueba', storeType: 'Ropa', onboardingCompleted: true, + storeStatus: true, + }, + ], + activeStores: [ + { + storeId: 'store-1', + storeName: 'Tienda de Prueba', + storeType: 'Ropa', + onboardingCompleted: true, + storeStatus: true, }, ], + inactiveStores: [], canCreateStore: false, error: null, + storeCount: 1, }); - render(); - expect(screen.getByText('Has alcanzado el límite máximo de tiendas para tu plan actual')).toBeInTheDocument(); + render( + + + + ); + expect(screen.queryByText('Crear tienda')).not.toBeInTheDocument(); }); it('muestra un mensaje de error cuando hay un error', () => { // Cambiamos el mock para simular un error (useUserStores as jest.Mock).mockReturnValue({ stores: [], + activeStores: [], + inactiveStores: [], canCreateStore: false, error: 'Error de prueba', + storeCount: 0, }); - render(); + render( + + + + ); expect(screen.getByText('Hubo un error al cargar tus tiendas. Por favor, intenta de nuevo.')).toBeInTheDocument(); }); }); diff --git a/utils/server/cdn-url.ts b/utils/server/cdn-url.ts index 8b657ffc..6dddb694 100644 --- a/utils/server/cdn-url.ts +++ b/utils/server/cdn-url.ts @@ -22,7 +22,7 @@ */ export function getCdnBaseUrl(): string { - const appEnv = process.env.NODE_ENV || 'development'; + const appEnv = process.env.APP_ENV || 'development'; if (appEnv === 'production') { return 'https://cdn.fasttify.com';