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 = Join-Path $OutputPath "$ProjectName-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" } # 1. Write services/api.ts (Api Client instance) $apiTs = @' export class ApiClient { private baseUrl: string; private token: string | null = null; constructor(baseUrl: string) { this.baseUrl = baseUrl; } setToken(token: string | null) { this.token = token; } async request(endpoint: string, options: RequestInit = {}) { const headers = new Headers(options.headers || {}); if (this.token) { headers.set('Authorization', `Bearer ${this.token}`); } if (!headers.has('Content-Type') && options.body) { headers.set('Content-Type', 'application/json'); } const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers, }); if (!response.ok) { const errText = await response.text(); throw new Error(errText || response.statusText); } if (response.status === 204) { return null; } return response.json(); } } const getDefaultApiUrl = () => { if (process.env.EXPO_PUBLIC_API_URL) { return process.env.EXPO_PUBLIC_API_URL; } if (Platform.OS === 'android') { return 'http://10.0.2.2:8080/api'; } return 'http://localhost:8080/api'; }; export const api = new ApiClient(getDefaultApiUrl()); '@ 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 }), }); } '@.Replace('__LOWPE__', $lowPE).Replace('__PE__', $pE).Replace('__E__', $e) Write-CoreFile (Join-Path $HooksPath "use$pE.ts") $hookContent # Entity Screen (FlatList index.tsx) $priceCell = if ($e -eq "Product") { ' <Text style={styles.itemSub}>{item.price !== undefined ? `${item.price} TL` : ' + "''}</Text>" } 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}>+ __UI_NEW_ENTITY__</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> __PRICE_CELL__ </View> )} ListEmptyComponent={ <Text style={styles.emptyText}>__UI_NO_DATA__</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}>__UI_PREVIOUS__</Text> </TouchableOpacity> <Text style={styles.pageInfo}>__UI_PAGE__</Text> <TouchableOpacity style={[styles.pageButton, page >= totalPages && styles.disabled]} disabled={page >= totalPages} onPress={() => setPage(p => Math.min(totalPages, p + 1))} > <Text style={styles.pageButtonText}>__UI_NEXT__</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', }, }); '@.Replace('__PE__', $pE).Replace('__LOWPE__', $lowPE).Replace('__UI_NEW_ENTITY__', ($L.uiNewEntity -f $e)).Replace('__UI_NO_DATA__', $L.uiNoData).Replace('__UI_PREVIOUS__', $L.uiPrevious).Replace('__UI_PAGE__', ($L.uiPage -f '{page}', '{totalPages}')).Replace('__UI_NEXT__', $L.uiNext).Replace('__PRICE_CELL__', $priceCell) 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 } |