Initial commit: Expo app with Better Auth integration
- Complete Expo React Native app setup with TypeScript - Better Auth authentication system integration - Secure storage implementation for session tokens - Authentication flow with login/logout functionality - API client configuration for backend communication - Responsive UI components with themed styling - Expo Router navigation setup - Development configuration and scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
13
hooks/use-auth.ts
Normal file
13
hooks/use-auth.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useSession } from "@/lib/auth/client";
|
||||
|
||||
export function useAuth() {
|
||||
const { data: session, isPending, error } = useSession();
|
||||
|
||||
return {
|
||||
user: session?.user ?? null,
|
||||
session: session ?? null,
|
||||
isLoading: isPending,
|
||||
isAuthenticated: !!session?.user,
|
||||
error: error ?? null,
|
||||
};
|
||||
}
|
||||
1
hooks/use-color-scheme.ts
Normal file
1
hooks/use-color-scheme.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useColorScheme } from 'react-native';
|
||||
21
hooks/use-color-scheme.web.ts
Normal file
21
hooks/use-color-scheme.web.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useColorScheme as useRNColorScheme } from 'react-native';
|
||||
|
||||
/**
|
||||
* To support static rendering, this value needs to be re-calculated on the client side for web
|
||||
*/
|
||||
export function useColorScheme() {
|
||||
const [hasHydrated, setHasHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasHydrated(true);
|
||||
}, []);
|
||||
|
||||
const colorScheme = useRNColorScheme();
|
||||
|
||||
if (hasHydrated) {
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
return 'light';
|
||||
}
|
||||
320
hooks/use-template-run.ts
Normal file
320
hooks/use-template-run.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import {
|
||||
runTemplate,
|
||||
getTemplateGeneration,
|
||||
pollTemplateGeneration
|
||||
} from '@/lib/api/template-runs';
|
||||
import {
|
||||
RunTemplateData,
|
||||
TemplateGeneration,
|
||||
RunProgress,
|
||||
GenerationStatus
|
||||
} from '@/lib/types/template-run';
|
||||
|
||||
type UseTemplateRunState = 'idle' | 'submitting' | 'running' | 'completed' | 'error';
|
||||
|
||||
interface UseTemplateRunOptions {
|
||||
onSuccess?: (result: TemplateGeneration) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onProgress?: (progress: RunProgress) => void;
|
||||
}
|
||||
|
||||
export function useTemplateRun(options: UseTemplateRunOptions = {}) {
|
||||
const [state, setState] = useState<UseTemplateRunState>('idle');
|
||||
const [progress, setProgress] = useState<RunProgress>({
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
message: '准备开始...',
|
||||
});
|
||||
const [result, setResult] = useState<TemplateGeneration | null>(null);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const pollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// 清理轮询
|
||||
const clearPolling = useCallback(() => {
|
||||
if (pollTimeoutRef.current) {
|
||||
clearTimeout(pollTimeoutRef.current);
|
||||
pollTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 更新进度状态
|
||||
const updateProgress = useCallback((generation: TemplateGeneration) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
let progressPercent = 0;
|
||||
let message = '处理中...';
|
||||
|
||||
switch (generation.status) {
|
||||
case 'pending':
|
||||
progressPercent = 10;
|
||||
message = '任务已提交,等待开始...';
|
||||
break;
|
||||
case 'running':
|
||||
progressPercent = 50;
|
||||
message = '正在生成内容,请稍候...';
|
||||
break;
|
||||
case 'completed':
|
||||
progressPercent = 100;
|
||||
message = '生成完成!';
|
||||
break;
|
||||
case 'failed':
|
||||
progressPercent = 0;
|
||||
message = '生成失败,请重试';
|
||||
break;
|
||||
}
|
||||
|
||||
const newProgress: RunProgress = {
|
||||
status: generation.status,
|
||||
progress: progressPercent,
|
||||
message,
|
||||
result: generation,
|
||||
};
|
||||
|
||||
setProgress(newProgress);
|
||||
options.onProgress?.(newProgress);
|
||||
}, [options.onProgress]);
|
||||
|
||||
// 处理轮询完成
|
||||
const handlePollComplete = useCallback((generation: TemplateGeneration) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
clearPolling();
|
||||
setResult(generation);
|
||||
setState('completed');
|
||||
updateProgress(generation);
|
||||
options.onSuccess?.(generation);
|
||||
}, [clearPolling, updateProgress, options.onSuccess]);
|
||||
|
||||
// 处理轮询错误
|
||||
const handlePollError = useCallback((error: Error) => {
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
clearPolling();
|
||||
setError(error);
|
||||
setState('error');
|
||||
options.onError?.(error);
|
||||
}, [clearPolling, options.onError]);
|
||||
|
||||
// 运行模板
|
||||
const executeTemplate = useCallback(async (
|
||||
templateId: string,
|
||||
data: RunTemplateData
|
||||
) => {
|
||||
if (state === 'submitting' || state === 'running') {
|
||||
console.warn('模板正在运行中,请勿重复提交');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 重置状态
|
||||
setState('submitting');
|
||||
setError(null);
|
||||
setResult(null);
|
||||
setProgress({
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
message: '正在提交任务...',
|
||||
});
|
||||
|
||||
// 提交模板运行
|
||||
const runResponse = await runTemplate(templateId, data);
|
||||
|
||||
if (!runResponse.success || !runResponse.data) {
|
||||
throw new Error('提交任务失败');
|
||||
}
|
||||
|
||||
const generationId = runResponse.data;
|
||||
setState('running');
|
||||
setProgress({
|
||||
status: 'pending',
|
||||
progress: 10,
|
||||
message: '任务已提交,开始生成...',
|
||||
});
|
||||
|
||||
// 开始轮询
|
||||
pollTemplateGeneration(
|
||||
generationId,
|
||||
handlePollComplete,
|
||||
handlePollError
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.error('运行模板失败:', err);
|
||||
|
||||
clearPolling();
|
||||
setError(err);
|
||||
setState('error');
|
||||
setProgress({
|
||||
status: 'failed',
|
||||
progress: 0,
|
||||
message: err.message || '运行失败',
|
||||
});
|
||||
|
||||
options.onError?.(err);
|
||||
}
|
||||
}, [state, clearPolling, handlePollComplete, handlePollError, options.onError]);
|
||||
|
||||
// 手动刷新状态
|
||||
const refreshStatus = useCallback(async (generationId: string) => {
|
||||
try {
|
||||
const response = await getTemplateGeneration(generationId);
|
||||
if (response.success && response.data) {
|
||||
updateProgress(response.data);
|
||||
|
||||
if (response.data.status === 'completed') {
|
||||
handlePollComplete(response.data);
|
||||
} else if (response.data.status === 'failed') {
|
||||
handlePollError(new Error('任务执行失败'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新状态失败:', error);
|
||||
}
|
||||
}, [updateProgress, handlePollComplete, handlePollError]);
|
||||
|
||||
// 取消运行
|
||||
const cancelRun = useCallback(() => {
|
||||
if (state === 'submitting' || state === 'running') {
|
||||
clearPolling();
|
||||
setState('idle');
|
||||
setProgress({
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
message: '已取消',
|
||||
});
|
||||
|
||||
Alert.alert('提示', '任务已取消');
|
||||
}
|
||||
}, [state, clearPolling]);
|
||||
|
||||
// 重置状态
|
||||
const reset = useCallback(() => {
|
||||
clearPolling();
|
||||
setState('idle');
|
||||
setProgress({
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
message: '准备开始...',
|
||||
});
|
||||
setResult(null);
|
||||
setError(null);
|
||||
}, [clearPolling]);
|
||||
|
||||
// 重试
|
||||
const retry = useCallback((templateId: string, data: RunTemplateData) => {
|
||||
reset();
|
||||
executeTemplate(templateId, data);
|
||||
}, [reset, executeTemplate]);
|
||||
|
||||
// 组件卸载时清理
|
||||
const cleanup = useCallback(() => {
|
||||
isMountedRef.current = false;
|
||||
clearPolling();
|
||||
}, [clearPolling]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
state,
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
isLoading: state === 'submitting' || state === 'running',
|
||||
isCompleted: state === 'completed',
|
||||
isError: state === 'error',
|
||||
|
||||
// 操作
|
||||
executeTemplate,
|
||||
refreshStatus,
|
||||
cancelRun,
|
||||
reset,
|
||||
retry,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
// 便捷的 Hook 用于表单数据管理
|
||||
export function useTemplateFormData(initialData: RunTemplateData = {}) {
|
||||
const [formData, setFormData] = useState<RunTemplateData>(initialData);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const updateField = useCallback((fieldName: string, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [fieldName]: value }));
|
||||
// 清除该字段的错误
|
||||
if (errors[fieldName]) {
|
||||
setErrors(prev => {
|
||||
const newErrors = { ...prev };
|
||||
delete newErrors[fieldName];
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
const setFieldError = useCallback((fieldName: string, error: string) => {
|
||||
setErrors(prev => ({ ...prev, [fieldName]: error }));
|
||||
}, []);
|
||||
|
||||
const clearErrors = useCallback(() => {
|
||||
setErrors({});
|
||||
}, []);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setFormData(initialData);
|
||||
clearErrors();
|
||||
}, [initialData, clearErrors]);
|
||||
|
||||
const validateForm = useCallback((validation: Record<string, any> = {}) => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
let isValid = true;
|
||||
|
||||
Object.entries(validation).forEach(([fieldName, rules]) => {
|
||||
const value = formData[fieldName];
|
||||
|
||||
// 检查必填
|
||||
if (rules.required && (value === undefined || value === null || value === '')) {
|
||||
newErrors[fieldName] = `${fieldName} 是必填项`;
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果值为空且不是必填,跳过其他验证
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 其他验证规则
|
||||
if (rules.min && typeof value === 'string' && value.length < rules.min) {
|
||||
newErrors[fieldName] = `${fieldName} 至少需要 ${rules.min} 个字符`;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (rules.max && typeof value === 'string' && value.length > rules.max) {
|
||||
newErrors[fieldName] = `${fieldName} 最多 ${rules.max} 个字符`;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (rules.pattern && typeof value === 'string' && !new RegExp(rules.pattern).test(value)) {
|
||||
newErrors[fieldName] = rules.message || `${fieldName} 格式不正确`;
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
return { isValid, errors: newErrors };
|
||||
}, [formData]);
|
||||
|
||||
return {
|
||||
formData,
|
||||
errors,
|
||||
updateField,
|
||||
setFieldError,
|
||||
clearErrors,
|
||||
resetForm,
|
||||
validateForm,
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
};
|
||||
}
|
||||
21
hooks/use-theme-color.ts
Normal file
21
hooks/use-theme-color.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Learn more about light and dark modes:
|
||||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/theme';
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user