Function bodies 81 total
TemplateSelectionScreen function · typescript · L11-L64 (54 LOC)app/(app)/(create)/index.tsx
export default function TemplateSelectionScreen() {
const { t, locale } = useTranslation();
const [templates, setTemplates] = useState<ContractTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
getTemplates()
.then(setTemplates)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) {
return (
<ScreenWrapper>
<HeaderBar title={t('select_template')} />
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
</ScreenWrapper>
);
}
return (
<ScreenWrapper scrollable={false}>
<HeaderBar title={t('select_template')} />
{error && (
<Text style={{ color: 'red', padding: 16 }}>{error}</Text>
)}
<FlatList
data={templates}
numColumns={2}
keyCreateLayout function · typescript · L3-L5 (3 LOC)app/(app)/(create)/_layout.tsx
export default function CreateLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}ReviewScreen function · typescript · L13-L78 (66 LOC)app/(app)/(create)/review.tsx
export default function ReviewScreen() {
const { t } = useTranslation();
const { user } = useAuth();
const params = useLocalSearchParams<{ templateId: string; values: string }>();
const [partyBPhone, setPartyBPhone] = useState('');
const [loading, setLoading] = useState(false);
const values = JSON.parse(params.values || '{}');
const handleSubmit = async () => {
if (!user) return;
setLoading(true);
try {
await createContract({
template_id: params.templateId,
party_a_id: user.id,
user_inputs: values,
party_b_phone: partyBPhone ? `+976${partyBPhone}` : undefined,
payment_amount: values.loan_amount ? Number(values.loan_amount) : undefined,
});
Alert.alert(t('contract_created'), '', [
{ text: 'OK', onPress: () => router.replace('/(app)/(home)') },
]);
} catch (err: any) {
Alert.alert(t('error_occurred'), err.message);
} finally {
setLoading(false);
}
};
return (DynamicFormScreen function · typescript · L9-L50 (42 LOC)app/(app)/(create)/[templateSlug].tsx
export default function DynamicFormScreen() {
const { templateSlug } = useLocalSearchParams<{ templateSlug: string }>();
const { template, loading } = useTemplate(templateSlug);
const { t, locale } = useTranslation();
if (loading || !template) {
return (
<ScreenWrapper>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
</ScreenWrapper>
);
}
const title = locale === 'en' && template.title_en ? template.title_en : template.title_mn;
const handleSubmit = async (values: Record<string, any>) => {
// Navigate to review screen with form data
router.push({
pathname: '/(app)/(create)/review',
params: {
templateId: template.id,
templateSlug: template.slug,
values: JSON.stringify(values),
},
});
};
return (
<ScreenWrapper>
<HeaderBar title={title} onBack={() => router.back()} />
ContractDetailScreen function · typescript · L16-L102 (87 LOC)app/(app)/(home)/[contractId].tsx
export default function ContractDetailScreen() {
const { contractId } = useLocalSearchParams<{ contractId: string }>();
const { contract, loading } = useContract(contractId);
const { t, locale } = useTranslation();
const [events, setEvents] = useState<ContractEvent[]>([]);
useEffect(() => {
if (contractId) {
getContractEvents(contractId).then(setEvents);
}
}, [contractId]);
if (loading || !contract) {
return (
<ScreenWrapper>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
</ScreenWrapper>
);
}
const templateTitle = contract.template
? (locale === 'en' && contract.template.title_en ? contract.template.title_en : contract.template.title_mn)
: '';
return (
<ScreenWrapper>
<HeaderBar title={t('contract_detail')} onBack={() => router.back()} />
<Card style={{ marginBottom: 16 }}>
<View stylHomeScreen function · typescript · L18-L66 (49 LOC)app/(app)/(home)/index.tsx
export default function HomeScreen() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(0);
const { contracts, loading, refetch } = useContracts(TABS[activeTab].statuses);
return (
<ScreenWrapper scrollable={false} padded={false}>
<View style={{ paddingHorizontal: 20 }}>
<HeaderBar title={t('the_vault')} />
<View style={{ flexDirection: 'row', marginBottom: 16, gap: 8 }}>
{TABS.map((tab, i) => (
<TouchableOpacity
key={tab.key}
onPress={() => setActiveTab(i)}
style={{
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 20,
backgroundColor: activeTab === i ? '#1E40AF' : '#E2E8F0',
}}
>
<Text style={{ color: activeTab === i ? '#FFFFFF' : '#374151', fontSize: 13, fontWeight: '600' }}>
{t(tab.key)}
</Text>
</TouchableOHomeLayout function · typescript · L3-L5 (3 LOC)app/(app)/(home)/_layout.tsx
export default function HomeLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}Repobility (the analyzer behind this table) · https://repobility.com
AppLayout function · typescript · L6-L70 (65 LOC)app/(app)/_layout.tsx
export default function AppLayout() {
const { session, loading } = useAuth();
const { t } = useTranslation();
if (loading) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
);
}
if (!session) {
return <Redirect href="/(auth)/login" />;
}
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarActiveTintColor: '#1E40AF',
tabBarInactiveTintColor: '#6B7280',
tabBarStyle: {
backgroundColor: '#FFFFFF',
borderTopColor: '#E2E8F0',
paddingBottom: 4,
height: 56,
},
tabBarLabelStyle: {
fontSize: 11,
fontWeight: '600',
},
}}
>
<Tabs.Screen
name="(home)"
options={{
title: t('home'),
tabBarIcon: ({ color }) => <Text style={{ fontSize: 20, color }}>📋</Text>,
}}
/>
NotificationsScreen function · typescript · L14-L80 (67 LOC)app/(app)/(notifications)/index.tsx
export default function NotificationsScreen() {
const { t, locale } = useTranslation();
const { user } = useAuth();
const [notifications, setNotifications] = useState<Notification[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (user) {
getNotifications(user.id)
.then(setNotifications)
.finally(() => setLoading(false));
}
}, [user]);
const handlePress = async (notification: Notification) => {
if (!notification.is_read) {
await markAsRead(notification.id);
setNotifications(prev => prev.map(n => n.id === notification.id ? { ...n, is_read: true } : n));
}
if (notification.contract_id) {
router.push(`/(app)/(home)/${notification.contract_id}`);
}
};
return (
<ScreenWrapper scrollable={false}>
<HeaderBar title={t('notifications')} />
<FlatList
data={notifications}
keyExtractor={(item) => item.id}
refreshing={loading}
onRefresh={() =>NotificationsLayout function · typescript · L3-L5 (3 LOC)app/(app)/(notifications)/_layout.tsx
export default function NotificationsLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}ProfileScreen function · typescript · L11-L64 (54 LOC)app/(app)/(profile)/index.tsx
export default function ProfileScreen() {
const { t } = useTranslation();
const { profile, signOut } = useAuth();
return (
<ScreenWrapper>
<HeaderBar title={t('profile')} />
<Card style={{ marginBottom: 16, alignItems: 'center', paddingVertical: 24 }}>
<View
style={{
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: '#1E40AF',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
}}
>
<Text style={{ fontSize: 32, color: '#FFFFFF', fontWeight: '700' }}>
{profile?.full_name?.charAt(0)?.toUpperCase() || '?'}
</Text>
</View>
<Text style={{ fontSize: 20, fontWeight: '700', color: '#111827' }}>
{profile?.full_name || '-'}
</Text>
{profile?.phone && (
<Text style={{ fontSize: 14, color: '#6B7280', marginTop: 4 }}>
+976 {formatPhone(profiProfileLayout function · typescript · L3-L5 (3 LOC)app/(app)/(profile)/_layout.tsx
export default function ProfileLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}SettingsScreen function · typescript · L10-L37 (28 LOC)app/(app)/(profile)/settings.tsx
export default function SettingsScreen() {
const { t, locale, setLocale } = useTranslation();
const { user, refreshProfile } = useAuth();
const toggleLocale = async () => {
const newLocale = locale === 'mn' ? 'en' : 'mn';
setLocale(newLocale);
if (user) {
await updateProfile(user.id, { preferred_locale: newLocale });
await refreshProfile();
}
};
return (
<ScreenWrapper>
<HeaderBar title={t('settings')} onBack={() => router.back()} />
<Card onPress={toggleLocale} style={{ marginBottom: 12 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 16, color: '#111827' }}>{t('language')}</Text>
<Text style={{ fontSize: 14, color: '#1E40AF', fontWeight: '600' }}>
{locale === 'mn' ? t('english') : t('mongolian')}
</Text>
</View>
</Card>
</ScreenWrapper>
);
}AuthLayout function · typescript · L3-L7 (5 LOC)app/(auth)/_layout.tsx
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }} />
);
}LoginScreen function · typescript · L10-L80 (71 LOC)app/(auth)/login.tsx
export default function LoginScreen() {
const { t } = useTranslation();
const { signInWithPhone } = useAuth();
const [phone, setPhone] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleLogin = async () => {
if (!phone || phone.length < 8) {
setError(t('invalid_phone'));
return;
}
setLoading(true);
setError('');
const fullPhone = phone.startsWith('+976') ? phone : `+976${phone}`;
const { error: authError } = await signInWithPhone(fullPhone);
setLoading(false);
if (authError) {
setError(authError.message);
} else {
router.push({ pathname: '/(auth)/verify', params: { phone: fullPhone } });
}
};
return (
<ScreenWrapper>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1, justifyContent: 'center' }}
>
<View style={{ alignItems: 'center', marginBottom: 40 }}>
Repobility · MCP-ready · https://repobility.com
RegisterScreen function · typescript · L10-L86 (77 LOC)app/(auth)/register.tsx
export default function RegisterScreen() {
const { t } = useTranslation();
const { signUp } = useAuth();
const [fullName, setFullName] = useState('');
const [phone, setPhone] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleRegister = async () => {
if (!fullName.trim()) {
setError(t('required_field'));
return;
}
if (!phone || phone.length < 8) {
setError(t('invalid_phone'));
return;
}
setLoading(true);
setError('');
const fullPhone = phone.startsWith('+976') ? phone : `+976${phone}`;
const { error: authError } = await signUp(fullPhone, fullName);
setLoading(false);
if (authError) {
setError(authError.message);
} else {
router.push({ pathname: '/(auth)/verify', params: { phone: fullPhone } });
}
};
return (
<ScreenWrapper>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'VerifyScreen function · typescript · L10-L70 (61 LOC)app/(auth)/verify.tsx
export default function VerifyScreen() {
const { t } = useTranslation();
const { verifyOtp } = useAuth();
const { phone } = useLocalSearchParams<{ phone: string }>();
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleVerify = async () => {
if (code.length !== 6) {
setError(t('otp_placeholder'));
return;
}
setLoading(true);
setError('');
const { error: authError } = await verifyOtp(phone || '', code);
setLoading(false);
if (authError) {
setError(authError.message);
} else {
router.replace('/(app)/(home)');
}
};
return (
<ScreenWrapper>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1, justifyContent: 'center' }}
>
<View style={{ alignItems: 'center', marginBottom: 40 }}>
<Text style={{ fontSize: 28, fontWeight: '800', color:AcceptContractScreen function · typescript · L15-L95 (81 LOC)app/contract/accept/[contractId].tsx
export default function AcceptContractScreen() {
const { contractId } = useLocalSearchParams<{ contractId: string }>();
const { contract, loading } = useContract(contractId);
const { user } = useAuth();
const { t, locale } = useTranslation();
const [template, setTemplate] = useState<ContractTemplate | null>(null);
const [submitting, setSubmitting] = useState(false);
useEffect(() => {
if (contract?.template_id) {
supabase
.from('contract_templates')
.select('*')
.eq('id', contract.template_id)
.single()
.then(({ data }) => setTemplate(data));
}
}, [contract?.template_id]);
if (loading || !contract || !template) {
return (
<ScreenWrapper>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
</ScreenWrapper>
);
}
if (contract.status !== 'pending_acceptance') {
return (
<ScreenContractLayout function · typescript · L3-L5 (3 LOC)app/contract/_layout.tsx
export default function ContractLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}SignContractScreen function · typescript · L13-L81 (69 LOC)app/contract/sign/[contractId].tsx
export default function SignContractScreen() {
const { contractId } = useLocalSearchParams<{ contractId: string }>();
const { contract, loading } = useContract(contractId);
const { user } = useAuth();
const { t } = useTranslation();
const [signing, setSigning] = useState(false);
if (loading || !contract) {
return (
<ScreenWrapper>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
</ScreenWrapper>
);
}
const isPartyA = user?.id === contract.party_a_id;
const handleSign = async (signatureData: string) => {
if (!user || !contractId) return;
setSigning(true);
try {
const updates: any = {};
if (isPartyA) {
updates.party_a_signed_at = new Date().toISOString();
updates.party_a_signature_hash = signatureData;
} else {
updates.party_b_signed_at = new Date().toISOString();
updates.partRoot function · typescript · L7-L28 (22 LOC)app/+html.tsx
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}Index function · typescript · L5-L21 (17 LOC)app/index.tsx
export default function Index() {
const { session, loading } = useAuth();
if (loading) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#F8FAFC' }}>
<ActivityIndicator size="large" color="#1E40AF" />
</View>
);
}
if (session) {
return <Redirect href="/(app)/(home)" />;
}
return <Redirect href="/(auth)/login" />;
}RootLayout function · typescript · L7-L21 (15 LOC)app/_layout.tsx
export default function RootLayout() {
return (
<AuthProvider>
<I18nProvider>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(app)" />
<Stack.Screen name="contract" />
<Stack.Screen name="+not-found" />
</Stack>
</I18nProvider>
</AuthProvider>
);
}Generated by Repobility's multi-pass static-analysis pipeline (https://repobility.com)
NotFoundScreen function · typescript · L4-L17 (14 LOC)app/+not-found.tsx
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}ContractCard function · typescript · L13-L45 (33 LOC)components/contract/ContractCard.tsx
export function ContractCard({ contract, onPress }: ContractCardProps) {
const { locale } = useTranslation();
const title = contract.template
? (locale === 'en' && contract.template.title_en ? contract.template.title_en : contract.template.title_mn)
: 'Contract';
return (
<Card onPress={onPress} style={{ marginBottom: 12 }}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }}>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 16, fontWeight: '600', color: '#111827', marginBottom: 4 }}>
{title}
</Text>
<Text style={{ fontSize: 13, color: '#6B7280' }}>
{formatDate(contract.created_at, locale)}
</Text>
</View>
<StatusBadge status={contract.status} />
</View>
{contract.payment_amount && (
<Text style={{ fontSize: 14, color: '#374151', fontWeight: '500' }}>
{formatCurrency(contract.paySignatureCanvas function · typescript · L11-L43 (33 LOC)components/contract/SignatureCanvas.tsx
export function SignatureCanvas({ onSave, loading }: SignatureCanvasProps) {
const { t } = useTranslation();
const handleSign = () => {
// In production, capture actual signature data from canvas
// For MVP, generate a mock signature hash
const mockSignature = `SIG-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
onSave(mockSignature);
};
return (
<View>
<View
style={{
height: 200,
borderWidth: 1.5,
borderColor: '#D1D5DB',
borderRadius: 10,
borderStyle: 'dashed',
backgroundColor: '#FAFAFA',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
}}
>
<Text style={{ color: '#9CA3AF', fontSize: 14 }}>
{t('sign')} - Tap to sign
</Text>
</View>
<Button title={t('sign')} onPress={handleSign} loading={loading} />
</View>
);
}StatusBadge function · typescript · L10-L13 (4 LOC)components/contract/StatusBadge.tsx
export function StatusBadge({ status }: StatusBadgeProps) {
const { t } = useTranslation();
return <Badge label={t(`status_${status}`)} color={getStatusColor(status)} />;
}Timeline function · typescript · L10-L42 (33 LOC)components/contract/Timeline.tsx
export function Timeline({ events }: TimelineProps) {
const { locale } = useTranslation();
return (
<View>
{events.map((event, index) => (
<View key={event.id} style={{ flexDirection: 'row', marginBottom: 16 }}>
<View style={{ alignItems: 'center', marginRight: 12 }}>
<View
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: index === events.length - 1 ? '#1E40AF' : '#CBD5E1',
}}
/>
{index < events.length - 1 && (
<View style={{ width: 2, flex: 1, backgroundColor: '#E2E8F0', marginTop: 4 }} />
)}
</View>
<View style={{ flex: 1 }}>
<Text style={{ fontSize: 14, fontWeight: '500', color: '#111827' }}>
{event.event_type.replace(/_/g, ' ')}
</Text>
<Text style={{ fontSize: 12, color: '#6B7280', marginTop: 2 }}>
DynamicForm function · typescript · L19-L87 (69 LOC)components/forms/DynamicForm.tsx
export function DynamicForm({
fields,
initialValues = {},
filledBy,
onSubmit,
submitLabel,
loading,
readOnlyValues = {},
}: DynamicFormProps) {
const { locale, t } = useTranslation();
const [values, setValues] = useState<FormValues>({ ...initialValues });
const [errors, setErrors] = useState<FormErrors>({});
const editableFields = fields.filter(f => f.filled_by === filledBy || f.filled_by === 'both');
const readOnlyFields = fields.filter(f => f.filled_by !== filledBy && f.filled_by !== 'both');
const handleChange = useCallback((key: string, value: any) => {
setValues(prev => ({ ...prev, [key]: value }));
setErrors(prev => {
const next = { ...prev };
delete next[key];
return next;
});
}, []);
const handleSubmit = () => {
const validationErrors = validateForm(editableFields, values, locale);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
onSubmit(values);
}FieldRenderer function · typescript · L19-L153 (135 LOC)components/forms/FieldRenderer.tsx
export function FieldRenderer({ field, value, onChange, error, editable = true }: FieldRendererProps) {
const { locale } = useTranslation();
const label = (locale === 'en' && field.label_en ? field.label_en : field.label_mn)
+ (field.required ? ' *' : '');
const placeholder = locale === 'en' && field.placeholder_en ? field.placeholder_en : field.placeholder_mn;
switch (field.type) {
case 'text':
return (
<TextField
label={label}
placeholder={placeholder}
value={value || ''}
onChangeText={onChange}
error={error}
editable={editable}
maxLength={field.validation?.max_length}
/>
);
case 'textarea':
return (
<TextField
label={label}
placeholder={placeholder}
value={value || ''}
onChangeText={onChange}
error={error}
editable={editable}
maxLength={field.validation?.max_length}
multiCurrencyField function · typescript · L13-L60 (48 LOC)components/forms/fields/CurrencyField.tsx
export function CurrencyField({ label, placeholder, value, onChangeText, error, editable = true }: CurrencyFieldProps) {
const [focused, setFocused] = useState(false);
const handleChange = (text: string) => {
const cleaned = text.replace(/[^0-9]/g, '');
onChangeText(cleaned);
};
const displayValue = value
? Number(value).toLocaleString('mn-MN')
: '';
return (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6 }}>{label}</Text>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1.5,
borderColor: error ? '#DC2626' : focused ? '#1E40AF' : '#D1D5DB',
borderRadius: 10,
backgroundColor: editable ? '#FFFFFF' : '#F3F4F6',
}}
>
<TextInput
value={focused ? value : displayValue}
onChangeText={handleChange}
placeholder={placeholder || '0'}
Want this analysis on your repo? https://repobility.com/scan/
DateField function · typescript · L12-L47 (36 LOC)components/forms/fields/DateField.tsx
export function DateField({ label, placeholder, value, onChangeText, error, editable = true }: DateFieldProps) {
const displayValue = value
? new Date(value).toLocaleDateString('mn-MN', { year: 'numeric', month: 'long', day: 'numeric' })
: '';
const handlePress = () => {
if (!editable) return;
// For MVP, use a simple text input approach
// In production, integrate @react-native-community/datetimepicker
const today = new Date();
onChangeText(today.toISOString().split('T')[0]);
};
return (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6 }}>{label}</Text>
<TouchableOpacity
onPress={handlePress}
disabled={!editable}
style={{
borderWidth: 1.5,
borderColor: error ? '#DC2626' : '#D1D5DB',
borderRadius: 10,
paddingHorizontal: 14,
paddingVertical: 12,
backgroundColor: editable ? '#FFFFFF' DropdownField function · typescript · L17-L78 (62 LOC)components/forms/fields/DropdownField.tsx
export function DropdownField({ label, placeholder, value, onChangeText, options, error, editable = true }: DropdownFieldProps) {
const [open, setOpen] = useState(false);
const { locale } = useTranslation();
const selectedOption = options.find(o => o.value === value);
const displayText = selectedOption
? (locale === 'en' && selectedOption.label_en ? selectedOption.label_en : selectedOption.label_mn)
: '';
return (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6 }}>{label}</Text>
<TouchableOpacity
onPress={() => editable && setOpen(true)}
disabled={!editable}
style={{
borderWidth: 1.5,
borderColor: error ? '#DC2626' : '#D1D5DB',
borderRadius: 10,
paddingHorizontal: 14,
paddingVertical: 12,
backgroundColor: editable ? '#FFFFFF' : '#F3F4F6',
flexDirection: 'row',
justifyContent: 'spNumberField function · typescript · L12-L29 (18 LOC)components/forms/fields/NumberField.tsx
export function NumberField({ label, placeholder, value, onChangeText, error, editable }: NumberFieldProps) {
const handleChange = (text: string) => {
const cleaned = text.replace(/[^0-9.-]/g, '');
onChangeText(cleaned);
};
return (
<Input
label={label}
placeholder={placeholder}
value={value}
onChangeText={handleChange}
error={error}
keyboardType="numeric"
editable={editable}
/>
);
}PhoneField function · typescript · L13-L57 (45 LOC)components/forms/fields/PhoneField.tsx
export function PhoneField({ label, placeholder, value, onChangeText, error, editable = true }: PhoneFieldProps) {
const [focused, setFocused] = useState(false);
const handleChange = (text: string) => {
const cleaned = text.replace(/[^0-9]/g, '');
onChangeText(cleaned.slice(0, 8));
};
return (
<View style={{ marginBottom: 16 }}>
<Text style={{ fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6 }}>{label}</Text>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1.5,
borderColor: error ? '#DC2626' : focused ? '#1E40AF' : '#D1D5DB',
borderRadius: 10,
backgroundColor: editable ? '#FFFFFF' : '#F3F4F6',
}}
>
<Text style={{ paddingLeft: 14, fontSize: 16, color: '#6B7280' }}>+976</Text>
<TextInput
value={value}
onChangeText={handleChange}
placeholder={placeholder || '99112233'}
placeholdTextField function · typescript · L14-L27 (14 LOC)components/forms/fields/TextField.tsx
export function TextField({ label, placeholder, value, onChangeText, error, editable, maxLength, multiline }: TextFieldProps) {
return (
<Input
label={label}
placeholder={placeholder}
value={value}
onChangeText={onChangeText}
error={error}
editable={editable}
maxLength={maxLength}
multiline={multiline}
/>
);
}validateForm function · typescript · L3-L121 (119 LOC)components/forms/validation.ts
export function validateForm(
fields: FieldDefinition[],
values: FormValues,
locale: 'mn' | 'en' = 'mn'
): FormErrors {
const errors: FormErrors = {};
for (const field of fields) {
const value = values[field.key];
const v = field.validation;
// Required check
if (field.required && (value === undefined || value === null || value === '')) {
errors[field.key] = locale === 'mn' ? 'Энэ талбарыг бөглөнө үү' : 'This field is required';
continue;
}
// Skip validation if empty and not required
if (value === undefined || value === null || value === '') continue;
const strValue = String(value);
// Type-specific validation
switch (field.type) {
case 'text':
case 'textarea':
if (v?.min_length && strValue.length < v.min_length) {
errors[field.key] = locale === 'mn'
? `Хамгийн багадаа ${v.min_length} тэмдэгт`
: `Minimum ${v.min_length} characters`;
}
if (v?.max_lengHeaderBar function · typescript · L9-L33 (25 LOC)components/layout/HeaderBar.tsx
export function HeaderBar({ title, onBack, rightAction }: HeaderBarProps) {
return (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 4,
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
{onBack && (
<TouchableOpacity onPress={onBack} style={{ marginRight: 12 }}>
<Text style={{ fontSize: 24, color: '#1E40AF' }}>‹</Text>
</TouchableOpacity>
)}
<Text style={{ fontSize: 20, fontWeight: '700', color: '#111827' }} numberOfLines={1}>
{title}
</Text>
</View>
{rightAction && <View>{rightAction}</View>}
</View>
);
}ScreenWrapper function · typescript · L10-L32 (23 LOC)components/layout/ScreenWrapper.tsx
export function ScreenWrapper({ children, scrollable = true, padded = true }: ScreenWrapperProps) {
const content = (
<View style={{ flex: 1, paddingHorizontal: padded ? 20 : 0 }}>
{children}
</View>
);
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#F8FAFC' }}>
{scrollable ? (
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{content}
</ScrollView>
) : (
content
)}
</SafeAreaView>
);
}Repobility (the analyzer behind this table) · https://repobility.com
Badge function · typescript · L8-L22 (15 LOC)components/ui/Badge.tsx
export function Badge({ label, color }: BadgeProps) {
return (
<View
style={{
backgroundColor: color + '1A',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 20,
alignSelf: 'flex-start',
}}
>
<Text style={{ color, fontSize: 12, fontWeight: '600' }}>{label}</Text>
</View>
);
}Button function · typescript · L19-L50 (32 LOC)components/ui/Button.tsx
export function Button({ title, onPress, variant = 'primary', loading, disabled, icon }: ButtonProps) {
const style = variantStyles[variant];
const isDisabled = disabled || loading;
return (
<TouchableOpacity
onPress={onPress}
disabled={isDisabled}
style={{
backgroundColor: style.bg,
borderColor: 'border' in style ? style.border : 'transparent',
borderWidth: 'border' in style ? 1.5 : 0,
borderRadius: 12,
paddingVertical: 14,
paddingHorizontal: 24,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
opacity: isDisabled ? 0.5 : 1,
}}
>
{loading ? (
<ActivityIndicator color={style.text} />
) : (
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
{icon}
<Text style={{ color: style.text, fontSize: 16, fontWeight: '600' }}>{title}</Text>
</View>
)}
</TouchableOpacity>
);
Card function · typescript · L9-L31 (23 LOC)components/ui/Card.tsx
export function Card({ children, onPress, style }: CardProps) {
const cardStyle = {
backgroundColor: '#FFFFFF',
borderRadius: 14,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 2,
...style,
};
if (onPress) {
return (
<TouchableOpacity onPress={onPress} style={cardStyle} activeOpacity={0.7}>
{children}
</TouchableOpacity>
);
}
return <View style={cardStyle}>{children}</View>;
}EmptyState function · typescript · L9-L17 (9 LOC)components/ui/EmptyState.tsx
export function EmptyState({ title, subtitle, icon }: EmptyStateProps) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 60 }}>
{icon && <Text style={{ fontSize: 48, marginBottom: 16 }}>{icon}</Text>}
<Text style={{ fontSize: 18, fontWeight: '600', color: '#374151', marginBottom: 8 }}>{title}</Text>
{subtitle && <Text style={{ fontSize: 14, color: '#6B7280', textAlign: 'center' }}>{subtitle}</Text>}
</View>
);
}Input function · typescript · L17-L68 (52 LOC)components/ui/Input.tsx
export function Input({
label,
placeholder,
value,
onChangeText,
error,
keyboardType = 'default',
secureTextEntry,
multiline,
editable = true,
maxLength,
}: InputProps) {
const [focused, setFocused] = useState(false);
return (
<View style={{ marginBottom: 16 }}>
{label && (
<Text style={{ fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6 }}>
{label}
</Text>
)}
<TextInput
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
placeholderTextColor="#9CA3AF"
keyboardType={keyboardType}
secureTextEntry={secureTextEntry}
multiline={multiline}
editable={editable}
maxLength={maxLength}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={{
borderWidth: 1.5,
borderColor: error ? '#DC2626' : focused ? '#1E40AF' : '#D1D5DB',
borderRadius: 10,
Modal function · typescript · L10-L34 (25 LOC)components/ui/Modal.tsx
export function Modal({ visible, onClose, title, children }: ModalProps) {
return (
<RNModal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
<View style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }}>
<View
style={{
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
maxHeight: '80%',
}}
>
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
{title && <Text style={{ fontSize: 18, fontWeight: '700', color: '#111827' }}>{title}</Text>}
<TouchableOpacity onPress={onClose}>
<Text style={{ fontSize: 24, color: '#6B7280' }}>×</Text>
</TouchableOpacity>
</View>
{children}
</View>
</View>
</RNModal>
);
}useContracts function · typescript · L5-L30 (26 LOC)hooks/useContracts.ts
export function useContracts(statusFilter?: ContractStatus[]) {
const [contracts, setContracts] = useState<ActiveContract[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetch = useCallback(async (filter?: ContractStatus[]) => {
try {
setLoading(true);
const data = await getContracts(filter);
setContracts(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, []);
const statusKey = statusFilter?.join(',');
useEffect(() => {
fetch(statusFilter);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [statusKey, fetch]);
return { contracts, loading, error, refetch: () => fetch(statusFilter) };
}useContract function · typescript · L6-L45 (40 LOC)hooks/useContract.ts
export function useContract(contractId: string | undefined) {
const [contract, setContract] = useState<ActiveContract | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!contractId) return;
const fetch = async () => {
try {
setLoading(true);
const data = await getContract(contractId);
setContract(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetch();
const channel = supabase
.channel(`contract-${contractId}`)
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'active_contracts', filter: `id=eq.${contractId}` },
(payload) => {
setContract((prev) => prev ? { ...prev, ...payload.new } : null);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [contracRepobility · MCP-ready · https://repobility.com
useRealtimeStatus function · typescript · L4-L33 (30 LOC)hooks/useRealtimeStatus.ts
export function useRealtimeStatus(
contractId: string | undefined,
onStatusChange: (newStatus: string) => void
) {
useEffect(() => {
if (!contractId) return;
const channel = supabase
.channel(`status-${contractId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'active_contracts',
filter: `id=eq.${contractId}`,
},
(payload) => {
if (payload.new && 'status' in payload.new) {
onStatusChange(payload.new.status as string);
}
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [contractId, onStatusChange]);
}useTemplate function · typescript · L5-L29 (25 LOC)hooks/useTemplate.ts
export function useTemplate(slug: string | undefined) {
const [template, setTemplate] = useState<ContractTemplate | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!slug) return;
const fetch = async () => {
try {
setLoading(true);
const data = await getTemplateBySlug(slug);
setTemplate(data);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetch();
}, [slug]);
return { template, loading, error };
}getContracts function · typescript · L4-L17 (14 LOC)lib/api/contracts.ts
export async function getContracts(status?: ContractStatus[]): Promise<ActiveContract[]> {
let query = supabase
.from('active_contracts')
.select('*, template:contract_templates(*), party_a:profiles!party_a_id(*), party_b:profiles!party_b_id(*)')
.order('created_at', { ascending: false });
if (status && status.length > 0) {
query = query.in('status', status);
}
const { data, error } = await query;
if (error) throw error;
return data || [];
}page 1 / 2next ›