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 && (
+
+ {primaryAction.content}
+
+ )}
+
+
+ );
+}
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) => (
-
- {
- window.location.href = routes.store.dashboard.main(store.storeId);
- }}
- className="w-full flex items-center gap-3 p-4 rounded-xl hover:bg-gray-50 active:bg-gray-100 transition-colors border border-gray-100 shadow-sm"
- aria-label={`Seleccionar tienda ${store.storeName}`}>
-
-
-
{store.storeName}
-
{store.storeType}
-
-
-
- ))
- ) : (
- // No stores state
-
-
-
-
No tienes tiendas configuradas aún
-
-
- )}
-
-
-
-
- {canCreateStore ? (
-
-
-
- Crear nueva tienda
-
-
- ) : (
- 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 (
-
-
-
-
Selecciona una tienda
-
para continuar a tu dashboard
-
-
-
- }>
-
-
+
+ {/* Card principal con todo el contenido - responsive */}
+
+ {/* Header con logo y UserMenu dentro de la card */}
+
+
+
+
+
+
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 ? (
-
+
+
+
setLogoFile(null)} variant="plain">
Cambiar
@@ -177,7 +175,16 @@ export function LogoUploader() {
{faviconUrl ? (
-
+
+
+
setFaviconFile(null)} variant="plain">
Cambiar
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
-
-
-
-
- Ver tienda
-
-
+
@@ -123,10 +85,14 @@ export function ThemePreview() {
Temas Disponibles
@@ -156,15 +122,6 @@ export function ThemePreview() {
-
-
-
-
- Temas Personalizados
-
-
-
-
diff --git a/app/store/components/store-setup/components/EcommerceSetup.tsx b/app/store/components/store-setup/components/EcommerceSetup.tsx
index 4d4dbf68..dd6a5297 100644
--- a/app/store/components/store-setup/components/EcommerceSetup.tsx
+++ b/app/store/components/store-setup/components/EcommerceSetup.tsx
@@ -8,7 +8,6 @@ import { useUserStoreData } from '@/app/(setup)/first-steps/hooks/useUserStoreDa
import useStoreDataStore from '@/context/core/storeDataStore';
import { getStoreId } from '@/utils/client/store-utils';
-// Import new components
import { SetupAdBanner } from '@/app/store/components/store-setup/components/ecommerce-setup-parts/SetupAdBanner';
import { SetupHeader } from '@/app/store/components/store-setup/components/ecommerce-setup-parts/SetupHeader';
import { SetupProgress } from '@/app/store/components/store-setup/components/ecommerce-setup-parts/SetupProgress';
diff --git a/app/store/components/theme-management/components/ImportThemeDropdown.tsx b/app/store/components/theme-management/components/ImportThemeDropdown.tsx
new file mode 100644
index 00000000..8747d3da
--- /dev/null
+++ b/app/store/components/theme-management/components/ImportThemeDropdown.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { ActionList, Button, Popover } from '@shopify/polaris';
+import { CaretDownIcon, UploadIcon } from '@shopify/polaris-icons';
+import { useState } from 'react';
+import type { ImportThemeDropdownProps } from '@/app/store/components/theme-management/types/theme-types';
+
+export function ImportThemeDropdown({ onUploadTheme }: ImportThemeDropdownProps) {
+ const [activePopover, setActivePopover] = useState(false);
+
+ const handleAction = (action: string): void => {
+ if (action === 'upload') {
+ onUploadTheme();
+ }
+ setActivePopover(false);
+ };
+
+ const dropdownActions = [
+ {
+ content: 'Subir archivo zip',
+ icon: UploadIcon,
+ onAction: () => handleAction('upload'),
+ },
+ ];
+
+ return (
+
setActivePopover(!activePopover)}>
+ Importar tema
+
+ }
+ 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}
+
+
+ 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
-
-
- setShowUploadModal(true)}>
- Subir nuevo tema
-
-
-
-
-
- {themes.length === 0 ? (
-
- Sube tu primer tema para personalizar la apariencia de tu tienda.
- setShowUploadModal(true)}>
- Subir tema
-
-
- ) : (
-
+ {activeTheme ? (
+
+ ) : (
+
+
+
+
+ No hay tema activo. Sube un tema para comenzar.
+
+
+ Subir tema
+
+
+
+
)}
-
+
+ {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({
+
+
+
+ Cancelar
+
+
+ Activar
+
+
+
)}
{/* Modal de confirmación de eliminación */}
{showDeleteModal && selectedTheme && (
-
+
@@ -105,6 +86,23 @@ export function ThemeModals({
+
+
+
+ Cancelar
+
+
+ Eliminar
+
+
+
)}
>
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 */}
+
+
+
+
+ {/* Información del tema */}
+
+
+
+
+
+
+
+
+
+ {theme.name}
+
+ Tema actual
+
+
+ Agregado: {formatDate(theme.createdAt)}
+
+
+ Versión {theme.version}
+
+
+
+
+
+ setActivePopover(!activePopover)} />}
+ onClose={() => setActivePopover(false)}
+ preferredPosition="below"
+ preferredAlignment="right">
+
+
+
+
+ Personalizar
+
+
+
+
+
+
+ );
+}
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 && (
-
- Procesar Tema
-
- )}
-
- Eliminar
-
-
-
-
- )}
-
- {/* 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'}
-
- ) : (
-
-
- {isConfirming ? 'Confirmando...' : 'Confirmar y Activar Tema'}
-
-
- Cancelar
-
-
- )}
-
- )}
+
+ ) : 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';