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",
|
||||
"infoPlist": {
|
||||
"LSApplicationQueriesSchemes": [
|
||||
"ppemerchantsdkv1",
|
||||
"ppemerchantsdkv2",
|
||||
"ppemerchantsdkv3",
|
||||
"paytmmp",
|
||||
"gpay",
|
||||
"ppemerchantsdkv1",
|
||||
"ppemerchantsdkv2",
|
||||
"ppemerchantsdkv3",
|
||||
"paytmmp",
|
||||
"gpay",
|
||||
"ppemerchantsdkv1",
|
||||
"ppemerchantsdkv2",
|
||||
"ppemerchantsdkv3",
|
||||
"paytmmp",
|
||||
"gpay",
|
||||
"ppemerchantsdkv1",
|
||||
"ppemerchantsdkv2",
|
||||
"ppemerchantsdkv3",
|
||||
"paytmmp",
|
||||
"gpay",
|
||||
"ppemerchantsdkv1",
|
||||
"ppemerchantsdkv2",
|
||||
"ppemerchantsdkv3",
|
||||
|
|
@ -33,7 +53,8 @@
|
|||
"ppemerchantsdkv3",
|
||||
"paytmmp",
|
||||
"gpay"
|
||||
]
|
||||
],
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
BottomDialog,
|
||||
BottomDropdown,
|
||||
} from "common-ui";
|
||||
import ProductsSelector from "../../../components/ProductsSelector";
|
||||
import { useRouter } from "expo-router";
|
||||
import { trpc } from "../../../src/trpc-client";
|
||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||
|
|
@ -220,10 +221,7 @@ export default function CustomizePopularItems() {
|
|||
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
|
||||
if (isLoadingConstants || isLoadingProducts) {
|
||||
|
|
@ -399,12 +397,13 @@ export default function CustomizePopularItems() {
|
|||
</View>
|
||||
) : (
|
||||
<>
|
||||
<BottomDropdown
|
||||
<ProductsSelector
|
||||
value={selectedProductId || 0}
|
||||
onChange={(val) => setSelectedProductId(val as number)}
|
||||
multiple={false}
|
||||
label="Select Product"
|
||||
options={productOptions}
|
||||
value={selectedProductId || ""}
|
||||
onValueChange={(val) => setSelectedProductId(val as number)}
|
||||
placeholder="Choose a product..."
|
||||
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
||||
/>
|
||||
|
||||
<View style={tw`flex-row gap-3 mt-6`}>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Formik, FormikHelpers } from 'formik';
|
|||
import * as Yup from 'yup';
|
||||
import { MyText, tw, MyTextInput, MyTouchableOpacity, ImageUploader, BottomDropdown } from 'common-ui';
|
||||
import { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||
import ProductsSelector from './ProductsSelector';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
|
@ -57,11 +58,7 @@ export default function BannerForm({
|
|||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||
const products = productsData?.products || [];
|
||||
|
||||
// Create product options for dropdown
|
||||
const productOptions: DropdownOption[] = products.map(product => ({
|
||||
label: `${product.name} (${product.price})`,
|
||||
value: product.id,
|
||||
}));
|
||||
|
||||
|
||||
|
||||
const handleImagePick = usePickImage({
|
||||
|
|
@ -200,16 +197,16 @@ export default function BannerForm({
|
|||
)}
|
||||
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<BottomDropdown
|
||||
label="Select Products"
|
||||
options={productOptions}
|
||||
<ProductsSelector
|
||||
value={values.productIds}
|
||||
onValueChange={(value) => {
|
||||
onChange={(value) => {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
setFieldValue('productIds', selectedValues.map(v => Number(v)));
|
||||
}}
|
||||
placeholder="Select products for banner (optional)"
|
||||
multiple={true}
|
||||
label="Select Products"
|
||||
placeholder="Select products for banner (optional)"
|
||||
labelFormat={(product) => `${product.name} (${product.price})`}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import { View, TouchableOpacity, Alert, ScrollView } from 'react-native';
|
||||
import { useFormik } from 'formik';
|
||||
import { MyText, tw, MyTextInput, MyTouchableOpacity, theme, BottomDropdown } from 'common-ui';
|
||||
import ProductsSelector from './ProductsSelector';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
|
||||
|
|
@ -35,10 +36,7 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
|
|||
|
||||
const products = productsData?.products || [];
|
||||
|
||||
const productOptions = products.map(product => ({
|
||||
label: `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`,
|
||||
value: product.id,
|
||||
}));
|
||||
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
|
|
@ -115,14 +113,13 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
|
|||
/>
|
||||
|
||||
{/* Products Selection */}
|
||||
<BottomDropdown
|
||||
label="Products"
|
||||
<ProductsSelector
|
||||
value={formik.values.product_ids}
|
||||
options={productOptions}
|
||||
onValueChange={(value) => formik.setFieldValue('product_ids', value as number[])}
|
||||
onChange={(value) => formik.setFieldValue('product_ids', value as number[])}
|
||||
multiple={true}
|
||||
label="Products"
|
||||
placeholder="Select products"
|
||||
style={{ marginBottom: 16 }}
|
||||
labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`}
|
||||
/>
|
||||
|
||||
{/* 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 { Formik, FieldArray } from 'formik';
|
||||
import DateTimePickerMod from 'common-ui/src/components/date-time-picker';
|
||||
import { tw, MyTextInput } from 'common-ui';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import BottomDropdown, { DropdownOption } from 'common-ui/src/components/bottom-dropdown';
|
||||
import ProductsSelector from '../components/ProductsSelector';
|
||||
|
||||
interface VendorSnippet {
|
||||
name: string;
|
||||
|
|
@ -44,10 +44,8 @@ export default function SlotForm({
|
|||
const isEditMode = !!slotId;
|
||||
const isPending = isCreating || isUpdating;
|
||||
|
||||
// Fetch products and groups
|
||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||
// Fetch groups
|
||||
const { data: groupsData } = trpc.admin.product.getGroups.useQuery();
|
||||
const products = productsData?.products || [];
|
||||
|
||||
|
||||
|
||||
|
|
@ -120,25 +118,13 @@ export default function SlotForm({
|
|||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
{({ handleSubmit, values, setFieldValue }) => {
|
||||
// Collect all product IDs from selected groups
|
||||
const allGroupProductIds = (values?.selectedGroupIds || []).flatMap(groupId => {
|
||||
const group = (groupsData?.groups || []).find(g => g.id === groupId);
|
||||
return group?.products.map(p => p.id) || [];
|
||||
});
|
||||
// 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(),
|
||||
// Map groups data to match ProductsSelector types (convert price from string to number)
|
||||
const mappedGroups = (groupsData?.groups || []).map(group => ({
|
||||
...group,
|
||||
products: group.products.map(product => ({
|
||||
...product,
|
||||
price: parseFloat(product.price as unknown as string) || 0,
|
||||
})),
|
||||
}));
|
||||
|
||||
return (
|
||||
|
|
@ -157,45 +143,17 @@ export default function SlotForm({
|
|||
<DateTimePickerMod value={values.freezeTime} setValue={(value) => setFieldValue('freezeTime', value)} />
|
||||
</View>
|
||||
|
||||
<View style={tw`mb-4`}>
|
||||
<Text style={tw`text-base mb-2`}>Select Product Groups (Optional)</Text>
|
||||
<BottomDropdown
|
||||
label="Select Product Groups"
|
||||
options={groupOptions}
|
||||
value={values.selectedGroupIds.map(id => id.toString())}
|
||||
onValueChange={(value) => {
|
||||
const selectedValues = Array.isArray(value) ? value : typeof value === 'string' ? [value] : [];
|
||||
const groupIds = selectedValues.map(v => parseInt(v as string));
|
||||
setFieldValue('selectedGroupIds', groupIds);
|
||||
|
||||
// Collect all products from selected groups
|
||||
const allGroupProducts = groupIds.flatMap(groupId => {
|
||||
const group = (groupsData?.groups || []).find(g => g.id === groupId);
|
||||
return group?.products.map(p => p.id) || [];
|
||||
});
|
||||
// Remove duplicates
|
||||
const uniqueProducts = [...new Set(allGroupProducts)];
|
||||
setFieldValue('selectedProductIds', uniqueProducts);
|
||||
}}
|
||||
placeholder="Select product groups"
|
||||
multiple={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={tw`mb-4`}>
|
||||
<Text style={tw`text-base mb-2`}>Select Products (Optional)</Text>
|
||||
<BottomDropdown
|
||||
label="Select Products"
|
||||
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>
|
||||
<View style={tw`mb-4`}>
|
||||
<ProductsSelector
|
||||
value={values.selectedProductIds}
|
||||
onChange={(newProductIds) => setFieldValue('selectedProductIds', newProductIds)}
|
||||
groups={mappedGroups}
|
||||
selectedGroupIds={values.selectedGroupIds}
|
||||
onGroupChange={(newGroupIds) => setFieldValue('selectedGroupIds', newGroupIds)}
|
||||
label="Select Products"
|
||||
placeholder="Select products for this slot"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Vendor Snippets */}
|
||||
<FieldArray name="vendorSnippetList">
|
||||
|
|
@ -214,43 +172,20 @@ export default function SlotForm({
|
|||
</View>
|
||||
|
||||
<View style={tw`mb-4`}>
|
||||
<BottomDropdown
|
||||
label="Select Groups"
|
||||
options={groupOptions.filter(option =>
|
||||
values.selectedGroupIds.includes(Number(option.value))
|
||||
)}
|
||||
value={snippet.groupIds?.map(id => id.toString()) || []}
|
||||
onValueChange={(value) => {
|
||||
const selectedValues = Array.isArray(value) ? value : [value];
|
||||
const selectedGroupIds = selectedValues.map(v => parseInt(v as string));
|
||||
setFieldValue(`vendorSnippetList.${index}.groupIds`, selectedGroupIds);
|
||||
|
||||
// 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
|
||||
<ProductsSelector
|
||||
value={snippet.productIds || []}
|
||||
onChange={(newProductIds) => setFieldValue(`vendorSnippetList.${index}.productIds`, newProductIds)}
|
||||
groups={mappedGroups.filter(group =>
|
||||
values.selectedGroupIds.includes(group.id)
|
||||
).map(group => ({
|
||||
...group,
|
||||
products: group.products.filter(p => values.selectedProductIds.includes(p.id))
|
||||
}))}
|
||||
selectedGroupIds={snippet.groupIds || []}
|
||||
onGroupChange={(newGroupIds) => setFieldValue(`vendorSnippetList.${index}.groupIds`, newGroupIds)}
|
||||
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"
|
||||
multiple={true}
|
||||
isDisabled={(product) => !values.selectedProductIds.includes(product.id)}
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { View, TouchableOpacity, Alert } from 'react-native';
|
|||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploader } from 'common-ui';
|
||||
import ProductsSelector from './ProductsSelector';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
|
||||
|
|
@ -63,11 +64,7 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
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();
|
||||
|
||||
|
|
@ -185,14 +182,14 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
|
|||
error={!!(touched.owner && errors.owner)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<BottomDropdown
|
||||
label="Products"
|
||||
<ProductsSelector
|
||||
value={values.products || []}
|
||||
options={productOptions}
|
||||
onValueChange={(value) => setFieldValue('products', value)}
|
||||
onChange={(value) => setFieldValue('products', value)}
|
||||
multiple={true}
|
||||
label="Products"
|
||||
placeholder="Select products"
|
||||
multiple
|
||||
style={{ marginBottom: 16 }}
|
||||
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId}
|
||||
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
||||
/>
|
||||
<View style={tw`mb-6`}>
|
||||
<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 { MyText, tw, DatePicker, MyTextInput } from 'common-ui';
|
||||
import BottomDropdown from 'common-ui/src/components/bottom-dropdown';
|
||||
import ProductsSelector from '../components/ProductsSelector';
|
||||
import { trpc } from '../src/trpc-client';
|
||||
import { VendorSnippetForm as VendorSnippetFormType, VendorSnippet } from '../types/vendor-snippets';
|
||||
|
||||
|
|
@ -20,9 +21,8 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
showIsPermanentCheckbox = false,
|
||||
}) => {
|
||||
|
||||
// Fetch slots and products
|
||||
// Fetch slots
|
||||
const { data: slotsData } = trpc.user.slots.getSlots.useQuery();
|
||||
const { data: productsData } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||
|
||||
const createSnippet = trpc.admin.vendorSnippets.create.useMutation();
|
||||
const updateSnippet = trpc.admin.vendorSnippets.update.useMutation();
|
||||
|
|
@ -108,16 +108,6 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
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 (
|
||||
<View style={tw`flex-1 bg-white`}>
|
||||
<View style={tw`px-6 py-4 border-b border-gray-200`}>
|
||||
|
|
@ -190,20 +180,14 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
|||
|
||||
{/* Product Selection */}
|
||||
<View>
|
||||
<MyText style={tw`text-gray-700 font-medium mb-2`}>Products</MyText>
|
||||
<BottomDropdown
|
||||
label="Select Products"
|
||||
value={formik.values.productIds}
|
||||
options={productOptions}
|
||||
onValueChange={(values) => formik.setFieldValue('productIds', values)}
|
||||
<ProductsSelector
|
||||
value={formik.values.productIds.map(id => parseInt(id))}
|
||||
onChange={(selectedProductIds) => formik.setFieldValue('productIds', (selectedProductIds as number[]).map(id => id.toString()))}
|
||||
multiple={true}
|
||||
label="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 && (
|
||||
<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
|
||||
config.resolver.nodeModulesPaths = [
|
||||
path.resolve(workspaceRoot, '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`
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,12 +8,69 @@ export default function Inauguration() {
|
|||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const triggerConfetti = useCallback(() => {
|
||||
const duration = 5 * 1000;
|
||||
const duration = 8 * 1000;
|
||||
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;
|
||||
|
||||
// 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 timeLeft = animationEnd - Date.now();
|
||||
|
||||
|
|
@ -21,27 +78,41 @@ export default function Inauguration() {
|
|||
return clearInterval(interval);
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
|
||||
const colors = ['#2E90FA', '#53B1FD', '#84CAFF', '#1570EF', '#175CD3'];
|
||||
|
||||
// Random mini-crackers
|
||||
const miniCrackerX = randomInRange(0.1, 0.9);
|
||||
const miniColors = crackerColors[Math.floor(Math.random() * crackerColors.length)];
|
||||
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
|
||||
colors: colors,
|
||||
shapes: ['circle', 'square'],
|
||||
scalar: randomInRange(0.8, 1.5)
|
||||
particleCount: 40,
|
||||
spread: 360,
|
||||
origin: { x: miniCrackerX, y: 0.85 },
|
||||
colors: miniColors,
|
||||
shapes: ['circle', 'square', 'star'],
|
||||
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,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
|
||||
colors: colors,
|
||||
shapes: ['circle', 'square'],
|
||||
scalar: randomInRange(0.8, 1.5)
|
||||
});
|
||||
}, 250);
|
||||
|
||||
// Occasional sparkle burst
|
||||
if (Math.random() > 0.7) {
|
||||
confetti({
|
||||
particleCount: 30,
|
||||
spread: 360,
|
||||
origin: { x: randomInRange(0.2, 0.8), y: 0.9 },
|
||||
colors: ['#FFFFFF', '#FFD700', '#C0C0C0'],
|
||||
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 = () => {
|
||||
|
|
@ -55,41 +126,100 @@ export default function Inauguration() {
|
|||
|
||||
return (
|
||||
<div className="relative w-full h-screen overflow-hidden">
|
||||
{/* Animated Gradient Background */}
|
||||
{/* Elegant Garden Background */}
|
||||
<div
|
||||
className="absolute inset-0 animate-gradient-xy"
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #2E90FA 0%, #1570EF 25%, #53B1FD 50%, #84CAFF 75%, #2E90FA 100%)',
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'gradientShift 15s ease infinite'
|
||||
background: `
|
||||
linear-gradient(180deg,
|
||||
#e8f5e9 0%,
|
||||
#c8e6c9 20%,
|
||||
#a5d6a7 40%,
|
||||
#81c784 60%,
|
||||
#66bb6a 80%,
|
||||
#4caf50 100%
|
||||
)
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Animated Mesh Gradient Overlay */}
|
||||
<div className="absolute inset-0 opacity-60">
|
||||
{/* Soft Nature Mesh Overlay */}
|
||||
<div className="absolute inset-0 opacity-40">
|
||||
<div
|
||||
className="absolute inset-0 animate-pulse"
|
||||
className="absolute inset-0"
|
||||
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%)',
|
||||
filter: 'blur(60px)'
|
||||
background: `
|
||||
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>
|
||||
|
||||
{/* Floating Bubbles */}
|
||||
{/* Floating Petals */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
{[...Array(20)].map((_, i) => (
|
||||
{[...Array(15)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full"
|
||||
key={`petal-${i}`}
|
||||
className="absolute"
|
||||
style={{
|
||||
width: `${Math.random() * 100 + 50}px`,
|
||||
height: `${Math.random() * 100 + 50}px`,
|
||||
width: `${Math.random() * 20 + 15}px`,
|
||||
height: `${Math.random() * 20 + 15}px`,
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
background: `radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%)`,
|
||||
animation: `floatBubble ${Math.random() * 10 + 10}s ease-in-out infinite`,
|
||||
animationDelay: `${Math.random() * 5}s`,
|
||||
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
|
||||
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 */}
|
||||
<div className="relative inline-block">
|
||||
{/* 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 */}
|
||||
<div className="relative">
|
||||
|
|
@ -118,9 +248,9 @@ export default function Inauguration() {
|
|||
<span
|
||||
className="bg-clip-text text-transparent animate-shimmer"
|
||||
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',
|
||||
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'
|
||||
}}
|
||||
>
|
||||
|
|
@ -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" />
|
||||
|
||||
{/* Button Container */}
|
||||
<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-[#1570EF] to-[#53B1FD] px-12 py-6">
|
||||
<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-[#4caf50] to-[#81c784] px-12 py-6">
|
||||
{/* Animated Background on Hover */}
|
||||
<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' }}
|
||||
/>
|
||||
|
||||
|
|
@ -194,7 +324,7 @@ export default function Inauguration() {
|
|||
|
||||
{/* 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 -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>
|
||||
|
||||
{/* Subtext under button */}
|
||||
|
|
@ -206,12 +336,12 @@ export default function Inauguration() {
|
|||
/* Welcome Message */
|
||||
<div className="text-center animate-fade-in-up">
|
||||
<div className="relative inline-block">
|
||||
{/* Background glow */}
|
||||
<div className="absolute -inset-8 bg-gradient-to-r from-[#2E90FA] to-[#84CAFF] rounded-full blur-3xl opacity-40" />
|
||||
{/* Background glow - elegant garden green */}
|
||||
<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">
|
||||
{/* 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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
|
|
@ -291,22 +421,67 @@ export default function Inauguration() {
|
|||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
@keyframes floatBubble {
|
||||
@keyframes gardenPulse {
|
||||
0%, 100% {
|
||||
transform: translateY(0) translateX(0) scale(1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-30px) translateX(10px) scale(1.1);
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px) translateX(-10px) scale(0.9);
|
||||
opacity: 0.3;
|
||||
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% {
|
||||
transform: translateY(-50px) translateX(20px) rotate(60deg);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) translateX(-25px) rotate(120deg);
|
||||
opacity: 0.4;
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-40px) translateX(5px) scale(1.05);
|
||||
opacity: 0.4;
|
||||
transform: translateY(-70px) translateX(15px) rotate(180deg);
|
||||
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