Compare commits

..

No commits in common. "86de06078ffc5ddf00b0c179b7cf39a2cdec69d4" and "38060e78ff8f97742243b6235b22e98f7b05d783" have entirely different histories.

21 changed files with 672 additions and 11329 deletions

View file

@ -13,26 +13,6 @@
"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",
@ -53,8 +33,7 @@
"ppemerchantsdkv3", "ppemerchantsdkv3",
"paytmmp", "paytmmp",
"gpay" "gpay"
], ]
"ITSAppUsesNonExemptEncryption": false
} }
}, },
"android": { "android": {

View file

@ -1,43 +1,22 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View } from 'react-native';
import { AppContainer } from 'common-ui'; import { AppContainer } from 'common-ui';
import SlotForm from '../../../components/SlotForm'; import SlotForm from '../../../components/SlotForm';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter } from 'expo-router';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '../../../src/trpc-client';
export default function AddSlot() { export default function AddSlot() {
const router = useRouter(); const router = useRouter();
const { baseslot } = useLocalSearchParams();
const baseSlotId = baseslot ? parseInt(baseslot as string) : null;
const { refetch } = trpc.admin.slots.getAll.useQuery(); const { refetch } = trpc.admin.slots.getAll.useQuery();
const { data: baseSlotData, isLoading } = trpc.admin.slots.getSlotById.useQuery(
{ id: baseSlotId! },
{ enabled: !!baseSlotId }
);
const handleSlotAdded = () => { const handleSlotAdded = () => {
refetch(); refetch();
router.back(); router.back();
}; };
if (isLoading && baseSlotId) {
return (
<AppContainer>
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Loading base slot...</Text>
</View>
</AppContainer>
);
}
return ( return (
<AppContainer> <AppContainer>
<SlotForm <SlotForm onSlotAdded={handleSlotAdded} />
onSlotAdded={handleSlotAdded}
initialProductIds={baseSlotData?.slot?.products?.map(p => p.id) || []}
initialGroupIds={baseSlotData?.slot?.groupIds || []}
/>
</AppContainer> </AppContainer>
); );
} }

View file

@ -17,7 +17,6 @@ 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";
@ -221,7 +220,10 @@ 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) {
@ -397,13 +399,12 @@ export default function CustomizePopularItems() {
</View> </View>
) : ( ) : (
<> <>
<ProductsSelector <BottomDropdown
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

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { View, TouchableOpacity, FlatList, Alert } from 'react-native'; import { View, TouchableOpacity, FlatList } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui'; import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui';
import { trpc } from '../../../src/trpc-client'; import { trpc } from '../../../src/trpc-client';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
@ -20,7 +20,6 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
setDialogProducts, setDialogProducts,
setDialogOpen, setDialogOpen,
}) => { }) => {
const [menuOpen, setMenuOpen] = useState(false);
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || []; const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
const displayProducts = slotProducts.slice(0, 2).join(', '); const displayProducts = slotProducts.slice(0, 2).join(', ');
@ -58,44 +57,9 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
<View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}> <View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}>
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText> <MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
</View> </View>
<TouchableOpacity
onPress={() => setMenuOpen(true)}
style={tw`ml-2 p-1`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<Entypo name="dots-three-vertical" size={20} color="#9CA3AF" />
</TouchableOpacity>
</View> </View>
</View> </View>
{/* Replicate Menu Dialog */}
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
router.push(`/add-slot?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText>
</View>
</TouchableOpacity>
</View>
</BottomDialog>
{/* Divider */} {/* Divider */}
<View style={tw`h-[1px] bg-gray-100 mb-4`} /> <View style={tw`h-[1px] bg-gray-100 mb-4`} />

View file

@ -4,7 +4,6 @@ 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';
@ -58,7 +57,11 @@ 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({
@ -197,16 +200,16 @@ export default function BannerForm({
)} )}
<View style={{ marginBottom: 16 }}> <View style={{ marginBottom: 16 }}>
<ProductsSelector <BottomDropdown
label="Select Products"
options={productOptions}
value={values.productIds} value={values.productIds}
onChange={(value) => { onValueChange={(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)));
}} }}
multiple={true}
label="Select Products"
placeholder="Select products for banner (optional)" placeholder="Select products for banner (optional)"
labelFormat={(product) => `${product.name} (${product.price})`} multiple={true}
/> />
</View> </View>

View file

@ -2,7 +2,6 @@ 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';
@ -36,7 +35,10 @@ 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: {
@ -113,13 +115,14 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
/> />
{/* Products Selection */} {/* Products Selection */}
<ProductsSelector <BottomDropdown
value={formik.values.product_ids}
onChange={(value) => formik.setFieldValue('product_ids', value as number[])}
multiple={true}
label="Products" label="Products"
value={formik.values.product_ids}
options={productOptions}
onValueChange={(value) => formik.setFieldValue('product_ids', value as number[])}
multiple={true}
placeholder="Select products" placeholder="Select products"
labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`} style={{ marginBottom: 16 }}
/> />
{/* Actions */} {/* Actions */}

View file

@ -1,188 +0,0 @@
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 from 'react'; import React, { useState, useEffect, useMemo } 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 ProductsSelector from '../components/ProductsSelector'; import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
interface VendorSnippet { interface VendorSnippet {
name: string; name: string;
@ -20,7 +20,6 @@ interface SlotFormProps {
initialIsActive?: boolean; initialIsActive?: boolean;
slotId?: number; slotId?: number;
initialProductIds?: number[]; initialProductIds?: number[];
initialGroupIds?: number[];
} }
export default function SlotForm({ export default function SlotForm({
@ -30,26 +29,13 @@ export default function SlotForm({
initialIsActive = true, initialIsActive = true,
slotId, slotId,
initialProductIds = [], initialProductIds = [],
initialGroupIds = [],
}: SlotFormProps) { }: SlotFormProps) {
const { data: slotData } = trpc.admin.slots.getSlotById.useQuery(
{ id: slotId! },
{ enabled: !!slotId }
);
const vendorSnippetsFromSlot = (slotData?.slot?.vendorSnippets || []).map((snippet: any) => ({
name: snippet.name || '',
groupIds: snippet.groupIds || [],
productIds: snippet.productIds || [],
validTill: snippet.validTill || undefined,
})) as VendorSnippet[];
const initialValues = { const initialValues = {
deliveryTime: initialDeliveryTime || (slotData?.slot?.deliveryTime ? new Date(slotData.slot.deliveryTime) : null), deliveryTime: initialDeliveryTime || null,
freezeTime: initialFreezeTime || (slotData?.slot?.freezeTime ? new Date(slotData.slot.freezeTime) : null), freezeTime: initialFreezeTime || null,
selectedGroupIds: initialGroupIds.length > 0 ? initialGroupIds : (slotData?.slot?.groupIds || []), selectedGroupIds: [] as number[],
selectedProductIds: initialProductIds.length > 0 ? initialProductIds : (slotData?.slot?.products?.map((p: any) => p.id) || []), selectedProductIds: initialProductIds,
vendorSnippetList: vendorSnippetsFromSlot, vendorSnippetList: [] as VendorSnippet[],
}; };
const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation(); const { mutate: createSlot, isPending: isCreating } = trpc.admin.slots.createSlot.useMutation();
@ -58,8 +44,10 @@ export default function SlotForm({
const isEditMode = !!slotId; const isEditMode = !!slotId;
const isPending = isCreating || isUpdating; const isPending = isCreating || isUpdating;
// Fetch groups // Fetch products and 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 || [];
@ -77,14 +65,20 @@ export default function SlotForm({
deliveryTime: values.deliveryTime.toISOString(), deliveryTime: values.deliveryTime.toISOString(),
freezeTime: values.freezeTime.toISOString(), freezeTime: values.freezeTime.toISOString(),
isActive: initialIsActive, isActive: initialIsActive,
groupIds: values.selectedGroupIds,
productIds: values.selectedProductIds, productIds: values.selectedProductIds,
vendorSnippets: values.vendorSnippetList.map((snippet: VendorSnippet) => ({ vendorSnippets: values.vendorSnippetList.map(snippet => ({
name: snippet.name, name: snippet.name,
productIds: snippet.productIds, productIds: snippet.productIds,
validTill: snippet.validTill, validTill: snippet.validTill,
})), })),
}; };
console.log({snippetList: values.vendorSnippetList})
values.vendorSnippetList.forEach((snippet, index) => {
console.log({snippet})
});
if (isEditMode && slotId) { if (isEditMode && slotId) {
@ -126,13 +120,25 @@ export default function SlotForm({
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
{({ handleSubmit, values, setFieldValue }) => { {({ handleSubmit, values, setFieldValue }) => {
// Map groups data to match ProductsSelector types (convert price from string to number) // Collect all product IDs from selected groups
const mappedGroups = (groupsData?.groups || []).map(group => ({ const allGroupProductIds = (values?.selectedGroupIds || []).flatMap(groupId => {
...group, const group = (groupsData?.groups || []).find(g => g.id === groupId);
products: group.products.map(product => ({ return group?.products.map(p => p.id) || [];
...product, });
price: parseFloat(product.price as unknown as string) || 0, // Remove duplicates
})), 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 (
@ -151,24 +157,52 @@ export default function SlotForm({
<DateTimePickerMod value={values.freezeTime} setValue={(value) => setFieldValue('freezeTime', value)} /> <DateTimePickerMod value={values.freezeTime} setValue={(value) => setFieldValue('freezeTime', value)} />
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<ProductsSelector <Text style={tw`text-base mb-2`}>Select Product Groups (Optional)</Text>
value={values.selectedProductIds} <BottomDropdown
onChange={(newProductIds) => setFieldValue('selectedProductIds', newProductIds)} label="Select Product Groups"
groups={mappedGroups} options={groupOptions}
selectedGroupIds={values.selectedGroupIds} value={values.selectedGroupIds.map(id => id.toString())}
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)} onValueChange={(value) => {
label="Select Products" const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
placeholder="Select products for this slot" const groupIds = selectedValues.map(v => parseInt(v as string));
/> setFieldValue('selectedGroupIds', groupIds);
</View>
// 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"
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"
multiple={true}
/>
</View>
{/* Vendor Snippets */} {/* Vendor Snippets */}
<FieldArray name="vendorSnippetList"> <FieldArray name="vendorSnippetList">
{({ push, remove }) => ( {({ push, remove }) => (
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<Text style={tw`text-lg font-semibold mb-4`}>Vendor Snippets</Text> <Text style={tw`text-lg font-semibold mb-4`}>Vendor Snippets</Text>
{values.vendorSnippetList.map((snippet: VendorSnippet, index: number) => ( {values.vendorSnippetList.map((snippet, index) => (
<View key={index} style={tw`bg-gray-50 p-4 rounded-lg mb-4`}> <View key={index} style={tw`bg-gray-50 p-4 rounded-lg mb-4`}>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<MyTextInput <MyTextInput
@ -180,20 +214,43 @@ export default function SlotForm({
</View> </View>
<View style={tw`mb-4`}> <View style={tw`mb-4`}>
<ProductsSelector <BottomDropdown
value={snippet.productIds || []} label="Select Groups"
onChange={(newProductIds) => setFieldValue(`vendorSnippetList.${index}.productIds`, newProductIds)} options={groupOptions.filter(option =>
groups={mappedGroups.filter(group => values.selectedGroupIds.includes(Number(option.value))
values.selectedGroupIds.includes(group.id) )}
).map(group => ({ value={snippet.groupIds?.map(id => id.toString()) || []}
...group, onValueChange={(value) => {
products: group.products.filter(p => values.selectedProductIds.includes(p.id)) const selectedValues = Array.isArray(value) ? value : [value];
}))} const selectedGroupIds = selectedValues.map(v => parseInt(v as string));
selectedGroupIds={snippet.groupIds || []} setFieldValue(`vendorSnippetList.${index}.groupIds`, selectedGroupIds);
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"
isDisabled={(product) => !values.selectedProductIds.includes(product.id)} multiple={true}
/> />
</View> </View>
<TouchableOpacity <TouchableOpacity

View file

@ -3,7 +3,6 @@ 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';
@ -64,7 +63,11 @@ 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();
@ -182,14 +185,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
error={!!(touched.owner && errors.owner)} error={!!(touched.owner && errors.owner)}
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
/> />
<ProductsSelector <BottomDropdown
value={values.products || []}
onChange={(value) => setFieldValue('products', value)}
multiple={true}
label="Products" label="Products"
value={values.products || []}
options={productOptions}
onValueChange={(value) => setFieldValue('products', value)}
placeholder="Select products" placeholder="Select products"
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId} multiple
labelFormat={(product) => `${product.name} - ₹${product.price}`} style={{ marginBottom: 16 }}
/> />
<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,7 +3,6 @@ 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';
@ -21,8 +20,9 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
showIsPermanentCheckbox = false, showIsPermanentCheckbox = false,
}) => { }) => {
// Fetch slots // Fetch slots and products
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,6 +108,16 @@ 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`}>
@ -180,14 +190,20 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
{/* Product Selection */} {/* Product Selection */}
<View> <View>
<ProductsSelector <MyText style={tw`text-gray-700 font-medium mb-2`}>Products</MyText>
value={formik.values.productIds.map(id => parseInt(id))} <BottomDropdown
onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
multiple={true}
label="Select Products" label="Select Products"
value={formik.values.productIds}
options={productOptions}
onValueChange={(values) => formik.setFieldValue('productIds', values)}
multiple={true}
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(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'), path.resolve(workspaceRoot, 'node_modules'),
path.resolve(projectRoot, '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`

View file

@ -1 +0,0 @@
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "group_ids" jsonb;

File diff suppressed because it is too large Load diff

View file

@ -491,13 +491,6 @@
"when": 1769718702463, "when": 1769718702463,
"tag": "0069_violet_smiling_tiger", "tag": "0069_violet_smiling_tiger",
"breakpoints": true "breakpoints": true
},
{
"idx": 70,
"version": "7",
"when": 1769958949864,
"tag": "0070_known_ares",
"breakpoints": true
} }
] ]
} }

View file

@ -192,7 +192,6 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
isActive: boolean('is_active').notNull().default(true), isActive: boolean('is_active').notNull().default(true),
isFlash: boolean('is_flash').notNull().default(false), isFlash: boolean('is_flash').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}), deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []),
}); });
export const vendorSnippets = mf.table('vendor_snippets', { export const vendorSnippets = mf.table('vendor_snippets', {

View file

@ -1,45 +0,0 @@
import { db } from '../db/db_index';
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema';
import { eq, inArray } from 'drizzle-orm';
/**
* Delete orders and all their related records
* @param orderIds Array of order IDs to delete
* @returns Promise<void>
* @throws Error if deletion fails
*/
export const deleteOrders = async (orderIds: number[]): Promise<void> => {
if (orderIds.length === 0) {
return;
}
try {
// Delete child records first (in correct order to avoid FK constraint errors)
// 1. Delete coupon usage records
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds));
// 2. Delete complaints related to these orders
await db.delete(complaints).where(inArray(complaints.orderId, orderIds));
// 3. Delete refunds
await db.delete(refunds).where(inArray(refunds.orderId, orderIds));
// 4. Delete payments
await db.delete(payments).where(inArray(payments.orderId, orderIds));
// 5. Delete order status records
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds));
// 6. Delete order items
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds));
// 7. Finally delete the orders themselves
await db.delete(orders).where(inArray(orders.id, orderIds));
console.log(`Successfully deleted ${orderIds.length} orders and all related records`);
} catch (error) {
console.error(`Failed to delete orders ${orderIds.join(', ')}:`, error);
throw error;
}
};

View file

@ -1,7 +1,6 @@
import './notif-job'; import './notif-job';
import { initializeAllStores } from '../stores/store-initializer'; import { initializeAllStores } from '../stores/store-initializer';
import { startOrderHandler, publishOrder } from './post-order-handler'; import { startOrderHandler, publishOrder } from './post-order-handler';
import { deleteOrders } from './delete-orders';
/** /**
* Initialize all application services * Initialize all application services
@ -24,7 +23,21 @@ export const initFunc = async (): Promise<void> => {
// Start post order handler (Redis Pub/Sub subscriber) // Start post order handler (Redis Pub/Sub subscriber)
await startOrderHandler(); await startOrderHandler();
// Wait a moment for subscription to be ready, then publish demo order
// setTimeout(async () => {
// console.log('Publishing demo order for testing...');
// await publishOrder({
// orders: [{
// deliveryTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // Tomorrow
// orderPlaceTime: new Date().toISOString(),
// totalAmount: 550,
// orderItems: [
// { productName: "Chicken Breast", quantity: 2 },
// { productName: "Mutton Curry Cut", quantity: 1 },
// ],
// }],
// });
// }, 20000);
console.log('Application initialization completed successfully'); console.log('Application initialization completed successfully');
} catch (error) { } catch (error) {

View file

@ -2,7 +2,7 @@ import { router, protectedProcedure } from "../trpc-index";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { db } from "../../db/db_index"; import { db } from "../../db/db_index";
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "../../db/schema"; import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from "../../db/schema";
import { eq, inArray, and, desc } from "drizzle-orm"; import { eq, inArray, and, desc } from "drizzle-orm";
import { ApiError } from "../../lib/api-error"; import { ApiError } from "../../lib/api-error";
import { appUrl } from "../../lib/env-exporter"; import { appUrl } from "../../lib/env-exporter";
@ -26,7 +26,6 @@ const createSlotSchema = z.object({
productIds: z.array(z.number().int().positive()).min(1), productIds: z.array(z.number().int().positive()).min(1),
validTill: z.string().optional(), validTill: z.string().optional(),
})).optional(), })).optional(),
groupIds: z.array(z.number()).optional(),
}); });
const getSlotByIdSchema = z.object({ const getSlotByIdSchema = z.object({
@ -44,7 +43,6 @@ const updateSlotSchema = z.object({
productIds: z.array(z.number().int().positive()).min(1), productIds: z.array(z.number().int().positive()).min(1),
validTill: z.string().optional(), validTill: z.string().optional(),
})).optional(), })).optional(),
groupIds: z.array(z.number()).optional(),
}); });
const deleteSlotSchema = z.object({ const deleteSlotSchema = z.object({
@ -231,7 +229,7 @@ export const slotsRouter = router({
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets } = input;
// Validate required fields // Validate required fields
if (!deliveryTime || !freezeTime) { if (!deliveryTime || !freezeTime) {
@ -246,7 +244,6 @@ export const slotsRouter = router({
deliveryTime: new Date(deliveryTime), deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [],
}) })
.returning(); .returning();
@ -351,7 +348,6 @@ export const slotsRouter = router({
slot: { slot: {
...slot, ...slot,
deliverySequence: slot.deliverySequence as number[], deliverySequence: slot.deliverySequence as number[],
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product), products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({ vendorSnippets: slot.vendorSnippets?.map(snippet => ({
...snippet, ...snippet,
@ -368,22 +364,12 @@ export const slotsRouter = router({
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
try{ try{
const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input; const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets } = input;
if (!deliveryTime || !freezeTime) { if (!deliveryTime || !freezeTime) {
throw new ApiError("Delivery time and orders close time are required", 400); throw new ApiError("Delivery time and orders close time are required", 400);
} }
// Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) {
const existingGroups = await db.query.productGroupInfo.findMany({
where: inArray(productGroupInfo.id, groupIds),
columns: { id: true },
});
validGroupIds = existingGroups.map(g => g.id);
}
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const [updatedSlot] = await tx const [updatedSlot] = await tx
.update(deliverySlotInfo) .update(deliverySlotInfo)
@ -391,7 +377,6 @@ export const slotsRouter = router({
deliveryTime: new Date(deliveryTime), deliveryTime: new Date(deliveryTime),
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [],
}) })
.where(eq(deliverySlotInfo.id, id)) .where(eq(deliverySlotInfo.id, id))
.returning(); .returning();

File diff suppressed because it is too large Load diff

View file

@ -8,69 +8,12 @@ export default function Inauguration() {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const triggerConfetti = useCallback(() => { const triggerConfetti = useCallback(() => {
const duration = 8 * 1000; const duration = 5 * 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();
@ -78,41 +21,27 @@ export default function Inauguration() {
return clearInterval(interval); return clearInterval(interval);
} }
// Random mini-crackers const particleCount = 50 * (timeLeft / duration);
const miniCrackerX = randomInRange(0.1, 0.9);
const miniColors = crackerColors[Math.floor(Math.random() * crackerColors.length)]; const colors = ['#2E90FA', '#53B1FD', '#84CAFF', '#1570EF', '#175CD3'];
confetti({ confetti({
particleCount: 40, ...defaults,
spread: 360, particleCount,
origin: { x: miniCrackerX, y: 0.85 }, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
colors: miniColors, colors: colors,
shapes: ['circle', 'square', 'star'], shapes: ['circle', 'square'],
scalar: randomInRange(0.8, 1.4), scalar: randomInRange(0.8, 1.5)
startVelocity: randomInRange(30, 50),
gravity: 1.1,
ticks: 100,
drift: randomInRange(-0.8, 0.8),
flat: false
}); });
confetti({
// Occasional sparkle burst ...defaults,
if (Math.random() > 0.7) { particleCount,
confetti({ origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
particleCount: 30, colors: colors,
spread: 360, shapes: ['circle', 'square'],
origin: { x: randomInRange(0.2, 0.8), y: 0.9 }, scalar: randomInRange(0.8, 1.5)
colors: ['#FFFFFF', '#FFD700', '#C0C0C0'], });
shapes: ['star', 'circle'], }, 250);
scalar: randomInRange(0.5, 1.0),
startVelocity: randomInRange(25, 40),
gravity: 0.8,
ticks: 80,
drift: 0,
flat: false
});
}
}, 400);
}, []); }, []);
const handleKickstart = () => { const handleKickstart = () => {
@ -126,100 +55,41 @@ export default function Inauguration() {
return ( return (
<div className="relative w-full h-screen overflow-hidden"> <div className="relative w-full h-screen overflow-hidden">
{/* Elegant Garden Background */} {/* Animated Gradient Background */}
<div <div
className="absolute inset-0" className="absolute inset-0 animate-gradient-xy"
style={{ style={{
background: ` background: 'linear-gradient(135deg, #2E90FA 0%, #1570EF 25%, #53B1FD 50%, #84CAFF 75%, #2E90FA 100%)',
linear-gradient(180deg, backgroundSize: '400% 400%',
#e8f5e9 0%, animation: 'gradientShift 15s ease infinite'
#c8e6c9 20%,
#a5d6a7 40%,
#81c784 60%,
#66bb6a 80%,
#4caf50 100%
)
`,
}} }}
/> />
{/* Soft Nature Mesh Overlay */} {/* Animated Mesh Gradient Overlay */}
<div className="absolute inset-0 opacity-40"> <div className="absolute inset-0 opacity-60">
<div <div
className="absolute inset-0" className="absolute inset-0 animate-pulse"
style={{ style={{
background: ` 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%)',
radial-gradient(circle at 30% 70%, rgba(255,255,255,0.4) 0%, transparent 40%), filter: 'blur(60px)'
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 Petals */} {/* Floating Bubbles */}
<div className="absolute inset-0 pointer-events-none overflow-hidden"> <div className="absolute inset-0 pointer-events-none overflow-hidden">
{[...Array(15)].map((_, i) => ( {[...Array(20)].map((_, i) => (
<div
key={`petal-${i}`}
className="absolute"
style={{
width: `${Math.random() * 20 + 15}px`,
height: `${Math.random() * 20 + 15}px`,
left: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`,
background: i % 3 === 0
? 'radial-gradient(ellipse at center, rgba(255,182,193,0.7) 0%, rgba(255,192,203,0.3) 70%, transparent 100%)'
: 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 <div
key={i} key={i}
className="absolute" className="absolute rounded-full"
style={{ style={{
width: '2px', width: `${Math.random() * 100 + 50}px`,
height: '100%', height: `${Math.random() * 100 + 50}px`,
left: `${20 + i * 15}%`, left: `${Math.random() * 100}%`,
background: 'linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.2) 30%, rgba(255,255,255,0.1) 70%, transparent 100%)', top: `${Math.random() * 100}%`,
transform: `rotate(${-15 + i * 5}deg)`, background: `radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%)`,
transformOrigin: 'top center', animation: `floatBubble ${Math.random() * 10 + 10}s ease-in-out infinite`,
animation: `lightRay ${8 + i * 2}s ease-in-out infinite`, animationDelay: `${Math.random() * 5}s`,
animationDelay: `${i * 1.5}s`,
}} }}
/> />
))} ))}
@ -240,7 +110,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-[#66bb6a] to-[#a5d6a7] rounded-3xl blur-2xl opacity-40 animate-pulse" /> <div className="absolute -inset-4 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-3xl blur-2xl opacity-40 animate-pulse" />
{/* Logo Text */} {/* Logo Text */}
<div className="relative"> <div className="relative">
@ -248,9 +118,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%, #E8F5E9 25%, #FFFFFF 50%, #E8F5E9 75%, #FFFFFF 100%)', backgroundImage: 'linear-gradient(90deg, #FFFFFF 0%, #E0F2FE 25%, #FFFFFF 50%, #E0F2FE 75%, #FFFFFF 100%)',
backgroundSize: '200% auto', backgroundSize: '200% auto',
textShadow: '0 0 40px rgba(76,175,80,0.4)', textShadow: '0 0 40px rgba(46,144,250,0.5)',
animation: 'shimmer 3s linear infinite' animation: 'shimmer 3s linear infinite'
}} }}
> >
@ -286,11 +156,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-[#4caf50] to-[#81c784] p-[2px]"> <div className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] p-[2px]">
<div className="relative rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] px-12 py-6"> <div className="relative rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] px-12 py-6">
{/* Animated Background on Hover */} {/* Animated Background on Hover */}
<div <div
className="absolute inset-0 bg-gradient-to-r from-[#43a047] via-[#66bb6a] to-[#43a047] opacity-0 group-hover:opacity-100 transition-opacity duration-500" className="absolute inset-0 bg-gradient-to-r from-[#2E90FA] via-[#53B1FD] to-[#2E90FA] opacity-0 group-hover:opacity-100 transition-opacity duration-500"
style={{ backgroundSize: '200% auto' }} style={{ backgroundSize: '200% auto' }}
/> />
@ -324,7 +194,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-[#c8e6c9] rounded-full animate-float-delayed-fast" /> <div className="absolute -bottom-2 -left-2 w-3 h-3 bg-[#84CAFF] rounded-full animate-float-delayed-fast" />
</button> </button>
{/* Subtext under button */} {/* Subtext under button */}
@ -336,12 +206,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 - elegant garden green */} {/* Background glow */}
<div className="absolute -inset-8 bg-gradient-to-r from-[#66bb6a] to-[#81c784] rounded-full blur-3xl opacity-40" /> <div className="absolute -inset-8 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] 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-[#4caf50] to-[#81c784] flex items-center justify-center shadow-lg"> <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">
<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>
@ -421,67 +291,22 @@ export default function Inauguration() {
100% { background-position: 200% center; } 100% { background-position: 200% center; }
} }
@keyframes gardenPulse { @keyframes floatBubble {
0%, 100% { 0%, 100% {
opacity: 0.4; transform: translateY(0) translateX(0) scale(1);
transform: scale(1); opacity: 0.3;
}
50% {
opacity: 0.6;
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% { 25% {
transform: translateY(-50px) translateX(20px) rotate(60deg); transform: translateY(-30px) translateX(10px) scale(1.1);
opacity: 0.7; opacity: 0.5;
} }
50% { 50% {
transform: translateY(-30px) translateX(-25px) rotate(120deg); transform: translateY(-20px) translateX(-10px) scale(0.9);
opacity: 0.4; opacity: 0.3;
} }
75% { 75% {
transform: translateY(-70px) translateX(15px) rotate(180deg); transform: translateY(-40px) translateX(5px) scale(1.05);
opacity: 0.6; opacity: 0.4;
}
}
@keyframes lightRay {
0%, 100% {
opacity: 0.1;
transform: rotate(-15deg) scaleY(1);
}
50% {
opacity: 0.3;
transform: rotate(-15deg) scaleY(1.1);
} }
} }

File diff suppressed because one or more lines are too long