This commit is contained in:
shafi54 2026-02-01 20:36:21 +05:30
parent bac9b04a28
commit edbc506062
12 changed files with 7540 additions and 628 deletions

View file

@ -13,6 +13,26 @@
"bundleIdentifier": "in.freshyo.adminui", "bundleIdentifier": "in.freshyo.adminui",
"infoPlist": { "infoPlist": {
"LSApplicationQueriesSchemes": [ "LSApplicationQueriesSchemes": [
"ppemerchantsdkv1",
"ppemerchantsdkv2",
"ppemerchantsdkv3",
"paytmmp",
"gpay",
"ppemerchantsdkv1",
"ppemerchantsdkv2",
"ppemerchantsdkv3",
"paytmmp",
"gpay",
"ppemerchantsdkv1",
"ppemerchantsdkv2",
"ppemerchantsdkv3",
"paytmmp",
"gpay",
"ppemerchantsdkv1",
"ppemerchantsdkv2",
"ppemerchantsdkv3",
"paytmmp",
"gpay",
"ppemerchantsdkv1", "ppemerchantsdkv1",
"ppemerchantsdkv2", "ppemerchantsdkv2",
"ppemerchantsdkv3", "ppemerchantsdkv3",
@ -33,7 +53,8 @@
"ppemerchantsdkv3", "ppemerchantsdkv3",
"paytmmp", "paytmmp",
"gpay" "gpay"
] ],
"ITSAppUsesNonExemptEncryption": false
} }
}, },
"android": { "android": {

View file

@ -17,6 +17,7 @@ import {
BottomDialog, BottomDialog,
BottomDropdown, BottomDropdown,
} from "common-ui"; } from "common-ui";
import ProductsSelector from "../../../components/ProductsSelector";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { trpc } from "../../../src/trpc-client"; import { trpc } from "../../../src/trpc-client";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import MaterialIcons from "@expo/vector-icons/MaterialIcons";
@ -220,10 +221,7 @@ export default function CustomizePopularItems() {
product => !popularProducts.find(p => p.id === product.id) product => !popularProducts.find(p => p.id === product.id)
); );
const productOptions = availableProducts.map(product => ({
label: `${product.name} - ₹${product.price}`,
value: product.id,
}));
// Show loading state while data is being fetched // Show loading state while data is being fetched
if (isLoadingConstants || isLoadingProducts) { if (isLoadingConstants || isLoadingProducts) {
@ -399,12 +397,13 @@ export default function CustomizePopularItems() {
</View> </View>
) : ( ) : (
<> <>
<BottomDropdown <ProductsSelector
value={selectedProductId || 0}
onChange={(val) => setSelectedProductId(val as number)}
multiple={false}
label="Select Product" label="Select Product"
options={productOptions}
value={selectedProductId || ""}
onValueChange={(val) => setSelectedProductId(val as number)}
placeholder="Choose a product..." placeholder="Choose a product..."
labelFormat={(product) => `${product.name} - ₹${product.price}`}
/> />
<View style={tw`flex-row gap-3 mt-6`}> <View style={tw`flex-row gap-3 mt-6`}>

View file

@ -4,6 +4,7 @@ import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui'; import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui';
import { DropdownOption } from 'common-ui/src/components/bottom-dropdown'; import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image'; import usePickImage from 'common-ui/src/components/use-pick-image';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
@ -57,11 +58,7 @@ export default function BannerForm({
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({}); const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const products = productsData?.products || []; 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({ const handleImagePick = usePickImage({
@ -200,16 +197,16 @@ export default function BannerForm({
)} )}
<View style={{ marginBottom: 16 }}> <View style={{ marginBottom: 16 }}>
<BottomDropdown <ProductsSelector
label="Select Products"
options={productOptions}
value={values.productIds} value={values.productIds}
onValueChange={(value) => { onChange={(value) => {
const selectedValues = Array.isArray(value) ? value : [value]; const selectedValues = Array.isArray(value) ? value : [value];
setFieldValue('productIds', selectedValues.map(v => Number(v))); setFieldValue('productIds', selectedValues.map(v => Number(v)));
}} }}
placeholder="Select products for banner (optional)"
multiple={true} multiple={true}
label="Select Products"
placeholder="Select products for banner (optional)"
labelFormat={(product) => `${product.name} (${product.price})`}
/> />
</View> </View>

View file

@ -2,6 +2,7 @@ import React from 'react';
import { View, TouchableOpacity, Alert, ScrollView } from 'react-native'; import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } from 'common-ui'; import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } from 'common-ui';
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
@ -35,10 +36,7 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
const products = productsData?.products || []; const products = productsData?.products || [];
const productOptions = products.map(product => ({
label: `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`,
value: product.id,
}));
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
@ -115,14 +113,13 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
/> />
{/* Products Selection */} {/* Products Selection */}
<BottomDropdown <ProductsSelector
label="Products"
value={formik.values.product_ids} value={formik.values.product_ids}
options={productOptions} onChange={(value) => formik.setFieldValue('product_ids', value as number[])}
onValueChange={(value) => formik.setFieldValue('product_ids', value as number[])}
multiple={true} multiple={true}
label="Products"
placeholder="Select products" placeholder="Select products"
style={{ marginBottom: 16 }} labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`}
/> />
{/* Actions */} {/* Actions */}

View file

@ -0,0 +1,188 @@
import React, { useState, useMemo } from 'react';
import { View } from 'react-native';
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
import { trpc } from '../src/trpc-client';
import { tw } from 'common-ui';
interface Product {
id: number;
name: string;
price: number;
unit?: string;
shortDescription?: string | null;
isOutOfStock?: boolean;
isSuspended?: boolean;
storeId?: number | null;
unitNotation?: string;
}
interface Group {
id: number;
groupName: string;
products: Product[];
}
interface ProductsSelectorProps {
value: number | number[];
onChange: (value: number | number[]) => void;
multiple?: boolean;
label?: string;
placeholder?: string;
disabled?: boolean;
error?: boolean;
isDisabled?: (product: Product) => boolean;
labelFormat?: (product: Product) => string;
groups?: Group[];
selectedGroupIds?: number[];
onGroupChange?: (groupIds: number[]) => void;
showGroups?: boolean;
}
export default function ProductsSelector({
value,
onChange,
multiple = true,
label = 'Select Products',
placeholder = 'Select products',
disabled = false,
error = false,
isDisabled,
labelFormat,
groups = [],
showGroups = true,
selectedGroupIds = [],
onGroupChange,
}: ProductsSelectorProps) {
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const products = productsData?.products || [];
const [searchQuery, setSearchQuery] = useState('');
// Format product label: name (unit) (₹price)
const formatProductLabel = (product: Product): string => {
if (labelFormat) {
return labelFormat(product);
}
const unit = product.unit ? ` (${product.unit})` : '';
const price = ` (₹${product.price})`;
return `${product.name}${unit}${price}`;
};
// Handle group selection changes
const handleGroupChange = (newGroupIds: number[]) => {
if (!onGroupChange) return;
const previousGroupIds = selectedGroupIds;
// Find which groups were added and which were removed
const addedGroups = newGroupIds.filter(id => !previousGroupIds.includes(id));
const removedGroups = previousGroupIds.filter(id => !newGroupIds.includes(id));
// Get current selected products
let currentProducts = Array.isArray(value) ? [...value] : value ? [value] : [];
// Add products from newly selected groups
const addedProducts = addedGroups.flatMap(groupId => {
const group = groups.find(g => g.id === groupId);
return group?.products.map(p => p.id) || [];
});
// Remove products from deselected groups
const removedProducts = removedGroups.flatMap(groupId => {
const group = groups.find(g => g.id === groupId);
return group?.products.map(p => p.id) || [];
});
// Update product list: add new ones, remove deselected group ones
currentProducts = [...new Set([...currentProducts, ...addedProducts])];
currentProducts = currentProducts.filter(id => !removedProducts.includes(id));
onGroupChange(newGroupIds);
if (multiple) {
onChange(currentProducts.length > 0 ? currentProducts : []);
} else {
onChange(currentProducts.length > 0 ? currentProducts[0] : 0);
}
};
// Filter products based on search query
const filteredProducts = useMemo(() => {
if (!searchQuery.trim()) return products;
const query = searchQuery.toLowerCase();
return products.filter(product =>
product.name.toLowerCase().includes(query) ||
(product.shortDescription && product.shortDescription.toLowerCase().includes(query)) ||
(product.unit && product.unit.toLowerCase().includes(query))
);
}, [products, searchQuery]);
// Build dropdown options
const productOptions: DropdownOption[] = useMemo(() => {
return filteredProducts.map((product) => {
const isFromGroup = selectedGroupIds.length > 0 && groups.some(group =>
selectedGroupIds.includes(group.id) && group.products.some(p => p.id === product.id)
);
const isProductDisabled = isDisabled ? isDisabled(product as Product) : false;
return {
label: `${formatProductLabel(product as Product)}${isFromGroup ? ' (from group)' : ''}`,
value: product.id.toString(),
disabled: isProductDisabled,
};
});
}, [filteredProducts, selectedGroupIds, groups, isDisabled, labelFormat]);
// Build group options if groups are provided
const groupOptions: DropdownOption[] = useMemo(() => {
return groups.map(group => ({
label: group.groupName,
value: group.id.toString(),
}));
}, [groups]);
return (
<View style={tw`w-full`}>
{/* Groups selector (if groups are provided and showGroups is true) */}
{showGroups && groups.length > 0 && (
<View style={tw`mb-4`}>
<BottomDropdown
label="Select Product Groups"
options={groupOptions}
value={selectedGroupIds.map(id => id.toString())}
onValueChange={(value) => {
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
const newGroupIds = selectedValues.map(v => parseInt(v as string));
handleGroupChange(newGroupIds);
}}
placeholder="Select product groups (optional)"
multiple={true}
/>
</View>
)}
{/* Products selector */}
<BottomDropdown
label={label}
options={productOptions}
value={multiple
? (Array.isArray(value) ? value.map(id => id.toString()) : [])
: (value ? value.toString() : '')
}
onValueChange={(selectedValue) => {
if (multiple) {
const selectedValues = Array.isArray(selectedValue) ? selectedValue : typeof selectedValue === 'string' ? [selectedValue] : [];
onChange(selectedValues.map(v => Number(v)));
} else {
onChange(Number(selectedValue));
}
}}
placeholder={placeholder}
multiple={multiple}
disabled={disabled}
error={error}
onSearch={setSearchQuery}
/>
</View>
);
}

View file

@ -1,10 +1,10 @@
import React, { useState, useEffect, useMemo } from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, Alert } from 'react-native'; import { View, Text, TouchableOpacity, Alert } from 'react-native';
import { Formik, FieldArray } from 'formik'; import { Formik, FieldArray } from 'formik';
import DateTimePickerMod from 'common-ui/src/components/date-time-picker'; import DateTimePickerMod from 'common-ui/src/components/date-time-picker';
import { tw, MyTextInput } from 'common-ui'; import { tw, MyTextInput } from 'common-ui';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown'; import ProductsSelector from '../components/ProductsSelector';
interface VendorSnippet { interface VendorSnippet {
name: string; name: string;
@ -44,10 +44,8 @@ export default function SlotForm({
const isEditMode = !!slotId; const isEditMode = !!slotId;
const isPending = isCreating || isUpdating; const isPending = isCreating || isUpdating;
// Fetch products and groups // Fetch groups
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const { data: groupsData } = trpc.admin.product.getGroups.useQuery(); const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
const products = productsData?.products || [];
@ -120,25 +118,13 @@ export default function SlotForm({
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
{({ handleSubmit, values, setFieldValue }) => { {({ handleSubmit, values, setFieldValue }) => {
// Collect all product IDs from selected groups // Map groups data to match ProductsSelector types (convert price from string to number)
const allGroupProductIds = (values?.selectedGroupIds || []).flatMap(groupId => { const mappedGroups = (groupsData?.groups || []).map(group => ({
const group = (groupsData?.groups || []).find(g => g.id === groupId); ...group,
return group?.products.map(p => p.id) || []; products: group.products.map(product => ({
}); ...product,
// Remove duplicates price: parseFloat(product.price as unknown as string) || 0,
const groupProductIds = [...new Set(allGroupProductIds)]; })),
const productOptions: DropdownOption[] = products.map(product => {
return {
label: `${product.name}${groupProductIds.includes(product.id) ? ' (from group)' : ''}`,
value: product.id.toString(),
disabled: groupProductIds.includes(product.id),
}});
const groupOptions: DropdownOption[] = (groupsData?.groups || []).map(group => ({
label: group.groupName,
value: group.id.toString(),
})); }));
return ( return (
@ -158,42 +144,14 @@ export default function SlotForm({
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<Text style={tw`text-base mb-2`}>Select Product Groups (Optional)</Text> <ProductsSelector
<BottomDropdown value={values.selectedProductIds}
label="Select Product Groups" onChange={(newProductIds) => setFieldValue('selectedProductIds', newProductIds)}
options={groupOptions} groups={mappedGroups}
value={values.selectedGroupIds.map(id => id.toString())} selectedGroupIds={values.selectedGroupIds}
onValueChange={(value) => { onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
const groupIds = selectedValues.map(v => parseInt(v as string));
setFieldValue('selectedGroupIds', groupIds);
// Collect all products from selected groups
const allGroupProducts = groupIds.flatMap(groupId => {
const group = (groupsData?.groups || []).find(g => g.id === groupId);
return group?.products.map(p => p.id) || [];
});
// Remove duplicates
const uniqueProducts = [...new Set(allGroupProducts)];
setFieldValue('selectedProductIds', uniqueProducts);
}}
placeholder="Select product groups"
multiple={true}
/>
</View>
<View style={tw`mb-4`}>
<Text style={tw`text-base mb-2`}>Select Products (Optional)</Text>
<BottomDropdown
label="Select Products" label="Select Products"
options={productOptions}
value={values.selectedProductIds.map(id => id.toString())}
onValueChange={(value) => {
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
setFieldValue('selectedProductIds', selectedValues.map(v => Number(v)));
}}
placeholder="Select products for this slot" placeholder="Select products for this slot"
multiple={true}
/> />
</View> </View>
@ -214,43 +172,20 @@ export default function SlotForm({
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<BottomDropdown <ProductsSelector
label="Select Groups" value={snippet.productIds || []}
options={groupOptions.filter(option => onChange={(newProductIds) => setFieldValue(`vendorSnippetList.${index}.productIds`, newProductIds)}
values.selectedGroupIds.includes(Number(option.value)) groups={mappedGroups.filter(group =>
)} values.selectedGroupIds.includes(group.id)
value={snippet.groupIds?.map(id => id.toString()) || []} ).map(group => ({
onValueChange={(value) => { ...group,
const selectedValues = Array.isArray(value) ? value : [value]; products: group.products.filter(p => values.selectedProductIds.includes(p.id))
const selectedGroupIds = selectedValues.map(v => parseInt(v as string)); }))}
setFieldValue(`vendorSnippetList.${index}.groupIds`, selectedGroupIds); selectedGroupIds={snippet.groupIds || []}
onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)}
// Auto-populate products from selected groups
const allSnippetProducts = selectedGroupIds.flatMap(groupId => {
const group = (groupsData?.groups || []).find(g => g.id === groupId);
return group?.products.map(p => p.id) || [];
});
// Remove duplicates
const uniqueSnippetProducts = [...new Set(allSnippetProducts)];
setFieldValue(`vendorSnippetList.${index}.productIds`, uniqueSnippetProducts);
}}
placeholder="Select groups for snippet"
multiple={true}
/>
</View>
<View style={tw`mb-4`}>
<BottomDropdown
label="Select Products" label="Select Products"
options={productOptions.filter(option =>
values.selectedProductIds.includes(Number(option.value))
)}
value={snippet.productIds?.map(id => id.toString()) || []}
onValueChange={(value) => {
const selectedValues = Array.isArray(value) ? value : [value];
setFieldValue(`vendorSnippetList.${index}.productIds`, selectedValues.map(v => parseInt(v as string)));
}}
placeholder="Select products for snippet" placeholder="Select products for snippet"
multiple={true} isDisabled={(product) => !values.selectedProductIds.includes(product.id)}
/> />
</View> </View>
<TouchableOpacity <TouchableOpacity

View file

@ -3,6 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui'; import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui';
import ProductsSelector from './ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import usePickImage from 'common-ui/src/components/use-pick-image'; import usePickImage from 'common-ui/src/components/use-pick-image';
@ -63,11 +64,7 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
value: staff.id, value: staff.id,
})) || []; })) || [];
const productOptions = productsData?.products.map(product => ({
label: `${product.name} - ₹${product.price}`,
value: product.id,
disabled: product.storeId !== null && product.storeId !== storeId, // Disable if belongs to another store
})) || [];
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation(); const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
@ -185,14 +182,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
error={!!(touched.owner && errors.owner)} error={!!(touched.owner && errors.owner)}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<BottomDropdown <ProductsSelector
label="Products"
value={values.products || []} value={values.products || []}
options={productOptions} onChange={(value) => setFieldValue('products', value)}
onValueChange={(value) => setFieldValue('products', value)} multiple={true}
label="Products"
placeholder="Select products" placeholder="Select products"
multiple isDisabled={(product) => product.storeId !== null && product.storeId !== storeId}
style={{ marginBottom: 16 }} labelFormat={(product) => `${product.name} - ₹${product.price}`}
/> />
<View style={tw`mb-6`}> <View style={tw`mb-6`}>
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText> <MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>

View file

@ -3,6 +3,7 @@ import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { MyText, tw, DatePicker, MyTextInput } from 'common-ui'; import { MyText, tw, DatePicker, MyTextInput } from 'common-ui';
import BottomDropdown from 'common-ui/src/components/bottom-dropdown'; import BottomDropdown from 'common-ui/src/components/bottom-dropdown';
import ProductsSelector from '../components/ProductsSelector';
import { trpc } from '../src/trpc-client'; import { trpc } from '../src/trpc-client';
import { VendorSnippetForm as VendorSnippetFormType, VendorSnippet } from '../types/vendor-snippets'; import { VendorSnippetForm as VendorSnippetFormType, VendorSnippet } from '../types/vendor-snippets';
@ -20,9 +21,8 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
showIsPermanentCheckbox = false, showIsPermanentCheckbox = false,
}) => { }) => {
// Fetch slots and products // Fetch slots
const { data: slotsData } = trpc.user.slots.getSlots.useQuery(); const { data: slotsData } = trpc.user.slots.getSlots.useQuery();
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
const createSnippet = trpc.admin.vendorSnippets.create.useMutation(); const createSnippet = trpc.admin.vendorSnippets.create.useMutation();
const updateSnippet = trpc.admin.vendorSnippets.update.useMutation(); const updateSnippet = trpc.admin.vendorSnippets.update.useMutation();
@ -108,16 +108,6 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
value: slot.id.toString(), value: slot.id.toString(),
})) || []; })) || [];
const productOptions = productsData?.products.map(product => ({
label: `${product.name} (${product.unit})`,
value: product.id.toString(),
})) || [];
const selectedProductLabels = formik.values.productIds
.map(id => productOptions.find(opt => opt.value === id)?.label)
.filter(Boolean)
.join(', ');
return ( return (
<View style={tw`flex-1 bg-white`}> <View style={tw`flex-1 bg-white`}>
<View style={tw`px-6 py-4 border-b border-gray-200`}> <View style={tw`px-6 py-4 border-b border-gray-200`}>
@ -190,20 +180,14 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
{/* Product Selection */} {/* Product Selection */}
<View> <View>
<MyText style={tw`text-gray-700 font-medium mb-2`}>Products</MyText> <ProductsSelector
<BottomDropdown value={formik.values.productIds.map(id => parseInt(id))}
label="Select Products" onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
value={formik.values.productIds}
options={productOptions}
onValueChange={(values) => formik.setFieldValue('productIds', values)}
multiple={true} multiple={true}
label="Select Products"
placeholder="Select products" placeholder="Select products"
labelFormat={(product) => `${product.name} (${product.unit})`}
/> />
{formik.values.productIds.length > 0 && (
<MyText style={tw`text-sm text-gray-600 mt-2`}>
Selected: {selectedProductLabels}
</MyText>
)}
{formik.errors.productIds && formik.touched.productIds && ( {formik.errors.productIds && formik.touched.productIds && (
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText> <MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText>
)} )}

View file

@ -13,8 +13,8 @@ config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve modules from // 2. Let Metro know where to resolve modules from
config.resolver.nodeModulesPaths = [ config.resolver.nodeModulesPaths = [
path.resolve(workspaceRoot, 'node_modules'),
path.resolve(projectRoot, 'node_modules'), path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
]; ];
// 3. Force Metro to resolve (sub)dependencies only from the top-level `node_modules` // 3. Force Metro to resolve (sub)dependencies only from the top-level `node_modules`

File diff suppressed because it is too large Load diff

View file

@ -8,12 +8,69 @@ export default function Inauguration() {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const triggerConfetti = useCallback(() => { const triggerConfetti = useCallback(() => {
const duration = 5 * 1000; const duration = 8 * 1000;
const animationEnd = Date.now() + duration; const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min; const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min;
// Cracker explosion function - shoots upward then falls
const fireCracker = (x: number, delay: number, colors: string[]) => {
setTimeout(() => {
// Initial burst - shoots upward like a firecracker
confetti({
particleCount: 120,
spread: 360,
origin: { x, y: 0.9 },
colors: colors,
shapes: ['circle', 'square', 'star'],
scalar: randomInRange(1.2, 2.0),
startVelocity: randomInRange(45, 65),
gravity: 1.2,
ticks: 150,
drift: randomInRange(-1, 1),
flat: false
});
// Secondary burst - shoots higher with different colors
setTimeout(() => {
confetti({
particleCount: 80,
spread: 360,
origin: { x, y: 0.8 },
colors: colors.slice().reverse(),
shapes: ['circle', 'square'],
scalar: randomInRange(1.0, 1.8),
startVelocity: randomInRange(35, 55),
gravity: 1.0,
ticks: 120,
drift: randomInRange(-0.5, 0.5),
flat: false
});
}, 150);
}, delay);
};
// Fire multiple crackers in sequence
const crackerColors = [
['#FF0000', '#FFD700', '#FF6B6B', '#FFA500'], // Red/Gold
['#2E90FA', '#53B1FD', '#84CAFF', '#FFFFFF'], // Blue/White
['#00FF00', '#32CD32', '#90EE90', '#98FB98'], // Green
['#FF1493', '#FF69B4', '#FFB6C1', '#FFC0CB'], // Pink
['#FFD700', '#FFA500', '#FF8C00', '#FFFF00'], // Gold/Orange
['#9400D3', '#8A2BE2', '#9370DB', '#BA55D3'], // Purple
];
// Launch crackers from different positions
fireCracker(0.15, 0, crackerColors[0]);
fireCracker(0.35, 300, crackerColors[1]);
fireCracker(0.5, 600, crackerColors[2]);
fireCracker(0.65, 900, crackerColors[3]);
fireCracker(0.85, 1200, crackerColors[4]);
fireCracker(0.25, 1500, crackerColors[5]);
fireCracker(0.75, 1800, crackerColors[0]);
fireCracker(0.45, 2100, crackerColors[2]);
// Continuous secondary bursts for the duration
const interval: any = setInterval(function() { const interval: any = setInterval(function() {
const timeLeft = animationEnd - Date.now(); const timeLeft = animationEnd - Date.now();
@ -21,27 +78,41 @@ export default function Inauguration() {
return clearInterval(interval); return clearInterval(interval);
} }
const particleCount = 50 * (timeLeft / duration); // Random mini-crackers
const miniCrackerX = randomInRange(0.1, 0.9);
const colors = ['#2E90FA', '#53B1FD', '#84CAFF', '#1570EF', '#175CD3']; const miniColors = crackerColors[Math.floor(Math.random() * crackerColors.length)];
confetti({ confetti({
...defaults, particleCount: 40,
particleCount, spread: 360,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }, origin: { x: miniCrackerX, y: 0.85 },
colors: colors, colors: miniColors,
shapes: ['circle', 'square'], shapes: ['circle', 'square', 'star'],
scalar: randomInRange(0.8, 1.5) scalar: randomInRange(0.8, 1.4),
startVelocity: randomInRange(30, 50),
gravity: 1.1,
ticks: 100,
drift: randomInRange(-0.8, 0.8),
flat: false
}); });
// Occasional sparkle burst
if (Math.random() > 0.7) {
confetti({ confetti({
...defaults, particleCount: 30,
particleCount, spread: 360,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }, origin: { x: randomInRange(0.2, 0.8), y: 0.9 },
colors: colors, colors: ['#FFFFFF', '#FFD700', '#C0C0C0'],
shapes: ['circle', 'square'], shapes: ['star', 'circle'],
scalar: randomInRange(0.8, 1.5) scalar: randomInRange(0.5, 1.0),
startVelocity: randomInRange(25, 40),
gravity: 0.8,
ticks: 80,
drift: 0,
flat: false
}); });
}, 250); }
}, 400);
}, []); }, []);
const handleKickstart = () => { const handleKickstart = () => {
@ -55,41 +126,100 @@ export default function Inauguration() {
return ( return (
<div className="relative w-full h-screen overflow-hidden"> <div className="relative w-full h-screen overflow-hidden">
{/* Animated Gradient Background */} {/* Elegant Garden Background */}
<div <div
className="absolute inset-0 animate-gradient-xy" className="absolute inset-0"
style={{ style={{
background: 'linear-gradient(135deg, #2E90FA 0%, #1570EF 25%, #53B1FD 50%, #84CAFF 75%, #2E90FA 100%)', background: `
backgroundSize: '400% 400%', linear-gradient(180deg,
animation: 'gradientShift 15s ease infinite' #e8f5e9 0%,
#c8e6c9 20%,
#a5d6a7 40%,
#81c784 60%,
#66bb6a 80%,
#4caf50 100%
)
`,
}} }}
/> />
{/* Animated Mesh Gradient Overlay */} {/* Soft Nature Mesh Overlay */}
<div className="absolute inset-0 opacity-60"> <div className="absolute inset-0 opacity-40">
<div <div
className="absolute inset-0 animate-pulse" className="absolute inset-0"
style={{ style={{
background: 'radial-gradient(circle at 20% 80%, #84CAFF 0%, transparent 50%), radial-gradient(circle at 80% 20%, #53B1FD 0%, transparent 50%), radial-gradient(circle at 40% 40%, #2E90FA 0%, transparent 40%)', background: `
filter: 'blur(60px)' radial-gradient(circle at 30% 70%, rgba(255,255,255,0.4) 0%, transparent 40%),
radial-gradient(circle at 70% 30%, rgba(200,230,201,0.5) 0%, transparent 35%),
radial-gradient(circle at 50% 50%, rgba(255,248,225,0.3) 0%, transparent 50%),
radial-gradient(circle at 20% 20%, rgba(232,245,233,0.4) 0%, transparent 30%),
radial-gradient(circle at 80% 80%, rgba(165,214,167,0.4) 0%, transparent 40%)
`,
filter: 'blur(80px)',
animation: 'gardenPulse 20s ease-in-out infinite'
}} }}
/> />
</div> </div>
{/* Floating Bubbles */} {/* Floating Petals */}
<div className="absolute inset-0 pointer-events-none overflow-hidden"> <div className="absolute inset-0 pointer-events-none overflow-hidden">
{[...Array(20)].map((_, i) => ( {[...Array(15)].map((_, i) => (
<div <div
key={i} key={`petal-${i}`}
className="absolute rounded-full" className="absolute"
style={{ style={{
width: `${Math.random() * 100 + 50}px`, width: `${Math.random() * 20 + 15}px`,
height: `${Math.random() * 100 + 50}px`, height: `${Math.random() * 20 + 15}px`,
left: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`,
background: `radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%)`, background: i % 3 === 0
animation: `floatBubble ${Math.random() * 10 + 10}s ease-in-out infinite`, ? 'radial-gradient(ellipse at center, rgba(255,182,193,0.7) 0%, rgba(255,192,203,0.3) 70%, transparent 100%)'
animationDelay: `${Math.random() * 5}s`, : i % 3 === 1
? 'radial-gradient(ellipse at center, rgba(255,255,224,0.7) 0%, rgba(255,250,205,0.3) 70%, transparent 100%)'
: 'radial-gradient(ellipse at center, rgba(221,160,221,0.6) 0%, rgba(216,191,216,0.3) 70%, transparent 100%)',
borderRadius: '50% 0 50% 0',
transform: `rotate(${Math.random() * 360}deg)`,
animation: `floatPetal ${Math.random() * 12 + 15}s ease-in-out infinite`,
animationDelay: `${Math.random() * 8}s`,
}}
/>
))}
{/* Floating Leaves */}
{[...Array(12)].map((_, i) => (
<div
key={`leaf-${i}`}
className="absolute"
style={{
width: `${Math.random() * 25 + 20}px`,
height: `${Math.random() * 15 + 12}px`,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
background: 'linear-gradient(45deg, rgba(76,175,80,0.4) 0%, rgba(129,199,132,0.3) 50%, rgba(165,214,167,0.2) 100%)',
borderRadius: '0 50% 0 50%',
transform: `rotate(${Math.random() * 360}deg)`,
animation: `floatLeaf ${Math.random() * 10 + 12}s ease-in-out infinite`,
animationDelay: `${Math.random() * 6}s`,
}}
/>
))}
</div>
{/* Elegant Light Rays */}
<div className="absolute inset-0 pointer-events-none">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="absolute"
style={{
width: '2px',
height: '100%',
left: `${20 + i * 15}%`,
background: 'linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.2) 30%, rgba(255,255,255,0.1) 70%, transparent 100%)',
transform: `rotate(${-15 + i * 5}deg)`,
transformOrigin: 'top center',
animation: `lightRay ${8 + i * 2}s ease-in-out infinite`,
animationDelay: `${i * 1.5}s`,
}} }}
/> />
))} ))}
@ -110,7 +240,7 @@ export default function Inauguration() {
{/* Logo Container with Glassmorphism */} {/* Logo Container with Glassmorphism */}
<div className="relative inline-block"> <div className="relative inline-block">
{/* Glow Effect Behind Logo */} {/* Glow Effect Behind Logo */}
<div className="absolute -inset-4 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-3xl blur-2xl opacity-40 animate-pulse" /> <div className="absolute -inset-4 bg-gradient-to-r from-[#66bb6a] to-[#a5d6a7] rounded-3xl blur-2xl opacity-40 animate-pulse" />
{/* Logo Text */} {/* Logo Text */}
<div className="relative"> <div className="relative">
@ -118,9 +248,9 @@ export default function Inauguration() {
<span <span
className="bg-clip-text text-transparent animate-shimmer" className="bg-clip-text text-transparent animate-shimmer"
style={{ style={{
backgroundImage: 'linear-gradient(90deg, #FFFFFF 0%, #E0F2FE 25%, #FFFFFF 50%, #E0F2FE 75%, #FFFFFF 100%)', backgroundImage: 'linear-gradient(90deg, #FFFFFF 0%, #E8F5E9 25%, #FFFFFF 50%, #E8F5E9 75%, #FFFFFF 100%)',
backgroundSize: '200% auto', backgroundSize: '200% auto',
textShadow: '0 0 40px rgba(46,144,250,0.5)', textShadow: '0 0 40px rgba(76,175,80,0.4)',
animation: 'shimmer 3s linear infinite' animation: 'shimmer 3s linear infinite'
}} }}
> >
@ -156,11 +286,11 @@ export default function Inauguration() {
<div className="absolute -inset-4 rounded-full border-2 border-white/20 group-hover:border-white/40 transition-all duration-500" /> <div className="absolute -inset-4 rounded-full border-2 border-white/20 group-hover:border-white/40 transition-all duration-500" />
{/* Button Container */} {/* Button Container */}
<div className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] p-[2px]"> <div className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] p-[2px]">
<div className="relative rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] px-12 py-6"> <div className="relative rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] px-12 py-6">
{/* Animated Background on Hover */} {/* Animated Background on Hover */}
<div <div
className="absolute inset-0 bg-gradient-to-r from-[#2E90FA] via-[#53B1FD] to-[#2E90FA] opacity-0 group-hover:opacity-100 transition-opacity duration-500" className="absolute inset-0 bg-gradient-to-r from-[#43a047] via-[#66bb6a] to-[#43a047] opacity-0 group-hover:opacity-100 transition-opacity duration-500"
style={{ backgroundSize: '200% auto' }} style={{ backgroundSize: '200% auto' }}
/> />
@ -194,7 +324,7 @@ export default function Inauguration() {
{/* Floating Particles Around Button */} {/* Floating Particles Around Button */}
<div className="absolute -top-2 -right-2 w-4 h-4 bg-white/60 rounded-full animate-float-fast" /> <div className="absolute -top-2 -right-2 w-4 h-4 bg-white/60 rounded-full animate-float-fast" />
<div className="absolute -bottom-2 -left-2 w-3 h-3 bg-[#84CAFF] rounded-full animate-float-delayed-fast" /> <div className="absolute -bottom-2 -left-2 w-3 h-3 bg-[#c8e6c9] rounded-full animate-float-delayed-fast" />
</button> </button>
{/* Subtext under button */} {/* Subtext under button */}
@ -206,12 +336,12 @@ export default function Inauguration() {
/* Welcome Message */ /* Welcome Message */
<div className="text-center animate-fade-in-up"> <div className="text-center animate-fade-in-up">
<div className="relative inline-block"> <div className="relative inline-block">
{/* Background glow */} {/* Background glow - elegant garden green */}
<div className="absolute -inset-8 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-full blur-3xl opacity-40" /> <div className="absolute -inset-8 bg-gradient-to-r from-[#66bb6a] to-[#81c784] rounded-full blur-3xl opacity-40" />
<div className="relative bg-white/10 backdrop-blur-lg rounded-3xl px-16 py-12 border border-white/20 shadow-2xl"> <div className="relative bg-white/10 backdrop-blur-lg rounded-3xl px-16 py-12 border border-white/20 shadow-2xl">
{/* Success Icon */} {/* Success Icon */}
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-gradient-to-r from-[#2E90FA] to-[#53B1FD] flex items-center justify-center shadow-lg"> <div className="w-20 h-20 mx-auto mb-6 rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] flex items-center justify-center shadow-lg">
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg> </svg>
@ -291,22 +421,67 @@ export default function Inauguration() {
100% { background-position: 200% center; } 100% { background-position: 200% center; }
} }
@keyframes floatBubble { @keyframes gardenPulse {
0%, 100% { 0%, 100% {
transform: translateY(0) translateX(0) scale(1); opacity: 0.4;
opacity: 0.3; transform: scale(1);
}
25% {
transform: translateY(-30px) translateX(10px) scale(1.1);
opacity: 0.5;
} }
50% { 50% {
transform: translateY(-20px) translateX(-10px) scale(0.9); opacity: 0.6;
opacity: 0.3; transform: scale(1.02);
}
}
@keyframes floatPetal {
0%, 100% {
transform: translateY(0) translateX(0) rotate(0deg);
opacity: 0.6;
}
20% {
transform: translateY(-40px) translateX(15px) rotate(45deg);
opacity: 0.8;
}
40% {
transform: translateY(-25px) translateX(-20px) rotate(90deg);
opacity: 0.5;
}
60% {
transform: translateY(-60px) translateX(10px) rotate(135deg);
opacity: 0.7;
}
80% {
transform: translateY(-35px) translateX(-15px) rotate(180deg);
opacity: 0.6;
}
}
@keyframes floatLeaf {
0%, 100% {
transform: translateY(0) translateX(0) rotate(0deg);
opacity: 0.5;
}
25% {
transform: translateY(-50px) translateX(20px) rotate(60deg);
opacity: 0.7;
}
50% {
transform: translateY(-30px) translateX(-25px) rotate(120deg);
opacity: 0.4;
} }
75% { 75% {
transform: translateY(-40px) translateX(5px) scale(1.05); transform: translateY(-70px) translateX(15px) rotate(180deg);
opacity: 0.4; opacity: 0.6;
}
}
@keyframes lightRay {
0%, 100% {
opacity: 0.1;
transform: rotate(-15deg) scaleY(1);
}
50% {
opacity: 0.3;
transform: rotate(-15deg) scaleY(1.1);
} }
} }

6386
session-ses_3ec7.md Normal file

File diff suppressed because one or more lines are too long