Compare commits
No commits in common. "86de06078ffc5ddf00b0c179b7cf39a2cdec69d4" and "38060e78ff8f97742243b6235b22e98f7b05d783" have entirely different histories.
86de06078f
...
38060e78ff
21 changed files with 672 additions and 11329 deletions
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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`}>
|
||||||
|
|
|
||||||
|
|
@ -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`} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "group_ids" jsonb;
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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', {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
6386
session-ses_3ec7.md
6386
session-ses_3ec7.md
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue