enh
This commit is contained in:
parent
bac9b04a28
commit
edbc506062
12 changed files with 7540 additions and 628 deletions
|
|
@ -13,6 +13,26 @@
|
||||||
"bundleIdentifier": "in.freshyo.adminui",
|
"bundleIdentifier": "in.freshyo.adminui",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"LSApplicationQueriesSchemes": [
|
"LSApplicationQueriesSchemes": [
|
||||||
|
"ppemerchantsdkv1",
|
||||||
|
"ppemerchantsdkv2",
|
||||||
|
"ppemerchantsdkv3",
|
||||||
|
"paytmmp",
|
||||||
|
"gpay",
|
||||||
|
"ppemerchantsdkv1",
|
||||||
|
"ppemerchantsdkv2",
|
||||||
|
"ppemerchantsdkv3",
|
||||||
|
"paytmmp",
|
||||||
|
"gpay",
|
||||||
|
"ppemerchantsdkv1",
|
||||||
|
"ppemerchantsdkv2",
|
||||||
|
"ppemerchantsdkv3",
|
||||||
|
"paytmmp",
|
||||||
|
"gpay",
|
||||||
|
"ppemerchantsdkv1",
|
||||||
|
"ppemerchantsdkv2",
|
||||||
|
"ppemerchantsdkv3",
|
||||||
|
"paytmmp",
|
||||||
|
"gpay",
|
||||||
"ppemerchantsdkv1",
|
"ppemerchantsdkv1",
|
||||||
"ppemerchantsdkv2",
|
"ppemerchantsdkv2",
|
||||||
"ppemerchantsdkv3",
|
"ppemerchantsdkv3",
|
||||||
|
|
@ -33,7 +53,8 @@
|
||||||
"ppemerchantsdkv3",
|
"ppemerchantsdkv3",
|
||||||
"paytmmp",
|
"paytmmp",
|
||||||
"gpay"
|
"gpay"
|
||||||
]
|
],
|
||||||
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
BottomDialog,
|
BottomDialog,
|
||||||
BottomDropdown,
|
BottomDropdown,
|
||||||
} from "common-ui";
|
} from "common-ui";
|
||||||
|
import ProductsSelector from "../../../components/ProductsSelector";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { trpc } from "../../../src/trpc-client";
|
import { trpc } from "../../../src/trpc-client";
|
||||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
|
|
@ -220,10 +221,7 @@ export default function CustomizePopularItems() {
|
||||||
product => !popularProducts.find(p => p.id === product.id)
|
product => !popularProducts.find(p => p.id === product.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const productOptions = availableProducts.map(product => ({
|
|
||||||
label: `${product.name} - ₹${product.price}`,
|
|
||||||
value: product.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Show loading state while data is being fetched
|
// Show loading state while data is being fetched
|
||||||
if (isLoadingConstants || isLoadingProducts) {
|
if (isLoadingConstants || isLoadingProducts) {
|
||||||
|
|
@ -399,12 +397,13 @@ export default function CustomizePopularItems() {
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<BottomDropdown
|
<ProductsSelector
|
||||||
|
value={selectedProductId || 0}
|
||||||
|
onChange={(val) => setSelectedProductId(val as number)}
|
||||||
|
multiple={false}
|
||||||
label="Select Product"
|
label="Select Product"
|
||||||
options={productOptions}
|
|
||||||
value={selectedProductId || ""}
|
|
||||||
onValueChange={(val) => setSelectedProductId(val as number)}
|
|
||||||
placeholder="Choose a product..."
|
placeholder="Choose a product..."
|
||||||
|
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={tw`flex-row gap-3 mt-6`}>
|
<View style={tw`flex-row gap-3 mt-6`}>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Formik, FormikHelpers } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui';
|
import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui';
|
||||||
import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||||
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
@ -57,11 +58,7 @@ export default function BannerForm({
|
||||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||||
const products = productsData?.products || [];
|
const products = productsData?.products || [];
|
||||||
|
|
||||||
// Create product options for dropdown
|
|
||||||
const productOptions: DropdownOption[] = products.map(product => ({
|
|
||||||
label: `${product.name} (${product.price})`,
|
|
||||||
value: product.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
const handleImagePick = usePickImage({
|
const handleImagePick = usePickImage({
|
||||||
|
|
@ -200,16 +197,16 @@ export default function BannerForm({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={{ marginBottom: 16 }}>
|
<View style={{ marginBottom: 16 }}>
|
||||||
<BottomDropdown
|
<ProductsSelector
|
||||||
label="Select Products"
|
|
||||||
options={productOptions}
|
|
||||||
value={values.productIds}
|
value={values.productIds}
|
||||||
onValueChange={(value) => {
|
onChange={(value) => {
|
||||||
const selectedValues = Array.isArray(value) ? value : [value];
|
const selectedValues = Array.isArray(value) ? value : [value];
|
||||||
setFieldValue('productIds', selectedValues.map(v => Number(v)));
|
setFieldValue('productIds', selectedValues.map(v => Number(v)));
|
||||||
}}
|
}}
|
||||||
placeholder="Select products for banner (optional)"
|
|
||||||
multiple={true}
|
multiple={true}
|
||||||
|
label="Select Products"
|
||||||
|
placeholder="Select products for banner (optional)"
|
||||||
|
labelFormat={(product) => `${product.name} (${product.price})`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } from 'common-ui';
|
import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } from 'common-ui';
|
||||||
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
|
||||||
|
|
@ -35,10 +36,7 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
|
||||||
|
|
||||||
const products = productsData?.products || [];
|
const products = productsData?.products || [];
|
||||||
|
|
||||||
const productOptions = products.map(product => ({
|
|
||||||
label: `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`,
|
|
||||||
value: product.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
|
@ -115,14 +113,13 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Products Selection */}
|
{/* Products Selection */}
|
||||||
<BottomDropdown
|
<ProductsSelector
|
||||||
label="Products"
|
|
||||||
value={formik.values.product_ids}
|
value={formik.values.product_ids}
|
||||||
options={productOptions}
|
onChange={(value) => formik.setFieldValue('product_ids', value as number[])}
|
||||||
onValueChange={(value) => formik.setFieldValue('product_ids', value as number[])}
|
|
||||||
multiple={true}
|
multiple={true}
|
||||||
|
label="Products"
|
||||||
placeholder="Select products"
|
placeholder="Select products"
|
||||||
style={{ marginBottom: 16 }}
|
labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|
|
||||||
188
apps/admin-ui/components/ProductsSelector.tsx
Normal file
188
apps/admin-ui/components/ProductsSelector.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||||
|
import { trpc } from '../src/trpc-client';
|
||||||
|
import { tw } from 'common-ui';
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
unit?: string;
|
||||||
|
shortDescription?: string | null;
|
||||||
|
isOutOfStock?: boolean;
|
||||||
|
isSuspended?: boolean;
|
||||||
|
storeId?: number | null;
|
||||||
|
unitNotation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Group {
|
||||||
|
id: number;
|
||||||
|
groupName: string;
|
||||||
|
products: Product[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductsSelectorProps {
|
||||||
|
value: number | number[];
|
||||||
|
onChange: (value: number | number[]) => void;
|
||||||
|
multiple?: boolean;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
error?: boolean;
|
||||||
|
isDisabled?: (product: Product) => boolean;
|
||||||
|
labelFormat?: (product: Product) => string;
|
||||||
|
groups?: Group[];
|
||||||
|
selectedGroupIds?: number[];
|
||||||
|
onGroupChange?: (groupIds: number[]) => void;
|
||||||
|
showGroups?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductsSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
multiple = true,
|
||||||
|
label = 'Select Products',
|
||||||
|
placeholder = 'Select products',
|
||||||
|
disabled = false,
|
||||||
|
error = false,
|
||||||
|
isDisabled,
|
||||||
|
labelFormat,
|
||||||
|
groups = [],
|
||||||
|
showGroups = true,
|
||||||
|
selectedGroupIds = [],
|
||||||
|
onGroupChange,
|
||||||
|
}: ProductsSelectorProps) {
|
||||||
|
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||||
|
const products = productsData?.products || [];
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
// Format product label: name (unit) (₹price)
|
||||||
|
const formatProductLabel = (product: Product): string => {
|
||||||
|
if (labelFormat) {
|
||||||
|
return labelFormat(product);
|
||||||
|
}
|
||||||
|
const unit = product.unit ? ` (${product.unit})` : '';
|
||||||
|
const price = ` (₹${product.price})`;
|
||||||
|
return `${product.name}${unit}${price}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle group selection changes
|
||||||
|
const handleGroupChange = (newGroupIds: number[]) => {
|
||||||
|
if (!onGroupChange) return;
|
||||||
|
|
||||||
|
const previousGroupIds = selectedGroupIds;
|
||||||
|
|
||||||
|
// Find which groups were added and which were removed
|
||||||
|
const addedGroups = newGroupIds.filter(id => !previousGroupIds.includes(id));
|
||||||
|
const removedGroups = previousGroupIds.filter(id => !newGroupIds.includes(id));
|
||||||
|
|
||||||
|
// Get current selected products
|
||||||
|
let currentProducts = Array.isArray(value) ? [...value] : value ? [value] : [];
|
||||||
|
|
||||||
|
// Add products from newly selected groups
|
||||||
|
const addedProducts = addedGroups.flatMap(groupId => {
|
||||||
|
const group = groups.find(g => g.id === groupId);
|
||||||
|
return group?.products.map(p => p.id) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove products from deselected groups
|
||||||
|
const removedProducts = removedGroups.flatMap(groupId => {
|
||||||
|
const group = groups.find(g => g.id === groupId);
|
||||||
|
return group?.products.map(p => p.id) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update product list: add new ones, remove deselected group ones
|
||||||
|
currentProducts = [...new Set([...currentProducts, ...addedProducts])];
|
||||||
|
currentProducts = currentProducts.filter(id => !removedProducts.includes(id));
|
||||||
|
|
||||||
|
onGroupChange(newGroupIds);
|
||||||
|
if (multiple) {
|
||||||
|
onChange(currentProducts.length > 0 ? currentProducts : []);
|
||||||
|
} else {
|
||||||
|
onChange(currentProducts.length > 0 ? currentProducts[0] : 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter products based on search query
|
||||||
|
const filteredProducts = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return products;
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return products.filter(product =>
|
||||||
|
product.name.toLowerCase().includes(query) ||
|
||||||
|
(product.shortDescription && product.shortDescription.toLowerCase().includes(query)) ||
|
||||||
|
(product.unit && product.unit.toLowerCase().includes(query))
|
||||||
|
);
|
||||||
|
}, [products, searchQuery]);
|
||||||
|
|
||||||
|
// Build dropdown options
|
||||||
|
const productOptions: DropdownOption[] = useMemo(() => {
|
||||||
|
return filteredProducts.map((product) => {
|
||||||
|
const isFromGroup = selectedGroupIds.length > 0 && groups.some(group =>
|
||||||
|
selectedGroupIds.includes(group.id) && group.products.some(p => p.id === product.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const isProductDisabled = isDisabled ? isDisabled(product as Product) : false;
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `${formatProductLabel(product as Product)}${isFromGroup ? ' (from group)' : ''}`,
|
||||||
|
value: product.id.toString(),
|
||||||
|
disabled: isProductDisabled,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [filteredProducts, selectedGroupIds, groups, isDisabled, labelFormat]);
|
||||||
|
|
||||||
|
// Build group options if groups are provided
|
||||||
|
const groupOptions: DropdownOption[] = useMemo(() => {
|
||||||
|
return groups.map(group => ({
|
||||||
|
label: group.groupName,
|
||||||
|
value: group.id.toString(),
|
||||||
|
}));
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tw`w-full`}>
|
||||||
|
{/* Groups selector (if groups are provided and showGroups is true) */}
|
||||||
|
{showGroups && groups.length > 0 && (
|
||||||
|
<View style={tw`mb-4`}>
|
||||||
|
<BottomDropdown
|
||||||
|
label="Select Product Groups"
|
||||||
|
options={groupOptions}
|
||||||
|
value={selectedGroupIds.map(id => id.toString())}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
|
||||||
|
const newGroupIds = selectedValues.map(v => parseInt(v as string));
|
||||||
|
handleGroupChange(newGroupIds);
|
||||||
|
}}
|
||||||
|
placeholder="Select product groups (optional)"
|
||||||
|
multiple={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Products selector */}
|
||||||
|
<BottomDropdown
|
||||||
|
label={label}
|
||||||
|
options={productOptions}
|
||||||
|
value={multiple
|
||||||
|
? (Array.isArray(value) ? value.map(id => id.toString()) : [])
|
||||||
|
: (value ? value.toString() : '')
|
||||||
|
}
|
||||||
|
onValueChange={(selectedValue) => {
|
||||||
|
if (multiple) {
|
||||||
|
const selectedValues = Array.isArray(selectedValue) ? selectedValue : typeof selectedValue === 'string' ? [selectedValue] : [];
|
||||||
|
onChange(selectedValues.map(v => Number(v)));
|
||||||
|
} else {
|
||||||
|
onChange(Number(selectedValue));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={placeholder}
|
||||||
|
multiple={multiple}
|
||||||
|
disabled={disabled}
|
||||||
|
error={error}
|
||||||
|
onSearch={setSearchQuery}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, Alert } from 'react-native';
|
import { View, Text, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Formik, FieldArray } from 'formik';
|
import { Formik, FieldArray } from 'formik';
|
||||||
import DateTimePickerMod from 'common-ui/src/components/date-time-picker';
|
import DateTimePickerMod from 'common-ui/src/components/date-time-picker';
|
||||||
import { tw, MyTextInput } from 'common-ui';
|
import { tw, MyTextInput } from 'common-ui';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
import ProductsSelector from '../components/ProductsSelector';
|
||||||
|
|
||||||
interface VendorSnippet {
|
interface VendorSnippet {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -44,10 +44,8 @@ export default function SlotForm({
|
||||||
const isEditMode = !!slotId;
|
const isEditMode = !!slotId;
|
||||||
const isPending = isCreating || isUpdating;
|
const isPending = isCreating || isUpdating;
|
||||||
|
|
||||||
// Fetch products and groups
|
// Fetch groups
|
||||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
|
||||||
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
|
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
|
||||||
const products = productsData?.products || [];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -120,25 +118,13 @@ export default function SlotForm({
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values, setFieldValue }) => {
|
{({ handleSubmit, values, setFieldValue }) => {
|
||||||
// Collect all product IDs from selected groups
|
// Map groups data to match ProductsSelector types (convert price from string to number)
|
||||||
const allGroupProductIds = (values?.selectedGroupIds || []).flatMap(groupId => {
|
const mappedGroups = (groupsData?.groups || []).map(group => ({
|
||||||
const group = (groupsData?.groups || []).find(g => g.id === groupId);
|
...group,
|
||||||
return group?.products.map(p => p.id) || [];
|
products: group.products.map(product => ({
|
||||||
});
|
...product,
|
||||||
// Remove duplicates
|
price: parseFloat(product.price as unknown as string) || 0,
|
||||||
const groupProductIds = [...new Set(allGroupProductIds)];
|
})),
|
||||||
|
|
||||||
const productOptions: DropdownOption[] = products.map(product => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
label: `${product.name}${groupProductIds.includes(product.id) ? ' (from group)' : ''}`,
|
|
||||||
value: product.id.toString(),
|
|
||||||
disabled: groupProductIds.includes(product.id),
|
|
||||||
}});
|
|
||||||
|
|
||||||
const groupOptions: DropdownOption[] = (groupsData?.groups || []).map(group => ({
|
|
||||||
label: group.groupName,
|
|
||||||
value: group.id.toString(),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -157,45 +143,17 @@ 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`}>
|
||||||
<Text style={tw`text-base mb-2`}>Select Product Groups (Optional)</Text>
|
<ProductsSelector
|
||||||
<BottomDropdown
|
value={values.selectedProductIds}
|
||||||
label="Select Product Groups"
|
onChange={(newProductIds) => setFieldValue('selectedProductIds', newProductIds)}
|
||||||
options={groupOptions}
|
groups={mappedGroups}
|
||||||
value={values.selectedGroupIds.map(id => id.toString())}
|
selectedGroupIds={values.selectedGroupIds}
|
||||||
onValueChange={(value) => {
|
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
|
||||||
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
|
label="Select Products"
|
||||||
const groupIds = selectedValues.map(v => parseInt(v as string));
|
placeholder="Select products for this slot"
|
||||||
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">
|
||||||
|
|
@ -214,43 +172,20 @@ export default function SlotForm({
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={tw`mb-4`}>
|
<View style={tw`mb-4`}>
|
||||||
<BottomDropdown
|
<ProductsSelector
|
||||||
label="Select Groups"
|
value={snippet.productIds || []}
|
||||||
options={groupOptions.filter(option =>
|
onChange={(newProductIds) => setFieldValue(`vendorSnippetList.${index}.productIds`, newProductIds)}
|
||||||
values.selectedGroupIds.includes(Number(option.value))
|
groups={mappedGroups.filter(group =>
|
||||||
)}
|
values.selectedGroupIds.includes(group.id)
|
||||||
value={snippet.groupIds?.map(id => id.toString()) || []}
|
).map(group => ({
|
||||||
onValueChange={(value) => {
|
...group,
|
||||||
const selectedValues = Array.isArray(value) ? value : [value];
|
products: group.products.filter(p => values.selectedProductIds.includes(p.id))
|
||||||
const selectedGroupIds = selectedValues.map(v => parseInt(v as string));
|
}))}
|
||||||
setFieldValue(`vendorSnippetList.${index}.groupIds`, selectedGroupIds);
|
selectedGroupIds={snippet.groupIds || []}
|
||||||
|
onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)}
|
||||||
// Auto-populate products from selected groups
|
|
||||||
const allSnippetProducts = selectedGroupIds.flatMap(groupId => {
|
|
||||||
const group = (groupsData?.groups || []).find(g => g.id === groupId);
|
|
||||||
return group?.products.map(p => p.id) || [];
|
|
||||||
});
|
|
||||||
// Remove duplicates
|
|
||||||
const uniqueSnippetProducts = [...new Set(allSnippetProducts)];
|
|
||||||
setFieldValue(`vendorSnippetList.${index}.productIds`, uniqueSnippetProducts);
|
|
||||||
}}
|
|
||||||
placeholder="Select groups for snippet"
|
|
||||||
multiple={true}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={tw`mb-4`}>
|
|
||||||
<BottomDropdown
|
|
||||||
label="Select Products"
|
label="Select Products"
|
||||||
options={productOptions.filter(option =>
|
|
||||||
values.selectedProductIds.includes(Number(option.value))
|
|
||||||
)}
|
|
||||||
value={snippet.productIds?.map(id => id.toString()) || []}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
const selectedValues = Array.isArray(value) ? value : [value];
|
|
||||||
setFieldValue(`vendorSnippetList.${index}.productIds`, selectedValues.map(v => parseInt(v as string)));
|
|
||||||
}}
|
|
||||||
placeholder="Select products for snippet"
|
placeholder="Select products for snippet"
|
||||||
multiple={true}
|
isDisabled={(product) => !values.selectedProductIds.includes(product.id)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui';
|
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui';
|
||||||
|
import ProductsSelector from './ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
|
|
||||||
|
|
@ -63,11 +64,7 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
value: staff.id,
|
value: staff.id,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const productOptions = productsData?.products.map(product => ({
|
|
||||||
label: `${product.name} - ₹${product.price}`,
|
|
||||||
value: product.id,
|
|
||||||
disabled: product.storeId !== null && product.storeId !== storeId, // Disable if belongs to another store
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
|
||||||
|
|
||||||
|
|
@ -185,14 +182,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
||||||
error={!!(touched.owner && errors.owner)}
|
error={!!(touched.owner && errors.owner)}
|
||||||
style={{ marginBottom: 16 }}
|
style={{ marginBottom: 16 }}
|
||||||
/>
|
/>
|
||||||
<BottomDropdown
|
<ProductsSelector
|
||||||
label="Products"
|
|
||||||
value={values.products || []}
|
value={values.products || []}
|
||||||
options={productOptions}
|
onChange={(value) => setFieldValue('products', value)}
|
||||||
onValueChange={(value) => setFieldValue('products', value)}
|
multiple={true}
|
||||||
|
label="Products"
|
||||||
placeholder="Select products"
|
placeholder="Select products"
|
||||||
multiple
|
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId}
|
||||||
style={{ marginBottom: 16 }}
|
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
||||||
/>
|
/>
|
||||||
<View style={tw`mb-6`}>
|
<View style={tw`mb-6`}>
|
||||||
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { MyText, tw, DatePicker, MyTextInput } from 'common-ui';
|
import { MyText, tw, DatePicker, MyTextInput } from 'common-ui';
|
||||||
import BottomDropdown from 'common-ui/src/components/bottom-dropdown';
|
import BottomDropdown from 'common-ui/src/components/bottom-dropdown';
|
||||||
|
import ProductsSelector from '../components/ProductsSelector';
|
||||||
import { trpc } from '../src/trpc-client';
|
import { trpc } from '../src/trpc-client';
|
||||||
import { VendorSnippetForm as VendorSnippetFormType, VendorSnippet } from '../types/vendor-snippets';
|
import { VendorSnippetForm as VendorSnippetFormType, VendorSnippet } from '../types/vendor-snippets';
|
||||||
|
|
||||||
|
|
@ -20,9 +21,8 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
||||||
showIsPermanentCheckbox = false,
|
showIsPermanentCheckbox = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// Fetch slots and products
|
// Fetch slots
|
||||||
const { data: slotsData } = trpc.user.slots.getSlots.useQuery();
|
const { data: slotsData } = trpc.user.slots.getSlots.useQuery();
|
||||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
|
||||||
|
|
||||||
const createSnippet = trpc.admin.vendorSnippets.create.useMutation();
|
const createSnippet = trpc.admin.vendorSnippets.create.useMutation();
|
||||||
const updateSnippet = trpc.admin.vendorSnippets.update.useMutation();
|
const updateSnippet = trpc.admin.vendorSnippets.update.useMutation();
|
||||||
|
|
@ -108,16 +108,6 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
||||||
value: slot.id.toString(),
|
value: slot.id.toString(),
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const productOptions = productsData?.products.map(product => ({
|
|
||||||
label: `${product.name} (${product.unit})`,
|
|
||||||
value: product.id.toString(),
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const selectedProductLabels = formik.values.productIds
|
|
||||||
.map(id => productOptions.find(opt => opt.value === id)?.label)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1 bg-white`}>
|
<View style={tw`flex-1 bg-white`}>
|
||||||
<View style={tw`px-6 py-4 border-b border-gray-200`}>
|
<View style={tw`px-6 py-4 border-b border-gray-200`}>
|
||||||
|
|
@ -190,20 +180,14 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
||||||
|
|
||||||
{/* Product Selection */}
|
{/* Product Selection */}
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-gray-700 font-medium mb-2`}>Products</MyText>
|
<ProductsSelector
|
||||||
<BottomDropdown
|
value={formik.values.productIds.map(id => parseInt(id))}
|
||||||
label="Select Products"
|
onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
|
||||||
value={formik.values.productIds}
|
|
||||||
options={productOptions}
|
|
||||||
onValueChange={(values) => formik.setFieldValue('productIds', values)}
|
|
||||||
multiple={true}
|
multiple={true}
|
||||||
|
label="Select Products"
|
||||||
placeholder="Select products"
|
placeholder="Select products"
|
||||||
|
labelFormat={(product) => `${product.name} (${product.unit})`}
|
||||||
/>
|
/>
|
||||||
{formik.values.productIds.length > 0 && (
|
|
||||||
<MyText style={tw`text-sm text-gray-600 mt-2`}>
|
|
||||||
Selected: {selectedProductLabels}
|
|
||||||
</MyText>
|
|
||||||
)}
|
|
||||||
{formik.errors.productIds && formik.touched.productIds && (
|
{formik.errors.productIds && formik.touched.productIds && (
|
||||||
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText>
|
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.productIds}</MyText>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ config.watchFolders = [workspaceRoot];
|
||||||
|
|
||||||
// 2. Let Metro know where to resolve modules from
|
// 2. Let Metro know where to resolve modules from
|
||||||
config.resolver.nodeModulesPaths = [
|
config.resolver.nodeModulesPaths = [
|
||||||
path.resolve(workspaceRoot, 'node_modules'),
|
|
||||||
path.resolve(projectRoot, 'node_modules'),
|
path.resolve(projectRoot, 'node_modules'),
|
||||||
|
path.resolve(workspaceRoot, 'node_modules'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 3. Force Metro to resolve (sub)dependencies only from the top-level `node_modules`
|
// 3. Force Metro to resolve (sub)dependencies only from the top-level `node_modules`
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,12 +8,69 @@ export default function Inauguration() {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const triggerConfetti = useCallback(() => {
|
const triggerConfetti = useCallback(() => {
|
||||||
const duration = 5 * 1000;
|
const duration = 8 * 1000;
|
||||||
const animationEnd = Date.now() + duration;
|
const animationEnd = Date.now() + duration;
|
||||||
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
|
||||||
|
|
||||||
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min;
|
const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min;
|
||||||
|
|
||||||
|
// Cracker explosion function - shoots upward then falls
|
||||||
|
const fireCracker = (x: number, delay: number, colors: string[]) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Initial burst - shoots upward like a firecracker
|
||||||
|
confetti({
|
||||||
|
particleCount: 120,
|
||||||
|
spread: 360,
|
||||||
|
origin: { x, y: 0.9 },
|
||||||
|
colors: colors,
|
||||||
|
shapes: ['circle', 'square', 'star'],
|
||||||
|
scalar: randomInRange(1.2, 2.0),
|
||||||
|
startVelocity: randomInRange(45, 65),
|
||||||
|
gravity: 1.2,
|
||||||
|
ticks: 150,
|
||||||
|
drift: randomInRange(-1, 1),
|
||||||
|
flat: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Secondary burst - shoots higher with different colors
|
||||||
|
setTimeout(() => {
|
||||||
|
confetti({
|
||||||
|
particleCount: 80,
|
||||||
|
spread: 360,
|
||||||
|
origin: { x, y: 0.8 },
|
||||||
|
colors: colors.slice().reverse(),
|
||||||
|
shapes: ['circle', 'square'],
|
||||||
|
scalar: randomInRange(1.0, 1.8),
|
||||||
|
startVelocity: randomInRange(35, 55),
|
||||||
|
gravity: 1.0,
|
||||||
|
ticks: 120,
|
||||||
|
drift: randomInRange(-0.5, 0.5),
|
||||||
|
flat: false
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fire multiple crackers in sequence
|
||||||
|
const crackerColors = [
|
||||||
|
['#FF0000', '#FFD700', '#FF6B6B', '#FFA500'], // Red/Gold
|
||||||
|
['#2E90FA', '#53B1FD', '#84CAFF', '#FFFFFF'], // Blue/White
|
||||||
|
['#00FF00', '#32CD32', '#90EE90', '#98FB98'], // Green
|
||||||
|
['#FF1493', '#FF69B4', '#FFB6C1', '#FFC0CB'], // Pink
|
||||||
|
['#FFD700', '#FFA500', '#FF8C00', '#FFFF00'], // Gold/Orange
|
||||||
|
['#9400D3', '#8A2BE2', '#9370DB', '#BA55D3'], // Purple
|
||||||
|
];
|
||||||
|
|
||||||
|
// Launch crackers from different positions
|
||||||
|
fireCracker(0.15, 0, crackerColors[0]);
|
||||||
|
fireCracker(0.35, 300, crackerColors[1]);
|
||||||
|
fireCracker(0.5, 600, crackerColors[2]);
|
||||||
|
fireCracker(0.65, 900, crackerColors[3]);
|
||||||
|
fireCracker(0.85, 1200, crackerColors[4]);
|
||||||
|
fireCracker(0.25, 1500, crackerColors[5]);
|
||||||
|
fireCracker(0.75, 1800, crackerColors[0]);
|
||||||
|
fireCracker(0.45, 2100, crackerColors[2]);
|
||||||
|
|
||||||
|
// Continuous secondary bursts for the duration
|
||||||
const interval: any = setInterval(function() {
|
const interval: any = setInterval(function() {
|
||||||
const timeLeft = animationEnd - Date.now();
|
const timeLeft = animationEnd - Date.now();
|
||||||
|
|
||||||
|
|
@ -21,27 +78,41 @@ export default function Inauguration() {
|
||||||
return clearInterval(interval);
|
return clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
const particleCount = 50 * (timeLeft / duration);
|
// Random mini-crackers
|
||||||
|
const miniCrackerX = randomInRange(0.1, 0.9);
|
||||||
const colors = ['#2E90FA', '#53B1FD', '#84CAFF', '#1570EF', '#175CD3'];
|
const miniColors = crackerColors[Math.floor(Math.random() * crackerColors.length)];
|
||||||
|
|
||||||
confetti({
|
confetti({
|
||||||
...defaults,
|
particleCount: 40,
|
||||||
particleCount,
|
spread: 360,
|
||||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
|
origin: { x: miniCrackerX, y: 0.85 },
|
||||||
colors: colors,
|
colors: miniColors,
|
||||||
shapes: ['circle', 'square'],
|
shapes: ['circle', 'square', 'star'],
|
||||||
scalar: randomInRange(0.8, 1.5)
|
scalar: randomInRange(0.8, 1.4),
|
||||||
|
startVelocity: randomInRange(30, 50),
|
||||||
|
gravity: 1.1,
|
||||||
|
ticks: 100,
|
||||||
|
drift: randomInRange(-0.8, 0.8),
|
||||||
|
flat: false
|
||||||
});
|
});
|
||||||
confetti({
|
|
||||||
...defaults,
|
// Occasional sparkle burst
|
||||||
particleCount,
|
if (Math.random() > 0.7) {
|
||||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
|
confetti({
|
||||||
colors: colors,
|
particleCount: 30,
|
||||||
shapes: ['circle', 'square'],
|
spread: 360,
|
||||||
scalar: randomInRange(0.8, 1.5)
|
origin: { x: randomInRange(0.2, 0.8), y: 0.9 },
|
||||||
});
|
colors: ['#FFFFFF', '#FFD700', '#C0C0C0'],
|
||||||
}, 250);
|
shapes: ['star', 'circle'],
|
||||||
|
scalar: randomInRange(0.5, 1.0),
|
||||||
|
startVelocity: randomInRange(25, 40),
|
||||||
|
gravity: 0.8,
|
||||||
|
ticks: 80,
|
||||||
|
drift: 0,
|
||||||
|
flat: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 400);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleKickstart = () => {
|
const handleKickstart = () => {
|
||||||
|
|
@ -55,41 +126,100 @@ export default function Inauguration() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-screen overflow-hidden">
|
<div className="relative w-full h-screen overflow-hidden">
|
||||||
{/* Animated Gradient Background */}
|
{/* Elegant Garden Background */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 animate-gradient-xy"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, #2E90FA 0%, #1570EF 25%, #53B1FD 50%, #84CAFF 75%, #2E90FA 100%)',
|
background: `
|
||||||
backgroundSize: '400% 400%',
|
linear-gradient(180deg,
|
||||||
animation: 'gradientShift 15s ease infinite'
|
#e8f5e9 0%,
|
||||||
|
#c8e6c9 20%,
|
||||||
|
#a5d6a7 40%,
|
||||||
|
#81c784 60%,
|
||||||
|
#66bb6a 80%,
|
||||||
|
#4caf50 100%
|
||||||
|
)
|
||||||
|
`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Animated Mesh Gradient Overlay */}
|
{/* Soft Nature Mesh Overlay */}
|
||||||
<div className="absolute inset-0 opacity-60">
|
<div className="absolute inset-0 opacity-40">
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 animate-pulse"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background: 'radial-gradient(circle at 20% 80%, #84CAFF 0%, transparent 50%), radial-gradient(circle at 80% 20%, #53B1FD 0%, transparent 50%), radial-gradient(circle at 40% 40%, #2E90FA 0%, transparent 40%)',
|
background: `
|
||||||
filter: 'blur(60px)'
|
radial-gradient(circle at 30% 70%, rgba(255,255,255,0.4) 0%, transparent 40%),
|
||||||
|
radial-gradient(circle at 70% 30%, rgba(200,230,201,0.5) 0%, transparent 35%),
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(255,248,225,0.3) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 20% 20%, rgba(232,245,233,0.4) 0%, transparent 30%),
|
||||||
|
radial-gradient(circle at 80% 80%, rgba(165,214,167,0.4) 0%, transparent 40%)
|
||||||
|
`,
|
||||||
|
filter: 'blur(80px)',
|
||||||
|
animation: 'gardenPulse 20s ease-in-out infinite'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Bubbles */}
|
{/* Floating Petals */}
|
||||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||||
{[...Array(20)].map((_, i) => (
|
{[...Array(15)].map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={`petal-${i}`}
|
||||||
className="absolute rounded-full"
|
className="absolute"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.random() * 100 + 50}px`,
|
width: `${Math.random() * 20 + 15}px`,
|
||||||
height: `${Math.random() * 100 + 50}px`,
|
height: `${Math.random() * 20 + 15}px`,
|
||||||
left: `${Math.random() * 100}%`,
|
left: `${Math.random() * 100}%`,
|
||||||
top: `${Math.random() * 100}%`,
|
top: `${Math.random() * 100}%`,
|
||||||
background: `radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%)`,
|
background: i % 3 === 0
|
||||||
animation: `floatBubble ${Math.random() * 10 + 10}s ease-in-out infinite`,
|
? 'radial-gradient(ellipse at center, rgba(255,182,193,0.7) 0%, rgba(255,192,203,0.3) 70%, transparent 100%)'
|
||||||
animationDelay: `${Math.random() * 5}s`,
|
: i % 3 === 1
|
||||||
|
? 'radial-gradient(ellipse at center, rgba(255,255,224,0.7) 0%, rgba(255,250,205,0.3) 70%, transparent 100%)'
|
||||||
|
: 'radial-gradient(ellipse at center, rgba(221,160,221,0.6) 0%, rgba(216,191,216,0.3) 70%, transparent 100%)',
|
||||||
|
borderRadius: '50% 0 50% 0',
|
||||||
|
transform: `rotate(${Math.random() * 360}deg)`,
|
||||||
|
animation: `floatPetal ${Math.random() * 12 + 15}s ease-in-out infinite`,
|
||||||
|
animationDelay: `${Math.random() * 8}s`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Floating Leaves */}
|
||||||
|
{[...Array(12)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={`leaf-${i}`}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
width: `${Math.random() * 25 + 20}px`,
|
||||||
|
height: `${Math.random() * 15 + 12}px`,
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
background: 'linear-gradient(45deg, rgba(76,175,80,0.4) 0%, rgba(129,199,132,0.3) 50%, rgba(165,214,167,0.2) 100%)',
|
||||||
|
borderRadius: '0 50% 0 50%',
|
||||||
|
transform: `rotate(${Math.random() * 360}deg)`,
|
||||||
|
animation: `floatLeaf ${Math.random() * 10 + 12}s ease-in-out infinite`,
|
||||||
|
animationDelay: `${Math.random() * 6}s`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Elegant Light Rays */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '100%',
|
||||||
|
left: `${20 + i * 15}%`,
|
||||||
|
background: 'linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.2) 30%, rgba(255,255,255,0.1) 70%, transparent 100%)',
|
||||||
|
transform: `rotate(${-15 + i * 5}deg)`,
|
||||||
|
transformOrigin: 'top center',
|
||||||
|
animation: `lightRay ${8 + i * 2}s ease-in-out infinite`,
|
||||||
|
animationDelay: `${i * 1.5}s`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -110,7 +240,7 @@ export default function Inauguration() {
|
||||||
{/* Logo Container with Glassmorphism */}
|
{/* Logo Container with Glassmorphism */}
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
{/* Glow Effect Behind Logo */}
|
{/* Glow Effect Behind Logo */}
|
||||||
<div className="absolute -inset-4 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-3xl blur-2xl opacity-40 animate-pulse" />
|
<div className="absolute -inset-4 bg-gradient-to-r from-[#66bb6a] to-[#a5d6a7] rounded-3xl blur-2xl opacity-40 animate-pulse" />
|
||||||
|
|
||||||
{/* Logo Text */}
|
{/* Logo Text */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -118,9 +248,9 @@ export default function Inauguration() {
|
||||||
<span
|
<span
|
||||||
className="bg-clip-text text-transparent animate-shimmer"
|
className="bg-clip-text text-transparent animate-shimmer"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: 'linear-gradient(90deg, #FFFFFF 0%, #E0F2FE 25%, #FFFFFF 50%, #E0F2FE 75%, #FFFFFF 100%)',
|
backgroundImage: 'linear-gradient(90deg, #FFFFFF 0%, #E8F5E9 25%, #FFFFFF 50%, #E8F5E9 75%, #FFFFFF 100%)',
|
||||||
backgroundSize: '200% auto',
|
backgroundSize: '200% auto',
|
||||||
textShadow: '0 0 40px rgba(46,144,250,0.5)',
|
textShadow: '0 0 40px rgba(76,175,80,0.4)',
|
||||||
animation: 'shimmer 3s linear infinite'
|
animation: 'shimmer 3s linear infinite'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -156,11 +286,11 @@ export default function Inauguration() {
|
||||||
<div className="absolute -inset-4 rounded-full border-2 border-white/20 group-hover:border-white/40 transition-all duration-500" />
|
<div className="absolute -inset-4 rounded-full border-2 border-white/20 group-hover:border-white/40 transition-all duration-500" />
|
||||||
|
|
||||||
{/* Button Container */}
|
{/* Button Container */}
|
||||||
<div className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] p-[2px]">
|
<div className="relative overflow-hidden rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] p-[2px]">
|
||||||
<div className="relative rounded-full bg-gradient-to-r from-[#1570EF] to-[#53B1FD] px-12 py-6">
|
<div className="relative rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] px-12 py-6">
|
||||||
{/* Animated Background on Hover */}
|
{/* Animated Background on Hover */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-gradient-to-r from-[#2E90FA] via-[#53B1FD] to-[#2E90FA] opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
className="absolute inset-0 bg-gradient-to-r from-[#43a047] via-[#66bb6a] to-[#43a047] opacity-0 group-hover:opacity-100 transition-opacity duration-500"
|
||||||
style={{ backgroundSize: '200% auto' }}
|
style={{ backgroundSize: '200% auto' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -194,7 +324,7 @@ export default function Inauguration() {
|
||||||
|
|
||||||
{/* Floating Particles Around Button */}
|
{/* Floating Particles Around Button */}
|
||||||
<div className="absolute -top-2 -right-2 w-4 h-4 bg-white/60 rounded-full animate-float-fast" />
|
<div className="absolute -top-2 -right-2 w-4 h-4 bg-white/60 rounded-full animate-float-fast" />
|
||||||
<div className="absolute -bottom-2 -left-2 w-3 h-3 bg-[#84CAFF] rounded-full animate-float-delayed-fast" />
|
<div className="absolute -bottom-2 -left-2 w-3 h-3 bg-[#c8e6c9] rounded-full animate-float-delayed-fast" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Subtext under button */}
|
{/* Subtext under button */}
|
||||||
|
|
@ -206,12 +336,12 @@ export default function Inauguration() {
|
||||||
/* Welcome Message */
|
/* Welcome Message */
|
||||||
<div className="text-center animate-fade-in-up">
|
<div className="text-center animate-fade-in-up">
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
{/* Background glow */}
|
{/* Background glow - elegant garden green */}
|
||||||
<div className="absolute -inset-8 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-full blur-3xl opacity-40" />
|
<div className="absolute -inset-8 bg-gradient-to-r from-[#66bb6a] to-[#81c784] rounded-full blur-3xl opacity-40" />
|
||||||
|
|
||||||
<div className="relative bg-white/10 backdrop-blur-lg rounded-3xl px-16 py-12 border border-white/20 shadow-2xl">
|
<div className="relative bg-white/10 backdrop-blur-lg rounded-3xl px-16 py-12 border border-white/20 shadow-2xl">
|
||||||
{/* Success Icon */}
|
{/* Success Icon */}
|
||||||
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-gradient-to-r from-[#2E90FA] to-[#53B1FD] flex items-center justify-center shadow-lg">
|
<div className="w-20 h-20 mx-auto mb-6 rounded-full bg-gradient-to-r from-[#4caf50] to-[#81c784] flex items-center justify-center shadow-lg">
|
||||||
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -291,22 +421,67 @@ export default function Inauguration() {
|
||||||
100% { background-position: 200% center; }
|
100% { background-position: 200% center; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes floatBubble {
|
@keyframes gardenPulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
transform: translateY(0) translateX(0) scale(1);
|
opacity: 0.4;
|
||||||
opacity: 0.3;
|
transform: scale(1);
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translateY(-30px) translateX(10px) scale(1.1);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-20px) translateX(-10px) scale(0.9);
|
opacity: 0.6;
|
||||||
opacity: 0.3;
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatPetal {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) translateX(0) rotate(0deg);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translateY(-40px) translateX(15px) rotate(45deg);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-25px) translateX(-20px) rotate(90deg);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-60px) translateX(10px) rotate(135deg);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateY(-35px) translateX(-15px) rotate(180deg);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatLeaf {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0) translateX(0) rotate(0deg);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: translateY(-50px) translateX(20px) rotate(60deg);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-30px) translateX(-25px) rotate(120deg);
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
transform: translateY(-40px) translateX(5px) scale(1.05);
|
transform: translateY(-70px) translateX(15px) rotate(180deg);
|
||||||
opacity: 0.4;
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lightRay {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: rotate(-15deg) scaleY(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: rotate(-15deg) scaleY(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
6386
session-ses_3ec7.md
Normal file
6386
session-ses_3ec7.md
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue