aport-tools 4.5.0 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -34
- package/src/cards/Card.tsx +129 -0
- package/src/cards/index.ts +1 -0
- package/src/components/Button.tsx +180 -0
- package/src/fonts/Text.tsx +137 -0
- package/{dist/fonts/index.d.ts → src/fonts/index.ts} +1 -0
- package/src/forms/ErrorList.tsx +47 -0
- package/src/forms/FORMDOC.md +87 -0
- package/{dist/forms/Form.d.ts → src/forms/Form.tsx} +2 -0
- package/src/forms/FormContext.tsx +248 -0
- package/src/forms/Input.tsx +174 -0
- package/src/forms/InputAttach.tsx +184 -0
- package/src/forms/InputCheck.tsx +169 -0
- package/src/forms/InputList.tsx +304 -0
- package/src/forms/Label.tsx +26 -0
- package/src/forms/Stepper.tsx +289 -0
- package/src/forms/TextArea.tsx +91 -0
- package/{dist/forms/index.d.ts → src/forms/index.ts} +4 -2
- package/src/index.ts +6 -0
- package/dist/cards/Card.d.ts +0 -57
- package/dist/cards/index.d.ts +0 -1
- package/dist/components/Button.d.ts +0 -52
- package/dist/defaults/reanimatedWrapper.d.ts +0 -2
- package/dist/fonts/Text.d.ts +0 -64
- package/dist/forms/ErrorList.d.ts +0 -6
- package/dist/forms/FormContext.d.ts +0 -132
- package/dist/forms/Input.d.ts +0 -43
- package/dist/forms/InputAttach.d.ts +0 -16
- package/dist/forms/InputCheck.d.ts +0 -19
- package/dist/forms/InputList.d.ts +0 -93
- package/dist/forms/Label.d.ts +0 -7
- package/dist/forms/Stepper.d.ts +0 -54
- package/dist/forms/TextArea.d.ts +0 -13
- package/dist/index.d.ts +0 -4
- package/dist/index.esm.js +0 -1493
- package/dist/index.esm.js.map +0 -1
- package/dist/index.js +0 -1526
- package/dist/index.js.map +0 -1
- /package/{dist/buttons/index.d.ts → src/buttons/index.ts} +0 -0
@@ -0,0 +1,248 @@
|
|
1
|
+
// src/forms/FormContext.tsx
|
2
|
+
|
3
|
+
import React, { createContext, useState, ReactNode, useContext } from 'react';
|
4
|
+
import Stepper from './Stepper';
|
5
|
+
|
6
|
+
interface FormContextProps {
|
7
|
+
/**
|
8
|
+
* Stores the current form values as an object.
|
9
|
+
* Each key is a form field name and each value is the current value of that field.
|
10
|
+
* Example:
|
11
|
+
* ```ts
|
12
|
+
* formValues = { name: "John", email: "john@example.com" };
|
13
|
+
* ```
|
14
|
+
*/
|
15
|
+
formValues: Record<string, any>;
|
16
|
+
|
17
|
+
/**
|
18
|
+
* A function to update a specific form field value.
|
19
|
+
* It takes the `name` of the form field and the new `value` to be set.
|
20
|
+
* and an optional `firstValue` to track the initial value.
|
21
|
+
* Example usage:
|
22
|
+
* ```ts
|
23
|
+
* setFormValue('email', 'newemail@example.com', 'oldemail@example.com');
|
24
|
+
* ```
|
25
|
+
*/
|
26
|
+
setFormValue: (name: string, value: any, firstValue?: any) => void;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Stores the current form errors as an object.
|
30
|
+
* Each key is a form field name and the value is an array of error messages for that field.
|
31
|
+
* Example:
|
32
|
+
* ```ts
|
33
|
+
* errors = { email: ['Email is required', 'Invalid email format'] };
|
34
|
+
* ```
|
35
|
+
*/
|
36
|
+
errors: Record<string, string[]>;
|
37
|
+
|
38
|
+
/**
|
39
|
+
* A function to update the errors for all form fields.
|
40
|
+
* It takes an object where each key is a form field name and the value is an array of error messages for that field.
|
41
|
+
* Example usage:
|
42
|
+
* ```ts
|
43
|
+
* setErrors({ email: ['Email is required', 'Invalid email format'] });
|
44
|
+
* ```
|
45
|
+
*/
|
46
|
+
setErrors: (errors: Record<string, string[]>) => void;
|
47
|
+
|
48
|
+
/**
|
49
|
+
* A function to trigger the form submission.
|
50
|
+
* It will invoke the `onSubmit` handler and handle form validation errors.
|
51
|
+
* Typically, this function would call `onSubmit` and handle errors by updating the `errors` state.
|
52
|
+
*/
|
53
|
+
handleSubmit: () => void;
|
54
|
+
|
55
|
+
handleFormSubmit: (formValues: Record<string, any>) => Promise<Record<string, string[]>>;
|
56
|
+
|
57
|
+
}
|
58
|
+
|
59
|
+
// Create a ref to hold the global `setFormValue` function
|
60
|
+
const globalSetFormValueRef = { current: null as ((name: string, value: any, firstValue?: any) => void) | null };
|
61
|
+
|
62
|
+
// Utility to use `setFormValue` globally
|
63
|
+
export const setFormValueGlobal = (name: string, value: any, firstValue?: any) => {
|
64
|
+
if (globalSetFormValueRef.current) {
|
65
|
+
globalSetFormValueRef.current(name, value, firstValue);
|
66
|
+
} else {
|
67
|
+
console.warn("setFormValueGlobal was called before the Form was rendered.");
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
const FormContext = createContext<FormContextProps | undefined>(undefined);
|
72
|
+
|
73
|
+
export const useFormContext = () => {
|
74
|
+
const context = useContext(FormContext);
|
75
|
+
if (!context) {
|
76
|
+
throw new Error("useFormContext must be used within a Form");
|
77
|
+
}
|
78
|
+
return context;
|
79
|
+
};
|
80
|
+
|
81
|
+
interface StepperProps {
|
82
|
+
/**
|
83
|
+
* Labels for each step (optional). If provided, these labels will display below each step indicator.
|
84
|
+
* Example: ['Step 1', 'Step 2', 'Step 3']
|
85
|
+
*/
|
86
|
+
steps?: string[];
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Current step index (e.g., 0 for step 1, 1 for step 2).
|
90
|
+
* Determines the active step and progress display.
|
91
|
+
*/
|
92
|
+
currentStep: number;
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Enables or disables the ability to click on a step to navigate to it.
|
96
|
+
* Default is `false`.
|
97
|
+
*/
|
98
|
+
presseable?: boolean;
|
99
|
+
|
100
|
+
/**
|
101
|
+
* Callback function that gets triggered when a step is clicked.
|
102
|
+
* Passes the index of the clicked step as an argument.
|
103
|
+
* Example: (stepIndex) => console.log(`Step ${stepIndex} clicked!`)
|
104
|
+
*/
|
105
|
+
onPress?: (stepIndex: number) => void;
|
106
|
+
|
107
|
+
/**
|
108
|
+
* Determines the shape of the step indicator.
|
109
|
+
* Options:
|
110
|
+
* - `'circular'` (default)
|
111
|
+
* - `'square'`
|
112
|
+
*/
|
113
|
+
stepStyle?: "circular" | "square";
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Total number of steps in the stepper.
|
117
|
+
* Must be greater than or equal to 1.
|
118
|
+
*/
|
119
|
+
totalSteps: number;
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Type of content displayed inside the step indicator.
|
123
|
+
* Options:
|
124
|
+
* - `'number'` (default): Displays the step index (e.g., 1, 2, 3).
|
125
|
+
* - `'icon'`: Displays a custom icon (requires the `icon` prop).
|
126
|
+
* - `'empty'`: Displays no content inside the step indicator.
|
127
|
+
*/
|
128
|
+
stepType?: "number" | "icon" | "empty";
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Custom icon(s) to display inside step indicators when `stepType` is set to `'icon'`.
|
132
|
+
* Accepts:
|
133
|
+
* - A single icon for all steps (ReactNode or string URI).
|
134
|
+
* - An array of icons (ReactNode[] or string[]), one for each step.
|
135
|
+
* Example: ['https://example.com/icon1.png', 'https://example.com/icon2.png']
|
136
|
+
*/
|
137
|
+
icon?: React.ReactNode | React.ReactNode[];
|
138
|
+
}
|
139
|
+
|
140
|
+
interface FormProps {
|
141
|
+
/**
|
142
|
+
* The children elements to render inside the form.
|
143
|
+
* Typically this will be form fields or custom form components.
|
144
|
+
* This prop is required and must be passed as ReactNode(s).
|
145
|
+
*/
|
146
|
+
children: React.ReactNode;
|
147
|
+
|
148
|
+
/**
|
149
|
+
* The callback function to handle form submission.
|
150
|
+
* It receives the form values as an object (`values`) and must return a promise
|
151
|
+
* that resolves to an object containing errors for the form fields.
|
152
|
+
* Example:
|
153
|
+
* ```ts
|
154
|
+
* const onSubmit = async (values: Record<string, any>) => {
|
155
|
+
* // handle the submission and return errors if any
|
156
|
+
* return { fieldName: ['Error message'] };
|
157
|
+
* }
|
158
|
+
* ```
|
159
|
+
* The return value should be an object where the keys are form field names
|
160
|
+
* and the values are arrays of error messages for those fields.
|
161
|
+
*/
|
162
|
+
onSubmit: (values: Record<string, any>) => Promise<Record<string, string[]>>;
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Optional stepper configuration for the form.
|
166
|
+
* If provided, it will integrate a stepper component to display form steps.
|
167
|
+
* This is an optional prop, so the form can be used without it.
|
168
|
+
* The `StepperProps` interface will define the expected props for the stepper.
|
169
|
+
*/
|
170
|
+
stepper?: StepperProps; // Add stepper prop (optional)
|
171
|
+
}
|
172
|
+
|
173
|
+
export const Form: React.FC<FormProps> = ({ children, onSubmit, stepper }) => {
|
174
|
+
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
175
|
+
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
176
|
+
const [firstValues, setFirstValues] = useState<Record<string, any>>({}); // Track firstValues
|
177
|
+
|
178
|
+
const setFormValue = (name: string, value: any, firstValue?: any) => {
|
179
|
+
// Update formValues with the latest value
|
180
|
+
setFormValues(prev => ({ ...prev, [name]: value }));
|
181
|
+
|
182
|
+
// If firstValue exists and is not empty, set it in firstValues
|
183
|
+
if (firstValue !== undefined && firstValue !== null && firstValue !== "") {
|
184
|
+
setFirstValues(prev => ({ ...prev, [name]: firstValue }));
|
185
|
+
}
|
186
|
+
};
|
187
|
+
|
188
|
+
|
189
|
+
const handleSubmit = async () => {
|
190
|
+
// Filter formValues to include only modified fields
|
191
|
+
const modifiedValues = Object.keys(formValues).reduce((result, key) => {
|
192
|
+
const hasFirstValue = key in firstValues;
|
193
|
+
const isModified = hasFirstValue && formValues[key] !== firstValues[key];
|
194
|
+
const isNewValue = !hasFirstValue;
|
195
|
+
|
196
|
+
// If the field was modified or is a new value, include it in the modified values
|
197
|
+
if (isModified || isNewValue) {
|
198
|
+
result[key] = formValues[key];
|
199
|
+
} else {
|
200
|
+
// If there were no changes, use the firstValue
|
201
|
+
if (hasFirstValue && firstValues[key] !== undefined) {
|
202
|
+
result[key] = firstValues[key];
|
203
|
+
}
|
204
|
+
}
|
205
|
+
return result;
|
206
|
+
}, {} as Record<string, any>);
|
207
|
+
|
208
|
+
// Call the onSubmit callback with the modified or firstValues
|
209
|
+
const validationErrors = await onSubmit(modifiedValues);
|
210
|
+
|
211
|
+
// Set the validation errors in state
|
212
|
+
setErrors(validationErrors);
|
213
|
+
|
214
|
+
// Prevent submission if there are any errors
|
215
|
+
if (Object.keys(validationErrors).length > 0) {
|
216
|
+
return; // Prevent submission
|
217
|
+
}
|
218
|
+
|
219
|
+
// If no errors, proceed with form submission (e.g., sending the form data to the server)
|
220
|
+
};
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
const handleFormSubmit = async (formValues: Record<string, any>) => {
|
225
|
+
const validationErrors = await onSubmit(formValues);
|
226
|
+
setErrors(validationErrors);
|
227
|
+
return validationErrors;
|
228
|
+
};
|
229
|
+
|
230
|
+
|
231
|
+
return (
|
232
|
+
<FormContext.Provider value={{ formValues, setFormValue, errors, setErrors, handleSubmit, handleFormSubmit }}>
|
233
|
+
{stepper && (
|
234
|
+
<Stepper
|
235
|
+
steps={stepper.steps}
|
236
|
+
currentStep={stepper.currentStep}
|
237
|
+
presseable={stepper.presseable}
|
238
|
+
onPress={stepper.onPress}
|
239
|
+
totalSteps={stepper.totalSteps}
|
240
|
+
stepType={stepper.stepType}
|
241
|
+
icon={stepper.icon}
|
242
|
+
stepStyle={stepper.stepStyle}
|
243
|
+
/>
|
244
|
+
)}
|
245
|
+
{children}
|
246
|
+
</FormContext.Provider>
|
247
|
+
);
|
248
|
+
};
|
@@ -0,0 +1,174 @@
|
|
1
|
+
// src/forms/Input.tsx
|
2
|
+
|
3
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
4
|
+
import {
|
5
|
+
TextInput,
|
6
|
+
StyleSheet,
|
7
|
+
View,
|
8
|
+
TextInputProps,
|
9
|
+
} from "react-native";
|
10
|
+
import { useFormContext } from "./FormContext";
|
11
|
+
import { Text } from "../fonts/Text";
|
12
|
+
import ErrorList from "./ErrorList";
|
13
|
+
import { ThemeContext } from "aport-themes";
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Defines the props for the Input component.
|
17
|
+
*/
|
18
|
+
interface InputProps extends TextInputProps {
|
19
|
+
/**
|
20
|
+
* The unique identifier for the input field, used to manage its state within the form.
|
21
|
+
*/
|
22
|
+
name: string;
|
23
|
+
|
24
|
+
/**
|
25
|
+
* Optional first value if you want to set values with fetch or dont have empty inputs.
|
26
|
+
*/
|
27
|
+
firstValue?: string | number; // Optional prop for the initial value
|
28
|
+
|
29
|
+
/**
|
30
|
+
* The text label displayed above the input field.
|
31
|
+
*/
|
32
|
+
label: string;
|
33
|
+
|
34
|
+
/**
|
35
|
+
* Specifies the type of input and applies corresponding formatting.
|
36
|
+
* - 'phone': Formats input as a phone number (xxx-xxx-xxxx).
|
37
|
+
* - 'id': Converts all input characters to uppercase (useful for IDs, passports).
|
38
|
+
* - 'uppercase': Forces all input characters to uppercase (useful for codes).
|
39
|
+
* - 'numeric': Restricts input to numeric characters only.
|
40
|
+
*/
|
41
|
+
inputType?: "phone" | "id" | "uppercase" | "numeric";
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Input component that supports labels, error messages, and integrates with the Theme system.
|
46
|
+
*
|
47
|
+
* @param {InputProps} props - Props passed to the component.
|
48
|
+
* @returns {JSX.Element} The rendered Input component.
|
49
|
+
*
|
50
|
+
* @example
|
51
|
+
* <Input
|
52
|
+
* name="email"
|
53
|
+
* label="Email"
|
54
|
+
* keyboardType="email-address"
|
55
|
+
* inputType="id"
|
56
|
+
* />
|
57
|
+
*/
|
58
|
+
export const Input: React.FC<InputProps> = ({
|
59
|
+
name,
|
60
|
+
label,
|
61
|
+
inputType,
|
62
|
+
firstValue,
|
63
|
+
editable = true,
|
64
|
+
style,
|
65
|
+
...rest
|
66
|
+
}) => {
|
67
|
+
const { formValues, setFormValue, errors: formErrors } = useFormContext();
|
68
|
+
const { theme } = useContext(ThemeContext);
|
69
|
+
const { colors } = theme;
|
70
|
+
|
71
|
+
const [internalValue, setInternalValue] = useState<string>("");
|
72
|
+
const isFirstRender = useRef(true); // Track the first render
|
73
|
+
|
74
|
+
// Initialize the internal value when `firstValue` changes or on first render
|
75
|
+
useEffect(() => {
|
76
|
+
if (isFirstRender.current) {
|
77
|
+
isFirstRender.current = false;
|
78
|
+
if (firstValue !== undefined) {
|
79
|
+
firstValue=firstValue.toString()
|
80
|
+
setInternalValue(firstValue);
|
81
|
+
setFormValue(name, firstValue, firstValue); // Pass firstValue here
|
82
|
+
} else {
|
83
|
+
setInternalValue(formValues[name] || "");
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}, [firstValue]);
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Handles text changes in the input field, applying formatting based on the inputType.
|
90
|
+
*
|
91
|
+
* @param {string} text - The current text input by the user.
|
92
|
+
*/
|
93
|
+
const handleChange = (text: string) => {
|
94
|
+
let formattedText = text;
|
95
|
+
|
96
|
+
// Apply different formatting based on inputType
|
97
|
+
switch (inputType) {
|
98
|
+
case "phone":
|
99
|
+
// Remove all non-digit characters
|
100
|
+
formattedText = text.replace(/\D/g, "");
|
101
|
+
// Insert hyphens to format as xxx-xxx-xxxx
|
102
|
+
if (formattedText.length > 3 && formattedText.length <= 6) {
|
103
|
+
formattedText = `${formattedText.slice(0, 3)}-${formattedText.slice(3)}`;
|
104
|
+
} else if (formattedText.length > 6) {
|
105
|
+
formattedText = `${formattedText.slice(0, 3)}-${formattedText.slice(
|
106
|
+
3,
|
107
|
+
6
|
108
|
+
)}-${formattedText.slice(6, 10)}`;
|
109
|
+
}
|
110
|
+
break;
|
111
|
+
case "id":
|
112
|
+
case "uppercase":
|
113
|
+
// Convert all input to uppercase
|
114
|
+
formattedText = text.toUpperCase();
|
115
|
+
break;
|
116
|
+
case "numeric":
|
117
|
+
// Allow only numeric input
|
118
|
+
formattedText = text.replace(/\D/g, "");
|
119
|
+
break;
|
120
|
+
default:
|
121
|
+
break;
|
122
|
+
}
|
123
|
+
|
124
|
+
setInternalValue(formattedText);
|
125
|
+
setFormValue(name, formattedText);
|
126
|
+
};
|
127
|
+
|
128
|
+
return (
|
129
|
+
<View style={styles.container}>
|
130
|
+
{/* Label */}
|
131
|
+
<Text style={[styles.label, { color: colors.text.hex }]}>{label}</Text>
|
132
|
+
|
133
|
+
{/* Input Field */}
|
134
|
+
<TextInput
|
135
|
+
style={[
|
136
|
+
styles.input,
|
137
|
+
{
|
138
|
+
backgroundColor: colors.body.hex,
|
139
|
+
borderColor: formErrors[name] ? colors.error.hex : "#CCC",
|
140
|
+
color: colors.text.hex,
|
141
|
+
},
|
142
|
+
style,
|
143
|
+
]}
|
144
|
+
value={internalValue}
|
145
|
+
onChangeText={handleChange}
|
146
|
+
placeholder={label}
|
147
|
+
editable={editable}
|
148
|
+
placeholderTextColor={colors.placeHolder.hex} // Can use a lighter version if needed
|
149
|
+
{...rest}
|
150
|
+
/>
|
151
|
+
|
152
|
+
{/* Display errors only if form has been submitted and there are errors */}
|
153
|
+
{formErrors[name] && formErrors[name].length > 0 && (
|
154
|
+
<ErrorList errors={formErrors[name]} />
|
155
|
+
)}
|
156
|
+
</View>
|
157
|
+
);
|
158
|
+
};
|
159
|
+
|
160
|
+
const styles = StyleSheet.create({
|
161
|
+
container: {
|
162
|
+
marginBottom: 16,
|
163
|
+
},
|
164
|
+
label: {
|
165
|
+
marginBottom: 4,
|
166
|
+
fontSize: 14,
|
167
|
+
},
|
168
|
+
input: {
|
169
|
+
height: 40,
|
170
|
+
borderWidth: 1,
|
171
|
+
borderRadius: 4,
|
172
|
+
paddingHorizontal: 8,
|
173
|
+
},
|
174
|
+
});
|
@@ -0,0 +1,184 @@
|
|
1
|
+
// src/components/InputAttach.tsx
|
2
|
+
|
3
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
4
|
+
import { View, TouchableOpacity, Image, StyleSheet, Alert } from "react-native";
|
5
|
+
import { useFormContext } from "./FormContext";
|
6
|
+
import * as ImagePicker from "expo-image-picker";
|
7
|
+
import { Text } from "../fonts";
|
8
|
+
import { ThemeContext } from "aport-themes";
|
9
|
+
import ErrorList from "./ErrorList";
|
10
|
+
|
11
|
+
interface FileData {
|
12
|
+
uri: string;
|
13
|
+
name: string;
|
14
|
+
type: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
interface InputAttachProps {
|
18
|
+
name: string;
|
19
|
+
type?: string[]; // Accepted file types
|
20
|
+
amount?: number; // Maximum number of files
|
21
|
+
disabled?: boolean; // If the input is pressable
|
22
|
+
placeholder?: string; // Placeholder text
|
23
|
+
firstValue?: FileData[]; // Pre-selected files
|
24
|
+
|
25
|
+
}
|
26
|
+
|
27
|
+
const InputAttach: React.FC<InputAttachProps> = ({
|
28
|
+
name,
|
29
|
+
type = ["png", "jpg", "jpeg"],
|
30
|
+
amount = 1,
|
31
|
+
disabled = false,
|
32
|
+
placeholder = "Upload an image",
|
33
|
+
firstValue = [],
|
34
|
+
}) => {
|
35
|
+
const { setFormValue, errors: formErrors } = useFormContext();
|
36
|
+
const [selectedFiles, setSelectedFiles] = useState<FileData[]>([]);
|
37
|
+
|
38
|
+
// Ref to track initialization
|
39
|
+
const isInitialized = useRef(false);
|
40
|
+
|
41
|
+
// Sync firstValue with selectedFiles when it changes
|
42
|
+
useEffect(() => {
|
43
|
+
if (!isInitialized.current) {
|
44
|
+
setSelectedFiles(firstValue);
|
45
|
+
setFormValue(name, firstValue, firstValue); // Pass firstValue here
|
46
|
+
isInitialized.current = true;
|
47
|
+
}
|
48
|
+
}, [firstValue, name, setFormValue]);
|
49
|
+
|
50
|
+
const { theme } = useContext(ThemeContext);
|
51
|
+
const { colors } = theme;
|
52
|
+
|
53
|
+
const pickImage = async () => {
|
54
|
+
if (disabled) return;
|
55
|
+
|
56
|
+
const permissionResult =
|
57
|
+
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
58
|
+
if (!permissionResult.granted) {
|
59
|
+
Alert.alert(
|
60
|
+
"Permission Required",
|
61
|
+
"We need access to your photos to upload images."
|
62
|
+
);
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
67
|
+
mediaTypes: ["images"], // Updated usage
|
68
|
+
allowsEditing: true,
|
69
|
+
allowsMultipleSelection: amount > 1,
|
70
|
+
quality: 1,
|
71
|
+
});
|
72
|
+
|
73
|
+
if (!result.canceled) {
|
74
|
+
const selectedAssets = result.assets || [result]; // Handle different return formats
|
75
|
+
const newFiles: FileData[] = selectedAssets.map((asset) => ({
|
76
|
+
uri: asset.uri,
|
77
|
+
name: asset.fileName || "unknown.jpg",
|
78
|
+
type: asset.mimeType || "image/jpeg",
|
79
|
+
}));
|
80
|
+
|
81
|
+
// Validate file types
|
82
|
+
const invalidFiles = newFiles.filter(
|
83
|
+
(file) => !type.some((ext) => file.name.toLowerCase().endsWith(ext))
|
84
|
+
);
|
85
|
+
if (invalidFiles.length > 0) {
|
86
|
+
Alert.alert(
|
87
|
+
"Invalid File Type",
|
88
|
+
`Please upload files of type: ${type.join(", ")}`
|
89
|
+
);
|
90
|
+
return;
|
91
|
+
}
|
92
|
+
|
93
|
+
// Check max amount
|
94
|
+
if (selectedFiles.length + newFiles.length > amount) {
|
95
|
+
Alert.alert("Limit Exceeded", `You can upload up to ${amount} files.`);
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
|
99
|
+
const updatedFiles = [...selectedFiles, ...newFiles].slice(0, amount);
|
100
|
+
setSelectedFiles(updatedFiles);
|
101
|
+
setFormValue(name, updatedFiles); // Update form context
|
102
|
+
}
|
103
|
+
};
|
104
|
+
|
105
|
+
const removeFile = (index: number) => {
|
106
|
+
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
|
107
|
+
setSelectedFiles(updatedFiles);
|
108
|
+
setFormValue(name, updatedFiles); // Update form context
|
109
|
+
};
|
110
|
+
|
111
|
+
return (
|
112
|
+
<View style={styles.container}>
|
113
|
+
<Text style={[styles.label, { color: colors.text.hex }]}>
|
114
|
+
{placeholder}
|
115
|
+
</Text>
|
116
|
+
<View style={styles.fileContainer}>
|
117
|
+
{selectedFiles.map((file, index) => (
|
118
|
+
<View key={index} style={styles.fileItem}>
|
119
|
+
<Image source={{ uri: file.uri }} style={styles.imagePreview} />
|
120
|
+
<TouchableOpacity
|
121
|
+
onPress={() => removeFile(index)}
|
122
|
+
style={[
|
123
|
+
styles.removeButton,
|
124
|
+
{ backgroundColor: colors.error.hex },
|
125
|
+
]}
|
126
|
+
>
|
127
|
+
<Text style={styles.removeButtonText}>X</Text>
|
128
|
+
</TouchableOpacity>
|
129
|
+
</View>
|
130
|
+
))}
|
131
|
+
{selectedFiles.length < amount && (
|
132
|
+
<TouchableOpacity
|
133
|
+
onPress={pickImage}
|
134
|
+
style={[
|
135
|
+
styles.addButton,
|
136
|
+
{
|
137
|
+
backgroundColor: colors.body.hex,
|
138
|
+
borderColor: formErrors[name] && colors.error.hex,
|
139
|
+
},
|
140
|
+
]}
|
141
|
+
disabled={disabled}
|
142
|
+
>
|
143
|
+
<Text
|
144
|
+
style={[styles.addButtonText, { color: colors.placeHolder.hex }]}
|
145
|
+
>+</Text>
|
146
|
+
</TouchableOpacity>
|
147
|
+
)}
|
148
|
+
</View>
|
149
|
+
{formErrors[name] && formErrors[name].length > 0 && (
|
150
|
+
<ErrorList errors={formErrors[name]} />
|
151
|
+
)}
|
152
|
+
</View>
|
153
|
+
);
|
154
|
+
};
|
155
|
+
|
156
|
+
const styles = StyleSheet.create({
|
157
|
+
container: { marginVertical: 10 },
|
158
|
+
label: { marginTop: 5 },
|
159
|
+
fileContainer: { flexDirection: "row", flexWrap: "wrap", gap: 10 },
|
160
|
+
fileItem: { position: "relative", width: 100, height: 100, marginRight: 10 },
|
161
|
+
imagePreview: { width: "100%", height: "100%", borderRadius: 5 },
|
162
|
+
removeButton: {
|
163
|
+
position: "absolute",
|
164
|
+
top: 0,
|
165
|
+
right: 0,
|
166
|
+
width: 20,
|
167
|
+
height: 20,
|
168
|
+
justifyContent: "center",
|
169
|
+
alignItems: "center",
|
170
|
+
borderRadius: 10,
|
171
|
+
},
|
172
|
+
removeButtonText: { color: "#fff", fontWeight: "bold" },
|
173
|
+
addButton: {
|
174
|
+
width: 100,
|
175
|
+
height: 100,
|
176
|
+
justifyContent: "center",
|
177
|
+
alignItems: "center",
|
178
|
+
borderRadius: 5,
|
179
|
+
},
|
180
|
+
addButtonText: { fontSize: 24 },
|
181
|
+
error: { marginTop: 5 },
|
182
|
+
});
|
183
|
+
|
184
|
+
export default InputAttach;
|