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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion amplify/data/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@ const schema = a
})
.identifier(['storeId'])
.secondaryIndexes(index => [index('userId'), index('customDomain'), index('storeName')])
.authorization(allow => [allow.authenticated().to(['read', 'update', 'delete', 'create'])]),
.authorization(allow => [
allow.authenticated().to(['read', 'update', 'delete', 'create']),
allow.publicApiKey().to(['read']),
allow.guest().to(['read']),
allow.ownerDefinedIn('userId').to(['read', 'update', 'delete', 'create']),
]),

Product: a
.model({
Expand Down Expand Up @@ -159,6 +164,7 @@ const schema = a
.authorization(allow => [
allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']),
allow.guest().to(['read']),
allow.publicApiKey().to(['read']),
]),

Collection: a
Expand All @@ -177,6 +183,7 @@ const schema = a
.authorization(allow => [
allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']),
allow.guest().to(['read']),
allow.publicApiKey().to(['read']),
]),

StoreTemplate: a
Expand All @@ -194,6 +201,7 @@ const schema = a
.authorization(allow => [
allow.ownerDefinedIn('owner').to(['update', 'delete', 'read', 'create']),
allow.guest().to(['read']),
allow.publicApiKey().to(['read']),
]),
})
.authorization(allow => [
Expand Down
57 changes: 57 additions & 0 deletions app/(setup-layout)/first-steps/hooks/useFirstStepsSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
storeInfoSchema,
additionalSettingsSchema,
} from '@/lib/zod-schemas/first-step'
import { useTemplateUpload } from '@/app/(setup-layout)/first-steps/hooks/useTemplateUpload'
import sellingOptionsData from '@/app/(setup-layout)/first-steps/data/selling-options.json'

export const useFirstStepsSetup = () => {
Expand Down Expand Up @@ -37,9 +38,11 @@ export const useFirstStepsSetup = () => {

const [validationErrors, setValidationErrors] = useState<Record<string, any>>({})
const [saving, setSaving] = useState(false)
const [uploadingTemplate, setUploadingTemplate] = useState(false)
const { userData } = useAuthUser()
const { loading, createUserStore, createStoreWithTemplate } = useUserStoreData()
const { encryptApiKey } = useApiKeyEncryption()
const { uploadTemplate } = useTemplateUpload()

const cognitoUsername =
userData && userData['cognito:username'] ? userData['cognito:username'] : null
Expand Down Expand Up @@ -131,6 +134,34 @@ export const useFirstStepsSetup = () => {

const result = await createStoreWithTemplate(storeInput)
if (result) {
// Subir plantillas a S3 después de crear la tienda
try {
setUploadingTemplate(true)
const templateResult = await uploadTemplate({
storeId: result.store.storeId,
storeName: formData.storeName,
domain: storeInput.customDomain,
storeData: {
theme: 'modern',
currency: 'COP',
description: formData.description,
contactEmail: formData.email,
contactPhone: formData.phone,
storeAddress: formData.location,
},
})

if (templateResult) {
console.log('templateResult:', templateResult)
} else {
console.warn('Error uploading template')
}
} catch (templateError) {
console.error('Error uploading template:', templateError)
} finally {
setUploadingTemplate(false)
}

setTimeout(() => {
window.location.href = routes.store.dashboard.main(result.store.storeId)
}, 3000)
Expand Down Expand Up @@ -165,6 +196,31 @@ export const useFirstStepsSetup = () => {
const result = await createStoreWithTemplate(quickStoreInput)

if (result) {
// Subir plantillas por defecto para quick setup
try {
setUploadingTemplate(true)
const templateResult = await uploadTemplate({
storeId: result.store.storeId,
storeName: storeName,
domain: quickStoreInput.customDomain,
storeData: {
theme: 'modern',
currency: 'COP',
description: 'Tienda creada con configuración rápida',
},
})

if (templateResult) {
console.log('templateResult:', templateResult)
} else {
console.warn('Error uploading template')
}
} catch (templateError) {
console.error('Error uploading template:', templateError)
} finally {
setUploadingTemplate(false)
}

setTimeout(() => {
window.location.href = routes.store.dashboard.main(result.store.storeId)
}, 3000)
Expand Down Expand Up @@ -194,6 +250,7 @@ export const useFirstStepsSetup = () => {
setValidationErrors,
saving,
setSaving,
uploadingTemplate,
userData,
loading,
createUserStore,
Expand Down
75 changes: 75 additions & 0 deletions app/(setup-layout)/first-steps/hooks/useTemplateUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState } from 'react'

interface TemplateUploadData {
storeId: string
storeName: string
domain: string
storeData?: {
theme?: string
currency?: string
description?: string
logo?: string
banner?: string
contactEmail?: string
contactPhone?: string
storeAddress?: string
}
}

interface TemplateUploadResponse {
success: boolean
message: string
templateUrls: Record<string, string>
uploadedFiles: number
files: Array<{ key: string; path: string; size: number }>
}

interface TemplateUploadError {
error: string
message?: string
}

export function useTemplateUpload() {
const [isUploading, setIsUploading] = useState(false)
const [error, setError] = useState<string | null>(null)

const uploadTemplate = async (
data: TemplateUploadData
): Promise<TemplateUploadResponse | null> => {
setIsUploading(true)
setError(null)

try {
const response = await fetch('/api/stores/template', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})

const result = await response.json()

if (!response.ok) {
const errorData = result as TemplateUploadError
throw new Error(errorData.message || errorData.error || 'Failed to upload template')
}

return result as TemplateUploadResponse
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'
setError(errorMessage)
console.error('Template upload error:', err)
return null
} finally {
setIsUploading(false)
}
}

return {
uploadTemplate,
isUploading,
error,
clearError: () => setError(null),
}
}
126 changes: 114 additions & 12 deletions app/[store]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,120 @@
'use client'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { storeRenderer } from '@/lib/store-renderer'

import { useParams } from 'next/navigation'
interface StorePageProps {
params: Promise<{
store: string
}>
searchParams: Promise<{
path?: string
}>
}

/**
* Página principal de tienda con SSR
* Maneja todas las rutas de tienda: /, /products/slug, /collections/slug
*/
export default async function StorePage({ params, searchParams }: StorePageProps) {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
const { store } = resolvedParams
const path = resolvedSearchParams.path || '/'

try {
// Resolver dominio completo (el middleware ya reescribió la URL)
const domain = `${store}.fasttify.com`

// Renderizar página usando el sistema
const result = await storeRenderer.renderPage(domain, path)

// Retornar HTML renderizado como componente dangerouslySetInnerHTML
// Esto permite SSR completo con SEO optimizado
return <div dangerouslySetInnerHTML={{ __html: result.html }} />
} catch (error: any) {
console.error(`Error rendering store page ${store}${path}:`, error)

// Mostrar 404 para tiendas no encontradas
if (error.type === 'STORE_NOT_FOUND' || error.statusCode === 404) {
notFound()
}

// Para otros errores, mostrar página de error
throw error
}
}

/**
* Genera metadata SEO para la página
*/
export async function generateMetadata({
params,
searchParams,
}: StorePageProps): Promise<Metadata> {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
const { store } = resolvedParams
const path = resolvedSearchParams.path || '/'

try {
const domain = `${store}.fasttify.com`
const result = await storeRenderer.renderPage(domain, path)

export default function StorePage() {
const params = useParams()
const store = (params.store as string) || undefined
const { metadata } = result

if (!store) {
return <div>No se encontró la tienda</div>
return {
title: metadata.title,
description: metadata.description,
alternates: {
canonical: metadata.canonical,
},
openGraph: metadata.openGraph
? {
title: metadata.openGraph.title,
description: metadata.openGraph.description,
url: metadata.openGraph.url,
type: metadata.openGraph.type as any,
images: metadata.openGraph.image ? [metadata.openGraph.image] : undefined,
siteName: metadata.openGraph.site_name,
}
: undefined,
twitter: metadata.openGraph
? {
card: 'summary_large_image',
title: metadata.openGraph.title,
description: metadata.openGraph.description,
images: metadata.openGraph.image ? [metadata.openGraph.image] : undefined,
}
: undefined,
other: metadata.schema
? {
'application-ld+json': JSON.stringify(metadata.schema),
}
: undefined,
}
} catch (error) {
console.error(`Error generating metadata for ${store}${path}:`, error)

// Metadata por defecto para errores
return {
title: `${store} - Tienda Online`,
description: `Descubre productos únicos en ${store}. ¡Compra online!`,
}
}
}

/**
* Configurar revalidación de páginas para ISR
* Esto permite que las páginas se regeneren automáticamente
*/
export const revalidate = 1800 // 30 minutos

return (
<div>
<h1>Tienda: {store}</h1>
</div>
)
/**
* Configurar generación estática para tiendas populares
* (esto se ejecutaría en build time)
*/
export async function generateStaticParams() {
// TODO: Obtener lista de tiendas activas desde la base de datos
// Por ahora retornamos array vacío para generar páginas bajo demanda
return []
}
Loading