feat: 对接 templateDetail 页面后端接口
- 创建 hooks/use-error.ts 统一错误处理 - 创建 hooks/use-template-detail.ts 获取模板详情 - 创建 hooks/use-template-generations.ts 获取模板生成记录 - 更新 hooks 使用 handleError 统一错误处理 - 优化 SearchResultsGrid 组件,复用 TemplateGeneration 类型 - 删除不必要的类型转换和重复代码 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,66 +1,79 @@
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
StatusBar as RNStatusBar,
|
||||
ActivityIndicator,
|
||||
} from 'react-native'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { LeftArrowIcon } from '@/components/icon'
|
||||
import SearchResultsGrid from '@/components/SearchResultsGrid'
|
||||
import { useTemplateDetail, useTemplateGenerations, type TemplateGeneration } from '@/hooks'
|
||||
|
||||
// 模板详情数据 - 根据实际需求可以改为从 API 获取
|
||||
const templateDetailData = [
|
||||
{
|
||||
id: 1,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/android-icon-background.png'),
|
||||
height: 214,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/android-icon-background.png'),
|
||||
height: 236,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/favicon.png'),
|
||||
height: 214,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/icon.png'),
|
||||
height: 200,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/android-icon-background.png'),
|
||||
height: 220,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '猫咪圣诞写真',
|
||||
image: require('@/assets/images/android-icon-background.png'),
|
||||
height: 214,
|
||||
},
|
||||
]
|
||||
const CARD_HEIGHTS = [214, 236, 200, 220, 210, 225]
|
||||
|
||||
export default function TemplateDetailScreen() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const params = useLocalSearchParams()
|
||||
const templateId = typeof params.id === 'string' ? params.id : undefined
|
||||
|
||||
const { data: templateDetail, loading: templateLoading, error: templateError, execute: loadTemplateDetail } = useTemplateDetail()
|
||||
const { generations, loading: generationsLoading, execute: loadGenerations } = useTemplateGenerations()
|
||||
|
||||
useEffect(() => {
|
||||
if (templateId) {
|
||||
loadTemplateDetail({ id: templateId })
|
||||
loadGenerations({ templateId, page: 1, limit: 20 })
|
||||
}
|
||||
}, [templateId, loadTemplateDetail, loadGenerations])
|
||||
|
||||
// 直接使用 TemplateGeneration 数据,只添加必要的 height 字段
|
||||
const displayData = generations.map((generation, index) => ({
|
||||
...generation,
|
||||
height: CARD_HEIGHTS[index % CARD_HEIGHTS.length],
|
||||
title: generation.template?.title || generation.template?.titleEn || '',
|
||||
image: generation.resultUrl?.[0] || generation.originalUrl || '',
|
||||
}))
|
||||
|
||||
if (templateLoading && !templateDetail) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<StatusBar style="light" />
|
||||
<RNStatusBar barStyle="light-content" />
|
||||
<View style={styles.centerContainer}>
|
||||
<ActivityIndicator size="large" color="#FFE500" />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
if (templateError && !templateDetail) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<StatusBar style="light" />
|
||||
<RNStatusBar barStyle="light-content" />
|
||||
<View style={styles.centerContainer}>
|
||||
<Text style={styles.errorText}>加载失败,请返回重试</Text>
|
||||
<Pressable style={styles.retryButton} onPress={() => router.back()}>
|
||||
<Text style={styles.retryButtonText}>返回</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<StatusBar style="light" />
|
||||
<RNStatusBar barStyle="light-content" />
|
||||
|
||||
|
||||
{/* 顶部导航栏 */}
|
||||
<View style={styles.header}>
|
||||
<Pressable
|
||||
@@ -74,15 +87,22 @@ export default function TemplateDetailScreen() {
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.mainTitle}>
|
||||
{t('templateDetail.title')}
|
||||
{templateDetail?.title || templateDetail?.titleEn || t('templateDetail.title')}
|
||||
</Text>
|
||||
<Text style={styles.subTitle}>
|
||||
{t('templateDetail.subtitle')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 图片网格 */}
|
||||
<SearchResultsGrid results={templateDetailData} />
|
||||
{/* 加载更多指示器 */}
|
||||
{generationsLoading && generations.length > 0 && (
|
||||
<View style={styles.loadingMoreContainer}>
|
||||
<ActivityIndicator size="small" color="#FFE500" />
|
||||
<Text style={styles.loadingMoreText}>加载中...</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<SearchResultsGrid results={displayData} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
@@ -92,6 +112,12 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: '#090A0B',
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -117,5 +143,32 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
fontWeight: '400',
|
||||
},
|
||||
errorText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: '#FF6699',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
loadingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
loadingMoreText: {
|
||||
color: '#ABABAB',
|
||||
fontSize: 12,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -12,17 +12,14 @@ import { useRouter } from 'expo-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { WhiteStarIcon } from '@/components/icon'
|
||||
import type { TemplateGeneration } from '@/hooks'
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window')
|
||||
|
||||
interface SearchResultItem {
|
||||
id: number
|
||||
title: string
|
||||
image: any
|
||||
interface SearchResultItem extends TemplateGeneration {
|
||||
height: number
|
||||
videoUrl?: any
|
||||
thumbnailUrl?: any
|
||||
duration?: string
|
||||
title: string
|
||||
image: string
|
||||
}
|
||||
|
||||
interface SearchResultsGridProps {
|
||||
@@ -33,27 +30,24 @@ export default function SearchResultsGrid({ results }: SearchResultsGridProps) {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [gridWidth, setGridWidth] = useState(screenWidth)
|
||||
|
||||
const horizontalPadding = 8 * 2 // gridContainer 的左右 padding
|
||||
const cardGap = 5 // 两个卡片之间的间距
|
||||
|
||||
const horizontalPadding = 8 * 2
|
||||
const cardGap = 5
|
||||
const cardWidth = (gridWidth - horizontalPadding - cardGap) / 2
|
||||
|
||||
const handleSameStylePress = (item: SearchResultItem) => {
|
||||
// 构建模板数据,传递给 generateVideo 页面
|
||||
const templateData = {
|
||||
id: item.id,
|
||||
videoUrl: item.videoUrl || item.image, // 如果没有 videoUrl,使用 image
|
||||
thumbnailUrl: item.thumbnailUrl || item.image, // 如果没有 thumbnailUrl,使用 image
|
||||
title: item.title,
|
||||
duration: item.duration || '00:00', // 默认时长
|
||||
id: item.template?.id,
|
||||
videoUrl: item.resultUrl?.[0] || item.originalUrl,
|
||||
thumbnailUrl: item.template?.coverImageUrl,
|
||||
title: item.template?.title || item.template?.titleEn || '',
|
||||
}
|
||||
router.push({
|
||||
pathname: '/generateVideo' as any,
|
||||
params: {
|
||||
template: JSON.stringify(templateData),
|
||||
},
|
||||
params: { template: JSON.stringify(templateData) },
|
||||
} as any)
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
@@ -61,6 +55,7 @@ export default function SearchResultsGrid({ results }: SearchResultsGridProps) {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
@@ -84,16 +79,9 @@ export default function SearchResultsGrid({ results }: SearchResultsGridProps) {
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.cardImageContainer,
|
||||
{ height: item.height },
|
||||
]}
|
||||
style={[styles.cardImageContainer, { height: item.height }]}
|
||||
>
|
||||
<Image
|
||||
source={item.image}
|
||||
style={styles.cardImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<Image source={item.image} style={styles.cardImage} contentFit="cover" />
|
||||
</View>
|
||||
<Text style={styles.cardTitle} numberOfLines={1}>
|
||||
{item.title}
|
||||
@@ -116,9 +104,6 @@ const styles = StyleSheet.create({
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
// paddingBottom: 100,
|
||||
},
|
||||
gridContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
@@ -142,18 +127,23 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginBottom: 8,
|
||||
position: 'relative',
|
||||
},
|
||||
cardImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
sameStyleButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: '#262A31',
|
||||
height:32,
|
||||
height: 32,
|
||||
marginHorizontal: 8,
|
||||
marginTop: 10,
|
||||
marginBottom: 8,
|
||||
@@ -166,12 +156,6 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
lineHeight: 17,
|
||||
},
|
||||
cardTitle: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: '#090A0B',
|
||||
|
||||
6
hooks/index.ts
Normal file
6
hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { useActivates } from './use-activates'
|
||||
export { useCategories } from './use-categories'
|
||||
export { useTemplateActions } from './use-template-actions'
|
||||
export { useTemplates, type TemplateDetail } from './use-templates'
|
||||
export { useTemplateDetail, type TemplateDetail as TemplateDetailType } from './use-template-detail'
|
||||
export { useTemplateGenerations, type TemplateGeneration } from './use-template-generations'
|
||||
@@ -1,17 +1,38 @@
|
||||
import { OWNER_ID } from "@/lib/auth"
|
||||
import { ApiError } from "@/lib/types"
|
||||
import { root } from '@repo/core'
|
||||
import { ActivityController, ListActivitiesResult } from "@repo/sdk"
|
||||
import { useState } from "react"
|
||||
import { ActivityController, type ListActivitiesInput, type ListActivitiesResult } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useActivates = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [data, setData] = useState<ListActivitiesResult>()
|
||||
const load = async () => {
|
||||
|
||||
const load = async (params?: ListActivitiesInput) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const c = root.get(ActivityController)
|
||||
const data = await c.list({ page: 1, limit: 10, isActive: true, orderBy: 'sortOrder', order: 'desc', ownerId: OWNER_ID })
|
||||
const activity = root.get(ActivityController)
|
||||
const { data, error } = await handleError(
|
||||
async () =>
|
||||
await activity.list({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
isActive: true,
|
||||
orderBy: 'sortOrder',
|
||||
order: 'desc',
|
||||
ownerId: OWNER_ID,
|
||||
...params,
|
||||
}),
|
||||
)
|
||||
if (error) {
|
||||
setError(error)
|
||||
return
|
||||
}
|
||||
setData(data)
|
||||
} catch (e) {
|
||||
setError(e as ApiError)
|
||||
@@ -24,6 +45,6 @@ export const useActivates = () => {
|
||||
load,
|
||||
loading,
|
||||
error,
|
||||
data
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
hooks/use-categories.ts
Normal file
52
hooks/use-categories.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { root } from '@repo/core'
|
||||
import { CategoryController, type ListCategoriesInput, type ListCategoriesResult } from '@repo/sdk'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useCategories = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [data, setData] = useState<ListCategoriesResult>()
|
||||
|
||||
const load = async (params?: ListCategoriesInput) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const category = root.get(CategoryController)
|
||||
const { data, error } = await handleError(
|
||||
async () =>
|
||||
await category.list({
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
isActive: true,
|
||||
orderBy: 'sortOrder',
|
||||
order: 'desc',
|
||||
withChildren: true,
|
||||
ownerId: OWNER_ID,
|
||||
...params,
|
||||
}),
|
||||
)
|
||||
if (error) {
|
||||
setError(error)
|
||||
return
|
||||
}
|
||||
setData(data)
|
||||
} catch (e) {
|
||||
setError(e as ApiError)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
load,
|
||||
loading,
|
||||
error,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
10
hooks/use-error.ts
Normal file
10
hooks/use-error.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
export async function handleError<T>(cb: () => Promise<T>) {
|
||||
try {
|
||||
const data = await cb()
|
||||
return { data, error: null }
|
||||
} catch (e) {
|
||||
return { data: null, error: e as ApiError }
|
||||
}
|
||||
}
|
||||
52
hooks/use-template-detail.ts
Normal file
52
hooks/use-template-detail.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { root } from '@repo/core'
|
||||
import { TemplateController, type TemplateDetail } from '@repo/sdk'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
interface UseTemplateDetailParams {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const useTemplateDetail = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [data, setData] = useState<TemplateDetail | undefined>()
|
||||
|
||||
const execute = useCallback(async (params: UseTemplateDetailParams) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const template = root.get(TemplateController)
|
||||
const { data, error } = await handleError(async () => await template.get(params.id))
|
||||
|
||||
if (error) {
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
return { data: undefined, error }
|
||||
}
|
||||
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
return { data, error: null }
|
||||
}, [])
|
||||
|
||||
const refetch = useCallback(
|
||||
(params: UseTemplateDetailParams) => {
|
||||
return execute(params)
|
||||
},
|
||||
[execute],
|
||||
)
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
execute,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
export type { TemplateDetail }
|
||||
108
hooks/use-template-generations.ts
Normal file
108
hooks/use-template-generations.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { root } from '@repo/core'
|
||||
import {
|
||||
TemplateGenerationController,
|
||||
type ListTemplateGenerationsInput,
|
||||
type ListTemplateGenerationsResult,
|
||||
type TemplateGeneration,
|
||||
} from '@repo/sdk'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
export const useTemplateGenerations = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [data, setData] = useState<ListTemplateGenerationsResult | undefined>()
|
||||
const currentPageRef = useRef(1)
|
||||
const hasMoreRef = useRef(true)
|
||||
|
||||
const execute = useCallback(async (params?: ListTemplateGenerationsInput) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
currentPageRef.current = params?.page || 1
|
||||
|
||||
const templateGeneration = root.get(TemplateGenerationController)
|
||||
const { data, error } = await handleError(
|
||||
async () =>
|
||||
await templateGeneration.list({
|
||||
page: params?.page || 1,
|
||||
limit: params?.limit || 20,
|
||||
...params,
|
||||
}),
|
||||
)
|
||||
if (error) {
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
return { data: undefined, error }
|
||||
}
|
||||
|
||||
const generations = data?.data || []
|
||||
hasMoreRef.current = generations.length >= (params?.limit || 20)
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
return { data, error: null }
|
||||
}, [])
|
||||
|
||||
const loadMore = useCallback(
|
||||
async (params?: Omit<ListTemplateGenerationsInput, 'page'>) => {
|
||||
if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
|
||||
|
||||
setLoadingMore(true)
|
||||
const nextPage = currentPageRef.current + 1
|
||||
|
||||
const templateGeneration = root.get(TemplateGenerationController)
|
||||
const { data: newData, error } = await handleError(
|
||||
async () =>
|
||||
await templateGeneration.list({
|
||||
page: nextPage,
|
||||
limit: params?.limit || 20,
|
||||
...params,
|
||||
}),
|
||||
)
|
||||
|
||||
if (error) {
|
||||
setLoadingMore(false)
|
||||
return { data: undefined, error }
|
||||
}
|
||||
|
||||
const newGenerations = newData?.data || []
|
||||
hasMoreRef.current = newGenerations.length >= (params?.limit || 20)
|
||||
currentPageRef.current = nextPage
|
||||
|
||||
setData((prev) => ({
|
||||
...newData,
|
||||
data: [...(prev?.data || []), ...newGenerations],
|
||||
}))
|
||||
setLoadingMore(false)
|
||||
return { data: newData, error: null }
|
||||
},
|
||||
[loading, loadingMore],
|
||||
)
|
||||
|
||||
const refetch = useCallback(
|
||||
(params?: ListTemplateGenerationsInput) => {
|
||||
hasMoreRef.current = true
|
||||
return execute(params)
|
||||
},
|
||||
[execute],
|
||||
)
|
||||
|
||||
return {
|
||||
data,
|
||||
generations: data?.data || [],
|
||||
loading,
|
||||
loadingMore,
|
||||
error,
|
||||
execute,
|
||||
refetch,
|
||||
loadMore,
|
||||
hasMore: hasMoreRef.current,
|
||||
}
|
||||
}
|
||||
|
||||
export type { TemplateGeneration }
|
||||
109
hooks/use-templates.ts
Normal file
109
hooks/use-templates.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { root } from '@repo/core'
|
||||
import { type ListTemplatesInput, type ListTemplatesResult, TemplateController, type TemplateDetail } from '@repo/sdk'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
|
||||
import { type ApiError } from '@/lib/types'
|
||||
|
||||
import { handleError } from './use-error'
|
||||
|
||||
const OWNER_ID = process.env.EXPO_PUBLIC_OWNER_ID || ''
|
||||
|
||||
type ListTemplatesParams = Omit<ListTemplatesInput, 'ownerId'>
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
limit: 20,
|
||||
sortBy: 'createdAt' as const,
|
||||
sortOrder: 'desc' as const,
|
||||
}
|
||||
|
||||
export const useTemplates = (initialParams?: ListTemplatesParams) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [error, setError] = useState<ApiError | null>(null)
|
||||
const [data, setData] = useState<ListTemplatesResult>()
|
||||
const currentPageRef = useRef(1)
|
||||
const hasMoreRef = useRef(true)
|
||||
|
||||
const execute = useCallback(async (params?: ListTemplatesParams) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
currentPageRef.current = params?.page || 1
|
||||
|
||||
const template = root.get(TemplateController)
|
||||
const { data, error } = await handleError(
|
||||
async () =>
|
||||
await template.list({
|
||||
...DEFAULT_PARAMS,
|
||||
...initialParams,
|
||||
...params,
|
||||
ownerId: OWNER_ID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (error) {
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
return { data: undefined, error }
|
||||
}
|
||||
|
||||
const templates = data?.templates || []
|
||||
hasMoreRef.current = templates.length >= (params?.limit || DEFAULT_PARAMS.limit)
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
return { data, error: null }
|
||||
}, [initialParams])
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (loadingMore || loading || !hasMoreRef.current) return { data: undefined, error: null }
|
||||
|
||||
setLoadingMore(true)
|
||||
const nextPage = currentPageRef.current + 1
|
||||
|
||||
const template = root.get(TemplateController)
|
||||
const { data: newData, error } = await handleError(
|
||||
async () =>
|
||||
await template.list({
|
||||
...DEFAULT_PARAMS,
|
||||
...initialParams,
|
||||
page: nextPage,
|
||||
ownerId: OWNER_ID,
|
||||
}),
|
||||
)
|
||||
|
||||
if (error) {
|
||||
setLoadingMore(false)
|
||||
return { data: undefined, error }
|
||||
}
|
||||
|
||||
const newTemplates = newData?.templates || []
|
||||
hasMoreRef.current = newTemplates.length >= DEFAULT_PARAMS.limit
|
||||
currentPageRef.current = nextPage
|
||||
|
||||
setData((prev) => ({
|
||||
...newData,
|
||||
templates: [...(prev?.templates || []), ...newTemplates],
|
||||
}))
|
||||
setLoadingMore(false)
|
||||
return { data: newData, error: null }
|
||||
}, [loading, loadingMore, initialParams])
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
hasMoreRef.current = true
|
||||
return execute()
|
||||
}, [execute])
|
||||
|
||||
return {
|
||||
data,
|
||||
templates: data?.templates || [],
|
||||
loading,
|
||||
loadingMore,
|
||||
error,
|
||||
execute,
|
||||
refetch,
|
||||
loadMore,
|
||||
hasMore: hasMoreRef.current,
|
||||
}
|
||||
}
|
||||
|
||||
export type { TemplateDetail }
|
||||
|
||||
Reference in New Issue
Block a user