freshyo/apps/admin-ui/components/BannerForm.tsx
2026-01-24 00:13:15 +05:30

262 lines
No EOL
8.9 KiB
TypeScript

import React, { useState } from 'react';
import { View, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui';
import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
export interface BannerFormData {
name: string;
imageUrl: string;
description: string;
productIds: number[];
redirectUrl: string;
// serialNum removed - will be assigned automatically by backend
}
interface BannerFormProps {
initialValues: BannerFormData;
onSubmit: (values: BannerFormData, imageUrl?: string) => Promise<void> | void;
onCancel: () => void;
submitButtonText?: string;
isSubmitting?: boolean;
existingImageUrl?: string;
mode?: 'create' | 'edit';
}
const validationSchema = Yup.object().shape({
name: Yup.string().trim().required('Banner name is required').max(255),
description: Yup.string().max(500),
productIds: Yup.array()
.of(Yup.number())
.optional(),
redirectUrl: Yup.string()
.url('Please enter a valid URL')
.optional(),
// serialNum validation removed - assigned automatically by backend
});
export default function BannerForm({
initialValues,
onSubmit,
onCancel,
submitButtonText = 'Create Banner',
isSubmitting = false,
existingImageUrl,
mode = 'create',
}: BannerFormProps) {
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
// Fetch products for dropdown
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const products = productsData?.products || [];
// Create product options for dropdown
const productOptions: DropdownOption[] = products.map(product => ({
label: `${product.name} (${product.price})`,
value: product.id,
}));
const handleImagePick = usePickImage({
setFile: async (assets: any) => {
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
setSelectedImages([]);
setDisplayImages([]);
return;
}
const files = Array.isArray(assets) ? assets : [assets];
const blobPromises = files.map(async (asset) => {
const response = await fetch(asset.uri);
const blob = await response.blob();
return { blob, mimeType: asset.mimeType || 'image/jpeg' };
});
const blobArray = await Promise.all(blobPromises);
setSelectedImages(blobArray);
setDisplayImages(files.map(asset => ({ uri: asset.uri })));
},
multiple: false, // Single image for banners
});
const handleRemoveImage = (uri: string) => {
const index = displayImages.findIndex(img => img.uri === uri);
if (index !== -1) {
const newDisplay = displayImages.filter((_, i) => i !== index);
const newFiles = selectedImages.filter((_, i) => i !== index);
setDisplayImages(newDisplay);
setSelectedImages(newFiles);
}
};
const handleFormikSubmit = async (values: BannerFormData) => {
try {
let imageUrl: string | undefined;
if (selectedImages.length > 0) {
// Generate upload URLs
const mimeTypes = selectedImages.map(s => s.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: 'store', // Using 'store' for now
mimeTypes,
});
// Upload image
const uploadUrl = uploadUrls[0];
const { blob, mimeType } = selectedImages[0];
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
body: blob,
headers: {
'Content-Type': mimeType,
},
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`);
}
imageUrl = uploadUrl;
}
// Call onSubmit with form values and imageUrl
await onSubmit(values, imageUrl);
} catch (error) {
console.error('Upload error:', error);
Alert.alert('Error', 'Failed to upload image');
}
};
return (
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleFormikSubmit}
>
{({
handleChange,
handleBlur,
handleSubmit,
values,
errors,
touched,
isValid,
dirty,
setFieldValue,
}) => (
<View style={{ flex: 1 }}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingHorizontal: 24, paddingVertical: 24 }}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<MyTextInput
topLabel="Banner Name"
placeholder="Enter banner name"
value={values.name}
onChangeText={handleChange('name')}
onBlur={handleBlur('name')}
style={{ marginBottom: errors.name && touched.name ? 8 : 16 }}
/>
{errors.name && touched.name && (
<MyText style={tw`text-red-500 text-xs mb-2 -mt-2`}>{errors.name}</MyText>
)}
<View style={{ marginBottom: 16 }}>
<MyText style={{ fontSize: 14, fontWeight: 'bold', color: '#374151', marginBottom: 12, textTransform: 'uppercase', letterSpacing: 0.05 }}>Banner Image</MyText>
<ImageUploader
images={displayImages}
existingImageUrls={existingImageUrl ? [existingImageUrl] : []}
onAddImage={handleImagePick}
onRemoveImage={handleRemoveImage}
onRemoveExistingImage={() => {
// Handle removing existing image in edit mode
}}
allowMultiple={false}
/>
</View>
<MyTextInput
topLabel="Description"
placeholder="Enter banner description (optional)"
value={values.description}
onChangeText={handleChange('description')}
onBlur={handleBlur('description')}
multiline
numberOfLines={3}
textAlignVertical="top"
style={{ marginBottom: errors.description && touched.description ? 8 : 16 }}
/>
{errors.description && touched.description && (
<MyText style={tw`text-red-500 text-xs mb-2 -mt-2`}>{errors.description}</MyText>
)}
<View style={{ marginBottom: 16 }}>
<BottomDropdown
label="Select Products"
options={productOptions}
value={values.productIds}
onValueChange={(value) => {
const selectedValues = Array.isArray(value) ? value : [value];
setFieldValue('productIds', selectedValues.map(v => Number(v)));
}}
placeholder="Select products for banner (optional)"
multiple={true}
/>
</View>
<MyTextInput
topLabel="Redirect URL (Optional)"
placeholder="https://example.com/redirect"
value={values.redirectUrl}
onChangeText={handleChange('redirectUrl')}
onBlur={handleBlur('redirectUrl')}
keyboardType="url"
autoCapitalize="none"
autoCorrect={false}
style={{ marginBottom: errors.redirectUrl && touched.redirectUrl ? 8 : 16 }}
/>
{errors.redirectUrl && touched.redirectUrl && (
<MyText style={tw`text-red-500 text-xs mb-2 -mt-2`}>{errors.redirectUrl}</MyText>
)}
{/* Action Buttons */}
<View style={tw`flex-row gap-4 mb-8`}>
<MyTouchableOpacity
onPress={onCancel}
disabled={isSubmitting}
style={tw`flex-1 bg-gray-100 rounded-lg py-4 items-center`}
>
<MyText style={tw`text-gray-700 font-semibold`}>Cancel</MyText>
</MyTouchableOpacity>
<MyTouchableOpacity
onPress={() => handleSubmit()}
disabled={isSubmitting || !isValid || !dirty}
style={tw`flex-1 rounded-lg py-4 items-center ${
isSubmitting || !isValid || !dirty
? 'bg-blue-400'
: 'bg-blue-600'
}`}
>
<MyText style={tw`text-white font-semibold`}>
{isSubmitting ? 'Saving...' : submitButtonText}
</MyText>
</MyTouchableOpacity>
</View>
</ScrollView>
</View>
)}
</Formik>
);
}