modules/frontend/Write-Frontend-RN-Feature.ps1
|
param( [string]$ProjectName, [string]$OutputPath, [string]$Platform, [string[]]$Entities, [string]$Language = "tr" ) . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1") . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Get-Locale.ps1") $L = Get-Locale -Language $Language $MobilePath = if ($Platform -eq "Both") { Join-Path $OutputPath "apps\mobile" } else { Join-Path $OutputPath "mobile" } $AppPath = Join-Path $MobilePath "app" $HooksPath = Join-Path $MobilePath "hooks" $ServicesPath = Join-Path $MobilePath "services" if (-not $Global:DryRun) { New-Item -ItemType Directory -Path $HooksPath -Force | Out-Null New-Item -ItemType Directory -Path $ServicesPath -Force | Out-Null } $nonUserEntities = $Entities | Where-Object { $_ -ne "User" } $sharedImport = if ($Platform -eq "Both") { "shared" } else { "../shared" } # 1. Write services/api.ts (Api Client instance) $apiTs = @" import { ApiClient } from '$sharedImport'; export const api = new ApiClient(process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8080/api'); "@ Write-CoreFile (Join-Path $ServicesPath "api.ts") $apiTs # 2. Write React Query hooks and screen components for each entity foreach ($e in $nonUserEntities) { $lowE = $e.ToLower() $pE = Get-Plural $e $lowPE = $pE.ToLower() # Create entity directories if (-not $Global:DryRun) { New-Item -ItemType Directory -Path (Join-Path $AppPath "(tabs)\$lowPE") -Force | Out-Null } # Hooks file $hookContent = @" import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '../services/api'; const KEY = ['$lowPE']; export function use$pE(page: number = 1, size: number = 20) { return useQuery({ queryKey: [...KEY, page, size], queryFn: () => api.request(`/$lowPE/paged?page=\${page}&size=\${size}`), }); } export function useAdd$e() { const qc = useQueryClient(); return useMutation({ mutationFn: (vars: { name: string; price?: number }) => api.request('/$lowPE', { method: 'POST', body: JSON.stringify(vars) }), onSuccess: () => qc.invalidateQueries({ queryKey: KEY }), }); } "@ Write-CoreFile (Join-Path $HooksPath "use$pE.ts") $hookContent # Entity Screen (FlatList index.tsx) $priceCell = if ($e -eq "Product") { " {item.price !== undefined ? `\${item.price} TL` : ''}" } else { "" } $indexScreen = @" import { View, Text, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'; import { useRouter } from 'expo-router'; import { use$pE } from '../../../hooks/use$pE'; import { useState } from 'react'; export default function ${pE}Screen() { const router = useRouter(); const [page, setPage] = useState(1); const { data, isLoading, isError } = use$pE(page, 20); const items = data?.data?.items || []; const totalPages = data?.data?.totalPages || 1; return ( <View style={styles.container}> <TouchableOpacity style={styles.addButton} onPress={() => router.push('/$lowPE/add')}> <Text style={styles.addButtonText}>+ $($L.uiNewEntity -f $e)</Text> </TouchableOpacity> {isLoading ? ( <ActivityIndicator size="large" color="#3b82f6" style={{ marginTop: 40 }} /> ) : ( <FlatList data={items} keyExtractor={(item: any) => item.id.toString()} style={styles.list} renderItem={({ item }: { item: any }) => ( <View style={styles.itemCard}> <Text style={styles.itemText}>{item.name}</Text> $priceCell </View> )} ListEmptyComponent={ <Text style={styles.emptyText}>$($L.uiNoData)</Text> } /> )} <View style={styles.pagination}> <TouchableOpacity style={[styles.pageButton, page === 1 && styles.disabled]} disabled={page === 1} onPress={() => setPage(p => Math.max(1, p - 1))} > <Text style={styles.pageButtonText}>$($L.uiPrevious)</Text> </TouchableOpacity> <Text style={styles.pageInfo}>$($L.uiPage -f '{page}', '{totalPages}')</Text> <TouchableOpacity style={[styles.pageButton, page >= totalPages && styles.disabled]} disabled={page >= totalPages} onPress={() => setPage(p => Math.min(totalPages, p + 1))} > <Text style={styles.pageButtonText}>$($L.uiNext)</Text> </TouchableOpacity> </View> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#121212', padding: 16, }, addButton: { backgroundColor: '#3b82f6', padding: 14, borderRadius: 8, alignItems: 'center', marginBottom: 16, }, addButtonText: { color: '#ffffff', fontWeight: 'bold', fontSize: 16, }, list: { flex: 1, }, itemCard: { backgroundColor: '#1e1e1e', padding: 16, borderRadius: 8, marginBottom: 10, borderWidth: 1, borderColor: '#333333', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, itemText: { color: '#ffffff', fontSize: 16, fontWeight: '500', }, itemSub: { color: '#10b981', fontWeight: '600', }, emptyText: { color: '#888888', textAlign: 'center', marginTop: 40, fontSize: 15, }, pagination: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginTop: 10, }, pageButton: { backgroundColor: '#1e1e1e', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 6, }, pageButtonText: { color: '#ffffff', fontWeight: '600', }, disabled: { opacity: 0.5, }, pageInfo: { color: '#888888', }, }); "@ Write-CoreFile (Join-Path $AppPath "(tabs)\$lowPE\index.tsx") $indexScreen # Entity Add Screen (add.tsx) $priceFieldState = if ($e -eq "Product") { " const [price, setPrice] = useState('');" } else { "" } $priceInputMarkup = if ($e -eq "Product") { @" <Text style={styles.label}>$($L.uiPrice)</Text> <TextInput style={styles.input} value={price} onChangeText={setPrice} keyboardType="numeric" placeholder="0.00" placeholderTextColor="#666666" /> "@ } else { "" } $mutatePayload = if ($e -eq "Product") { "{ name, price: parseFloat(price) || 0 }" } else { "{ name }" } $addScreen = @" import { View, Text, TextInput, StyleSheet, TouchableOpacity, ActivityIndicator, Alert } from 'react-native'; import { useRouter } from 'expo-router'; import { useState } from 'react'; import { useAdd$e } from '../../../hooks/use$pE'; export default function Add${e}Screen() { const router = useRouter(); const [name, setName] = useState(''); $priceFieldState const addMutation = useAdd$e(); const handleSubmit = async () => { if (!name.trim()) { Alert.alert('Error', 'Name is required.'); return; } try { await addMutation.mutateAsync($mutatePayload); Alert.alert('Success', '$($L.uiEntityAdded -f $e)'); router.back(); } catch (err: any) { Alert.alert('Error', err.message || '$($L.uiAddFailed)'); } }; return ( <View style={styles.container}> <Text style={styles.label}>$($L.uiEntityName -f $e)</Text> <TextInput style={styles.input} value={name} onChangeText={setName} placeholder="Name" placeholderTextColor="#666666" /> $priceInputMarkup <TouchableOpacity style={[styles.saveButton, addMutation.isPending && styles.disabled]} onPress={handleSubmit} disabled={addMutation.isPending} > {addMutation.isPending ? ( <ActivityIndicator color="#ffffff" /> ) : ( <Text style={styles.saveButtonText}>$($L.uiAdd)</Text> )} </TouchableOpacity> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#121212', padding: 20, }, label: { color: '#ffffff', fontSize: 16, fontWeight: '600', marginBottom: 8, marginTop: 12, }, input: { backgroundColor: '#1e1e1e', color: '#ffffff', padding: 14, borderRadius: 8, fontSize: 16, borderWidth: 1, borderColor: '#333333', }, saveButton: { backgroundColor: '#3b82f6', padding: 14, borderRadius: 8, alignItems: 'center', marginTop: 30, }, saveButtonText: { color: '#ffffff', fontWeight: 'bold', fontSize: 16, }, disabled: { opacity: 0.7, }, }); "@ Write-CoreFile (Join-Path $AppPath "(tabs)\$lowPE\add.tsx") $addScreen } |