improve event request form styling
This commit is contained in:
parent
56ddba112a
commit
97ce397281
7 changed files with 1692 additions and 1027 deletions
|
@ -3,418 +3,437 @@ import { motion } from 'framer-motion';
|
|||
import toast from 'react-hot-toast';
|
||||
import type { EventRequestFormData } from './EventRequestForm';
|
||||
import InvoiceBuilder from './InvoiceBuilder';
|
||||
import type { InvoiceData } from './InvoiceBuilder';
|
||||
import type { InvoiceData, InvoiceItem } from './InvoiceBuilder';
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
// Enhanced animation variants with faster transitions
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.035,
|
||||
when: "beforeChildren",
|
||||
duration: 0.3,
|
||||
ease: "easeOut"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Animation variants
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 24
|
||||
stiffness: 500,
|
||||
damping: 25,
|
||||
mass: 0.8,
|
||||
duration: 0.25
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Input field hover animation
|
||||
const inputHoverVariants = {
|
||||
hover: {
|
||||
scale: 1.01,
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.15 }
|
||||
}
|
||||
};
|
||||
|
||||
// Button animation
|
||||
const buttonVariants = {
|
||||
hover: {
|
||||
scale: 1.03,
|
||||
transition: { duration: 0.15, ease: "easeOut" }
|
||||
},
|
||||
tap: {
|
||||
scale: 0.97,
|
||||
transition: { duration: 0.1 }
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle animation
|
||||
const toggleVariants = {
|
||||
checked: { backgroundColor: "rgba(var(--p), 0.2)" },
|
||||
unchecked: { backgroundColor: "rgba(0, 0, 0, 0.05)" },
|
||||
hover: { scale: 1.01, transition: { duration: 0.15 } }
|
||||
};
|
||||
|
||||
interface ASFundingSectionProps {
|
||||
formData: EventRequestFormData;
|
||||
onDataChange: (data: Partial<EventRequestFormData>) => void;
|
||||
}
|
||||
|
||||
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataChange }) => {
|
||||
const [invoiceFile, setInvoiceFile] = useState<File | null>(formData.invoice);
|
||||
const [invoiceFiles, setInvoiceFiles] = useState<File[]>(formData.invoice_files || []);
|
||||
const [useJsonInput, setUseJsonInput] = useState(false);
|
||||
const [jsonInput, setJsonInput] = useState('');
|
||||
const [jsonError, setJsonError] = useState('');
|
||||
const [showExample, setShowExample] = useState(false);
|
||||
const [jsonInput, setJsonInput] = useState<string>('');
|
||||
const [jsonError, setJsonError] = useState<string>('');
|
||||
const [showJsonInput, setShowJsonInput] = useState<boolean>(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// Example JSON for the user to reference
|
||||
const jsonExample = {
|
||||
items: [
|
||||
{
|
||||
item: "Chicken Plate",
|
||||
quantity: 10,
|
||||
unit_price: 12.99
|
||||
},
|
||||
{
|
||||
item: "Vegetarian Plate",
|
||||
quantity: 5,
|
||||
unit_price: 11.99
|
||||
},
|
||||
{
|
||||
item: "Bottled Water",
|
||||
quantity: 15,
|
||||
unit_price: 1.50
|
||||
}
|
||||
],
|
||||
tax: 10.14,
|
||||
tip: 15.00,
|
||||
vendor: "L&L Hawaiian BBQ"
|
||||
};
|
||||
|
||||
// Handle single invoice file upload (for backward compatibility)
|
||||
// Handle invoice file upload
|
||||
const handleInvoiceFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const file = e.target.files[0];
|
||||
setInvoiceFile(file);
|
||||
onDataChange({ invoice: file });
|
||||
const newFiles = Array.from(e.target.files) as File[];
|
||||
setInvoiceFiles(newFiles);
|
||||
onDataChange({ invoice_files: newFiles });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle multiple invoice files upload
|
||||
const handleMultipleInvoiceFilesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const files = Array.from(e.target.files) as File[];
|
||||
|
||||
// Check file sizes
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB limit per file
|
||||
const oversizedFiles = files.filter(file => file.size > MAX_FILE_SIZE);
|
||||
|
||||
if (oversizedFiles.length > 0) {
|
||||
toast.error(`Some files exceed the 10MB size limit: ${oversizedFiles.map(f => f.name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update state with new files
|
||||
const updatedFiles = [...invoiceFiles, ...files];
|
||||
setInvoiceFiles(updatedFiles);
|
||||
onDataChange({ invoice_files: updatedFiles });
|
||||
|
||||
// Also set the first file as the main invoice file for backward compatibility
|
||||
if (files.length > 0 && !formData.invoice) {
|
||||
setInvoiceFile(files[0]);
|
||||
onDataChange({ invoice: files[0] });
|
||||
}
|
||||
|
||||
toast.success(`Added ${files.length} file${files.length > 1 ? 's' : ''} successfully`);
|
||||
}
|
||||
};
|
||||
|
||||
// Remove an invoice file
|
||||
const handleRemoveInvoiceFile = (index: number) => {
|
||||
const updatedFiles = [...invoiceFiles];
|
||||
const removedFileName = updatedFiles[index].name;
|
||||
updatedFiles.splice(index, 1);
|
||||
setInvoiceFiles(updatedFiles);
|
||||
onDataChange({ invoice_files: updatedFiles });
|
||||
|
||||
// Update the main invoice file if needed
|
||||
if (invoiceFile && invoiceFile.name === removedFileName) {
|
||||
const newMainFile = updatedFiles.length > 0 ? updatedFiles[0] : null;
|
||||
setInvoiceFile(newMainFile);
|
||||
onDataChange({ invoice: newMainFile });
|
||||
}
|
||||
|
||||
toast.success(`Removed ${removedFileName}`);
|
||||
};
|
||||
|
||||
// Handle invoice data change
|
||||
const handleInvoiceDataChange = (invoiceData: InvoiceData) => {
|
||||
// Update the invoiceData in the form
|
||||
onDataChange({ invoiceData });
|
||||
|
||||
// For backward compatibility, create a properly formatted JSON string
|
||||
const jsonFormat = {
|
||||
items: invoiceData.items.map(item => ({
|
||||
item: item.description,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unitPrice
|
||||
})),
|
||||
tax: invoiceData.taxAmount,
|
||||
tip: invoiceData.tipAmount,
|
||||
total: invoiceData.total,
|
||||
vendor: invoiceData.vendor
|
||||
};
|
||||
|
||||
// For backward compatibility, still update the itemized_invoice field
|
||||
// but with a more structured format that's easier to parse if needed
|
||||
const itemizedText = JSON.stringify(jsonFormat, null, 2);
|
||||
|
||||
// Update the itemized_invoice field for backward compatibility
|
||||
onDataChange({ itemized_invoice: itemizedText });
|
||||
};
|
||||
|
||||
// Handle JSON input change
|
||||
const handleJsonInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
setJsonInput(value);
|
||||
|
||||
// Validate JSON as user types
|
||||
if (value.trim()) {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
setJsonInput(e.target.value);
|
||||
setJsonError('');
|
||||
} catch (err) {
|
||||
setJsonError('Invalid JSON format. Please check your syntax.');
|
||||
}
|
||||
} else {
|
||||
setJsonError('');
|
||||
}
|
||||
};
|
||||
|
||||
// Show JSON example
|
||||
const showJsonExample = () => {
|
||||
// Toggle example visibility
|
||||
setShowExample(!showExample);
|
||||
|
||||
// If showing example, populate the textarea with the example JSON
|
||||
if (!showExample) {
|
||||
setJsonInput(JSON.stringify(jsonExample, null, 2));
|
||||
const example = {
|
||||
vendor: "Example Restaurant",
|
||||
items: [
|
||||
{
|
||||
id: "item-1",
|
||||
description: "Burger",
|
||||
quantity: 2,
|
||||
unitPrice: 12.99,
|
||||
amount: 25.98
|
||||
},
|
||||
{
|
||||
id: "item-2",
|
||||
description: "Fries",
|
||||
quantity: 2,
|
||||
unitPrice: 4.99,
|
||||
amount: 9.98
|
||||
}
|
||||
],
|
||||
subtotal: 35.96,
|
||||
taxRate: 9.0,
|
||||
taxAmount: 3.24,
|
||||
tipPercentage: 13.9,
|
||||
tipAmount: 5.00,
|
||||
total: 44.20
|
||||
};
|
||||
setJsonInput(JSON.stringify(example, null, 2));
|
||||
};
|
||||
|
||||
// Validate and apply JSON
|
||||
const validateAndApplyJson = () => {
|
||||
try {
|
||||
// Parse the JSON input
|
||||
const parsedJson = JSON.parse(jsonInput);
|
||||
|
||||
// Validate the structure
|
||||
let validationError = '';
|
||||
|
||||
// Check for required fields
|
||||
if (!parsedJson.items || !Array.isArray(parsedJson.items)) {
|
||||
validationError = 'Missing or invalid "items" array.';
|
||||
} else if (parsedJson.items.length === 0) {
|
||||
validationError = 'The "items" array cannot be empty.';
|
||||
} else {
|
||||
// Check each item in the array
|
||||
for (let i = 0; i < parsedJson.items.length; i++) {
|
||||
const item = parsedJson.items[i];
|
||||
if (!item.item || typeof item.item !== 'string') {
|
||||
validationError = `Item #${i + 1} is missing a valid "item" name.`;
|
||||
break;
|
||||
}
|
||||
if (typeof item.quantity !== 'number' || item.quantity <= 0) {
|
||||
validationError = `Item #${i + 1} is missing a valid "quantity" (must be a positive number).`;
|
||||
break;
|
||||
}
|
||||
if (typeof item.unit_price !== 'number' || item.unit_price < 0) {
|
||||
validationError = `Item #${i + 1} is missing a valid "unit_price" (must be a non-negative number).`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check other required fields
|
||||
if (!validationError) {
|
||||
if (typeof parsedJson.tax !== 'number') {
|
||||
validationError = 'Missing or invalid "tax" amount (must be a number).';
|
||||
} else if (typeof parsedJson.tip !== 'number') {
|
||||
validationError = 'Missing or invalid "tip" amount (must be a number).';
|
||||
} else if (!parsedJson.vendor || typeof parsedJson.vendor !== 'string') {
|
||||
validationError = 'Missing or invalid "vendor" name.';
|
||||
}
|
||||
}
|
||||
|
||||
if (validationError) {
|
||||
setJsonError(validationError);
|
||||
if (!jsonInput.trim()) {
|
||||
setJsonError('JSON input is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate subtotal and total
|
||||
const subtotal = parsedJson.items.reduce((sum: number, item: any) => sum + (item.quantity * item.unit_price), 0);
|
||||
const total = subtotal + parsedJson.tax + parsedJson.tip;
|
||||
const data = JSON.parse(jsonInput);
|
||||
|
||||
// Convert the JSON to the format expected by InvoiceData
|
||||
const invoiceData: InvoiceData = {
|
||||
items: parsedJson.items.map((item: any, index: number) => ({
|
||||
id: `item-${index + 1}`,
|
||||
description: item.item,
|
||||
// Validate structure
|
||||
if (!data.vendor) {
|
||||
setJsonError('Vendor field is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.items) || data.items.length === 0) {
|
||||
setJsonError('Items array is required and must contain at least one item');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate items
|
||||
for (const item of data.items) {
|
||||
if (!item.description || typeof item.unitPrice !== 'number' || typeof item.quantity !== 'number') {
|
||||
setJsonError('Each item must have description, unitPrice, and quantity fields');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate tax, tip, and total
|
||||
if (typeof data.taxAmount !== 'number') {
|
||||
setJsonError('Tax amount must be a number');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.tipAmount !== 'number') {
|
||||
setJsonError('Tip amount must be a number');
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data.total !== 'number') {
|
||||
setJsonError('Total is required and must be a number');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create itemized invoice string for Pocketbase
|
||||
const itemizedInvoice = JSON.stringify({
|
||||
vendor: data.vendor,
|
||||
items: data.items.map((item: InvoiceItem) => ({
|
||||
item: item.description,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.unit_price,
|
||||
amount: item.quantity * item.unit_price
|
||||
unit_price: item.unitPrice,
|
||||
amount: item.amount
|
||||
})),
|
||||
subtotal: subtotal,
|
||||
taxRate: subtotal ? (parsedJson.tax / subtotal) * 100 : 0,
|
||||
taxAmount: parsedJson.tax,
|
||||
tipPercentage: subtotal ? (parsedJson.tip / subtotal) * 100 : 0,
|
||||
tipAmount: parsedJson.tip,
|
||||
total: total,
|
||||
vendor: parsedJson.vendor
|
||||
};
|
||||
subtotal: data.subtotal,
|
||||
tax: data.taxAmount,
|
||||
tip: data.tipAmount,
|
||||
total: data.total
|
||||
}, null, 2);
|
||||
|
||||
// Update the form data
|
||||
handleInvoiceDataChange(invoiceData);
|
||||
// Apply the JSON data to the form
|
||||
onDataChange({
|
||||
invoiceData: data,
|
||||
itemized_invoice: itemizedInvoice,
|
||||
as_funding_required: true
|
||||
});
|
||||
|
||||
// Update the itemized_invoice field with the complete JSON including calculated total
|
||||
const completeJson = {
|
||||
...parsedJson,
|
||||
subtotal: subtotal,
|
||||
total: total
|
||||
};
|
||||
onDataChange({ itemized_invoice: JSON.stringify(completeJson, null, 2) });
|
||||
|
||||
// Show success message
|
||||
toast.success('JSON invoice data applied successfully!');
|
||||
|
||||
// Optionally, switch back to the invoice builder view to show the applied data
|
||||
setUseJsonInput(false);
|
||||
} catch (err) {
|
||||
setJsonError('Failed to parse JSON. Please check your syntax.');
|
||||
toast.success('Invoice data applied successfully');
|
||||
setShowJsonInput(false);
|
||||
} catch (error) {
|
||||
setJsonError('Invalid JSON format: ' + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle drag events for file upload
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const newFiles = Array.from(e.dataTransfer.files) as File[];
|
||||
setInvoiceFiles(newFiles);
|
||||
onDataChange({ invoice_files: newFiles });
|
||||
}
|
||||
};
|
||||
|
||||
// Handle invoice data change
|
||||
const handleInvoiceDataChange = (data: InvoiceData) => {
|
||||
// Create itemized invoice string for Pocketbase
|
||||
const itemizedInvoice = JSON.stringify({
|
||||
vendor: data.vendor,
|
||||
items: data.items.map((item: InvoiceItem) => ({
|
||||
item: item.description,
|
||||
quantity: item.quantity,
|
||||
unit_price: item.unitPrice,
|
||||
amount: item.amount
|
||||
})),
|
||||
subtotal: data.subtotal,
|
||||
tax: data.taxAmount,
|
||||
tip: data.tipAmount,
|
||||
total: data.total
|
||||
}, null, 2);
|
||||
|
||||
onDataChange({
|
||||
invoiceData: data,
|
||||
itemized_invoice: itemizedInvoice
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-primary">AS Funding Information</h2>
|
||||
|
||||
<CustomAlert
|
||||
type="warning"
|
||||
title="Important Information"
|
||||
message="Please make sure the restaurant is a valid AS Funding food vendor! An invoice can be an unofficial receipt. Just make sure that the restaurant name and location, desired pickup or delivery date and time, all the items ordered plus their prices, discount/fees/tax/tip, and total are on the invoice! We don't recommend paying out of pocket because reimbursements can be a hassle when you're not a Principal Member."
|
||||
className="mb-6"
|
||||
icon="heroicons:exclamation-triangle"
|
||||
/>
|
||||
|
||||
{/* Invoice Builder Instructions */}
|
||||
<motion.div variants={itemVariants} className="bg-base-300/50 p-4 rounded-lg mb-6">
|
||||
<h3 className="font-bold text-lg mb-2">How to Use the Invoice Builder</h3>
|
||||
<ol className="list-decimal list-inside space-y-2 text-sm">
|
||||
<li>Enter the vendor/restaurant name in the field provided.</li>
|
||||
<li>Add each item from your invoice by filling in the description, quantity, and unit price, then click "Add Item".</li>
|
||||
<li>The subtotal, tax, and tip will be calculated automatically based on the tax rate and tip percentage.</li>
|
||||
<li>You can adjust the tax rate (default is 7.75% for San Diego) and tip percentage as needed.</li>
|
||||
<li>Remove items by clicking the "X" button next to each item.</li>
|
||||
<li>Upload your actual invoice file (receipt, screenshot, etc.) using the file upload below.</li>
|
||||
</ol>
|
||||
</motion.div>
|
||||
|
||||
{/* JSON Invoice Paste Option */}
|
||||
<motion.div variants={itemVariants} className="bg-base-300/50 p-4 rounded-lg mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="font-bold text-lg">Paste JSON Invoice</h3>
|
||||
<div className="form-control">
|
||||
<label className="label cursor-pointer">
|
||||
<span className="label-text mr-2">Use JSON input</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="toggle toggle-primary"
|
||||
checked={useJsonInput}
|
||||
onChange={(e) => setUseJsonInput(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{useJsonInput && (
|
||||
<>
|
||||
<div className="form-control mb-4">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Paste your JSON invoice data</span>
|
||||
<span className="label-text-alt">
|
||||
<div className="tooltip tooltip-left" data-tip={jsonInput.trim().length > 0 ? "Clear the text field to see an example" : ""}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-ghost"
|
||||
onClick={showJsonExample}
|
||||
disabled={jsonInput.trim().length > 0}
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
See example
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
className={`textarea textarea-bordered h-48 font-mono text-sm ${jsonError ? 'textarea-error' : ''}`}
|
||||
value={jsonInput}
|
||||
onChange={handleJsonInputChange}
|
||||
placeholder="paste json here"
|
||||
></textarea>
|
||||
{jsonError && (
|
||||
<label className="label">
|
||||
<span className="label-text-alt text-error">{jsonError}</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={validateAndApplyJson}
|
||||
disabled={!!jsonError}
|
||||
>
|
||||
Apply JSON
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Required JSON Format"
|
||||
message={`Your JSON must include: an array of items (each with item name, quantity, and unit_price), tax amount, tip amount, and vendor name. The total will be calculated automatically.`}
|
||||
className="mb-4"
|
||||
icon="heroicons:information-circle"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Invoice Builder */}
|
||||
{!useJsonInput && (
|
||||
<InvoiceBuilder
|
||||
invoiceData={formData.invoiceData}
|
||||
onChange={handleInvoiceDataChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Invoice file upload */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">
|
||||
Upload your invoice files (receipts, screenshots, etc.)
|
||||
</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
className="file-input file-input-bordered w-full file-input-primary hover:file-input-ghost transition-all duration-300"
|
||||
onChange={handleMultipleInvoiceFilesChange}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
required={invoiceFiles.length === 0}
|
||||
/>
|
||||
|
||||
{invoiceFiles.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-medium mb-2">Uploaded files:</p>
|
||||
<div className="space-y-2">
|
||||
{invoiceFiles.map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between bg-base-300/30 p-2 rounded">
|
||||
<span className="text-sm truncate max-w-[80%]">{file.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-xs"
|
||||
onClick={() => handleRemoveInvoiceFile(index)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Official food invoices will be required 2 weeks before the start of your event. Please use the following naming format: EventName_OrderLocation_DateOfEvent (i.e. QPWorkathon#1_PapaJohns_01/06/2025)
|
||||
<motion.div variants={itemVariants}>
|
||||
<h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
|
||||
AS Funding Details
|
||||
</h2>
|
||||
<p className="text-lg text-base-content/80 mb-6">
|
||||
Please provide the necessary information for your Associated Students funding request.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<CustomAlert
|
||||
type="warning"
|
||||
title="Important Note"
|
||||
title="Important Deadline"
|
||||
message="AS Funding requests must be submitted at least 6 weeks before your event. Please check the Funding Guide or the Google Calendar for the funding request deadlines."
|
||||
className="mb-4"
|
||||
icon="heroicons:clock"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Invoice Upload Section */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<h3 className="text-xl font-semibold mb-2 text-primary">Invoice Information</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">Upload your invoice files or create an itemized invoice below.</p>
|
||||
|
||||
<motion.div
|
||||
className={`mt-2 border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? 'border-primary bg-primary/5' : 'border-base-300 hover:border-primary/50'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02, boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)" }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById('invoice-files')?.click()}
|
||||
>
|
||||
<input
|
||||
id="invoice-files"
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={handleInvoiceFileChange}
|
||||
accept=".pdf,.jpg,.jpeg,.png"
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<motion.div
|
||||
className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary"
|
||||
whileHover={{ rotate: 15, scale: 1.1 }}
|
||||
>
|
||||
<Icon icon="heroicons:document-text" className="h-8 w-8" />
|
||||
</motion.div>
|
||||
|
||||
{invoiceFiles.length > 0 ? (
|
||||
<>
|
||||
<p className="font-medium text-primary">{invoiceFiles.length} file(s) selected:</p>
|
||||
<div className="max-h-24 overflow-y-auto text-left w-full">
|
||||
<ul className="list-disc list-inside text-sm">
|
||||
{invoiceFiles.map((file, index) => (
|
||||
<li key={index} className="truncate">{file.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Click or drag to replace</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="font-medium">Drop your invoice files here or click to browse</p>
|
||||
<p className="text-xs text-gray-500">Supports PDF, JPG, JPEG, PNG</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* JSON/Builder Toggle */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-bold text-primary">Invoice Details</h3>
|
||||
|
||||
<motion.div
|
||||
className="bg-base-300/50 p-1 rounded-lg flex items-center"
|
||||
whileHover="hover"
|
||||
variants={toggleVariants}
|
||||
>
|
||||
<motion.button
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${!showJsonInput ? 'bg-primary text-primary-content' : 'hover:bg-base-200'
|
||||
}`}
|
||||
onClick={() => setShowJsonInput(false)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Invoice Builder
|
||||
</motion.button>
|
||||
<motion.button
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${showJsonInput ? 'bg-primary text-primary-content' : 'hover:bg-base-200'
|
||||
}`}
|
||||
onClick={() => setShowJsonInput(true)}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
Paste JSON
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{showJsonInput ? (
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="label-text font-medium">JSON Invoice Data</label>
|
||||
<motion.button
|
||||
type="button"
|
||||
className="btn btn-sm btn-ghost text-primary"
|
||||
onClick={showJsonExample}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={buttonVariants}
|
||||
>
|
||||
<Icon icon="heroicons:code-bracket" className="w-4 h-4 mr-1" />
|
||||
Show Example
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<motion.textarea
|
||||
className={`textarea textarea-bordered w-full h-64 font-mono text-sm ${jsonError ? 'textarea-error' : 'focus:textarea-primary'}`}
|
||||
value={jsonInput}
|
||||
onChange={handleJsonInputChange}
|
||||
placeholder="Paste your JSON invoice data here..."
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
|
||||
{jsonError && (
|
||||
<div className="text-error text-sm flex items-center gap-1">
|
||||
<Icon icon="heroicons:exclamation-circle" className="w-4 h-4" />
|
||||
{jsonError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<motion.button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={validateAndApplyJson}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={buttonVariants}
|
||||
>
|
||||
<Icon icon="heroicons:check-circle" className="w-5 h-5 mr-1" />
|
||||
Apply JSON
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<InvoiceBuilder
|
||||
invoiceData={formData.invoiceData || {
|
||||
vendor: '',
|
||||
items: [],
|
||||
subtotal: 0,
|
||||
taxRate: 0,
|
||||
taxAmount: 0,
|
||||
tipPercentage: 0,
|
||||
tipAmount: 0,
|
||||
total: 0
|
||||
}}
|
||||
onChange={handleInvoiceDataChange}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,8 +2,20 @@ import React from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import type { EventRequestFormData } from './EventRequestForm';
|
||||
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
|
||||
// Enhanced animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
when: "beforeChildren"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Animation variants
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
|
@ -17,6 +29,15 @@ const itemVariants = {
|
|||
}
|
||||
};
|
||||
|
||||
// Input field hover animation
|
||||
const inputHoverVariants = {
|
||||
hover: {
|
||||
scale: 1.01,
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
interface EventDetailsSectionProps {
|
||||
formData: EventRequestFormData;
|
||||
onDataChange: (data: Partial<EventRequestFormData>) => void;
|
||||
|
@ -24,101 +45,156 @@ interface EventDetailsSectionProps {
|
|||
|
||||
const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onDataChange }) => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-primary">Event Details</h2>
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
|
||||
Event Details
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="bg-base-300/50 p-4 rounded-lg mb-6">
|
||||
<p className="text-sm">
|
||||
Please remember to ping @Coordinators in #-events on Slack once you've submitted this form so that they can fill out a TAP form for you.
|
||||
</p>
|
||||
</div>
|
||||
<motion.div variants={itemVariants}>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Coordinator Notification"
|
||||
message="Please remember to ping @Coordinators in #-events on Slack once you've submitted this form so that they can fill out a TAP form for you."
|
||||
className="mb-6"
|
||||
icon="heroicons:information-circle"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Event Name */}
|
||||
<motion.div variants={itemVariants} className="form-control bg-base-200/50 p-4 rounded-lg">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">Event Name</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
<motion.input
|
||||
type="text"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
|
||||
value={formData.name}
|
||||
onChange={(e) => onDataChange({ name: e.target.value })}
|
||||
placeholder="Enter event name"
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Event Description */}
|
||||
<motion.div variants={itemVariants} className="form-control bg-base-200/50 p-4 rounded-lg">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">Event Description</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
<motion.textarea
|
||||
className="textarea textarea-bordered focus:textarea-primary transition-all duration-300 min-h-[120px] mt-2"
|
||||
value={formData.event_description}
|
||||
onChange={(e) => onDataChange({ event_description: e.target.value })}
|
||||
placeholder="Provide a detailed description of your event"
|
||||
rows={4}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Date and Time Section */}
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
||||
>
|
||||
{/* Event Start Date */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Event Start Date & Time</span>
|
||||
<span className="label-text font-medium text-lg">Event Start</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
<motion.input
|
||||
type="datetime-local"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
|
||||
value={formData.start_date_time}
|
||||
onChange={(e) => onDataChange({ start_date_time: e.target.value })}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Event End Date */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Event End Date & Time</span>
|
||||
<span className="label-text font-medium text-lg">Event End</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
<motion.input
|
||||
type="datetime-local"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
|
||||
value={formData.end_date_time}
|
||||
onChange={(e) => onDataChange({ end_date_time: e.target.value })}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Event Location */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Event Location</span>
|
||||
<span className="label-text font-medium text-lg">Event Location</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
<motion.input
|
||||
type="text"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
|
||||
value={formData.location}
|
||||
onChange={(e) => onDataChange({ location: e.target.value })}
|
||||
placeholder="Enter event location"
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Room Booking */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Do you/will you have a room booking for this event?</span>
|
||||
<span className="label-text font-medium text-lg">Room Booking Status</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="flex gap-6 mt-2">
|
||||
<motion.label
|
||||
className="flex items-center gap-3 cursor-pointer bg-base-100 p-3 rounded-lg hover:bg-primary/10 transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -126,9 +202,13 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
|
|||
onChange={() => onDataChange({ will_or_have_room_booking: true })}
|
||||
required
|
||||
/>
|
||||
<span>Yes</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<span className="font-medium">Yes, I have/will have a booking</span>
|
||||
</motion.label>
|
||||
<motion.label
|
||||
className="flex items-center gap-3 cursor-pointer bg-base-100 p-3 rounded-lg hover:bg-primary/10 transition-colors"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -136,11 +216,11 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
|
|||
onChange={() => onDataChange({ will_or_have_room_booking: false })}
|
||||
required
|
||||
/>
|
||||
<span>No</span>
|
||||
</label>
|
||||
<span className="font-medium">No, I don't need a booking</span>
|
||||
</motion.label>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -82,6 +82,9 @@ export interface EventRequestFormData {
|
|||
formReviewed?: boolean; // Track if the form has been reviewed
|
||||
}
|
||||
|
||||
// Add CustomAlert import
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
|
||||
const EventRequestForm: React.FC = () => {
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
@ -164,10 +167,22 @@ const EventRequestForm: React.FC = () => {
|
|||
|
||||
// Handle form section data changes
|
||||
const handleSectionDataChange = (sectionData: Partial<EventRequestFormData>) => {
|
||||
setFormData(prevData => ({
|
||||
...prevData,
|
||||
...sectionData
|
||||
}));
|
||||
// Ensure both needs_graphics and flyers_needed are synchronized
|
||||
if (sectionData.flyers_needed !== undefined && sectionData.needs_graphics === undefined) {
|
||||
sectionData.needs_graphics = sectionData.flyers_needed ? true : false;
|
||||
}
|
||||
|
||||
// Ensure both needs_as_funding and as_funding_required are synchronized
|
||||
if (sectionData.needs_as_funding !== undefined && sectionData.as_funding_required === undefined) {
|
||||
sectionData.as_funding_required = sectionData.needs_as_funding ? true : false;
|
||||
}
|
||||
|
||||
setFormData(prevData => {
|
||||
// Save to localStorage
|
||||
const updatedData = { ...prevData, ...sectionData };
|
||||
localStorage.setItem('eventRequestFormData', JSON.stringify(updatedData));
|
||||
return updatedData;
|
||||
});
|
||||
};
|
||||
|
||||
// Add this function before the handleSubmit function
|
||||
|
@ -260,19 +275,22 @@ const EventRequestForm: React.FC = () => {
|
|||
end_date_time: new Date(formData.end_date_time).toISOString(),
|
||||
event_description: formData.event_description,
|
||||
flyers_needed: formData.flyers_needed,
|
||||
photography_needed: formData.photography_needed,
|
||||
as_funding_required: formData.needs_as_funding,
|
||||
food_drinks_being_served: formData.food_drinks_being_served,
|
||||
// Store the itemized_invoice as a string for backward compatibility
|
||||
itemized_invoice: formData.itemized_invoice,
|
||||
flyer_type: formData.flyer_type,
|
||||
other_flyer_type: formData.other_flyer_type,
|
||||
flyer_advertising_start_date: formData.flyer_advertising_start_date ? new Date(formData.flyer_advertising_start_date).toISOString() : '',
|
||||
flyer_additional_requests: formData.flyer_additional_requests,
|
||||
photography_needed: formData.photography_needed,
|
||||
required_logos: formData.required_logos,
|
||||
advertising_format: formData.advertising_format,
|
||||
will_or_have_room_booking: formData.will_or_have_room_booking,
|
||||
expected_attendance: formData.expected_attendance,
|
||||
as_funding_required: formData.as_funding_required,
|
||||
food_drinks_being_served: formData.food_drinks_being_served,
|
||||
// Store the itemized_invoice as a string for backward compatibility
|
||||
itemized_invoice: formData.itemized_invoice,
|
||||
// Add these fields explicitly to match the schema
|
||||
needs_graphics: formData.needs_graphics,
|
||||
needs_as_funding: formData.needs_as_funding,
|
||||
// Store the invoice data as a properly formatted JSON object
|
||||
invoice_data: {
|
||||
items: formData.invoiceData.items.map(item => ({
|
||||
|
@ -417,18 +435,30 @@ const EventRequestForm: React.FC = () => {
|
|||
|
||||
// Validate TAP Form Section
|
||||
const validateTAPFormSection = () => {
|
||||
if (!formData.expected_attendance) {
|
||||
toast.error('Please enter the expected attendance');
|
||||
// Verify that all required fields are filled
|
||||
if (!formData.will_or_have_room_booking && formData.will_or_have_room_booking !== false) {
|
||||
toast.error('Please indicate whether you will or have a room booking');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!formData.room_booking && formData.will_or_have_room_booking) {
|
||||
if (!formData.expected_attendance || formData.expected_attendance <= 0) {
|
||||
toast.error('Please enter a valid expected attendance');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.will_or_have_room_booking && !formData.room_booking) {
|
||||
toast.error('Please upload your room booking confirmation');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.food_drinks_being_served === null || formData.food_drinks_being_served === undefined) {
|
||||
toast.error('Please specify if food/drinks will be served');
|
||||
if (!formData.food_drinks_being_served && formData.food_drinks_being_served !== false) {
|
||||
toast.error('Please indicate whether food/drinks will be served');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate AS funding question if food is being served
|
||||
if (formData.food_drinks_being_served && formData.needs_as_funding === undefined) {
|
||||
toast.error('Please indicate whether you need AS funding');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -495,7 +525,18 @@ const EventRequestForm: React.FC = () => {
|
|||
isValid = validateASFundingSection();
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
if (!isValid) {
|
||||
return; // Don't proceed if validation fails
|
||||
}
|
||||
|
||||
// If we're moving from step 4 to step 5
|
||||
if (currentStep === 4 && nextStep === 5) {
|
||||
// If food and drinks aren't being served or if AS funding isn't needed, skip to step 6 (review)
|
||||
if (!formData.food_drinks_being_served || !formData.needs_as_funding) {
|
||||
nextStep = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current step
|
||||
setCurrentStep(nextStep);
|
||||
|
||||
|
@ -507,7 +548,6 @@ const EventRequestForm: React.FC = () => {
|
|||
formReviewed: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submission with validation
|
||||
|
@ -653,55 +693,19 @@ const EventRequestForm: React.FC = () => {
|
|||
variants={containerVariants}
|
||||
className="space-y-6"
|
||||
>
|
||||
{formData.food_drinks_being_served && (
|
||||
<div className="bg-base-200/50 p-6 rounded-lg mb-6">
|
||||
<h3 className="text-xl font-semibold mb-4">Do you need AS funding for this event?</h3>
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<button
|
||||
className={`btn btn-lg ${formData.needs_as_funding ? 'btn-primary' : 'btn-outline'} flex-1`}
|
||||
onClick={() => {
|
||||
setFormData({ ...formData, needs_as_funding: true, as_funding_required: true });
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-lg ${!formData.needs_as_funding && formData.needs_as_funding !== null ? 'btn-primary' : 'btn-outline'} flex-1`}
|
||||
onClick={() => {
|
||||
setFormData({ ...formData, needs_as_funding: false, as_funding_required: false });
|
||||
}}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<h3 className="text-xl font-semibold mb-4">AS Funding Details</h3>
|
||||
<p className="mb-4">Please provide the necessary information for your AS funding request.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!formData.food_drinks_being_served && (
|
||||
<div className="bg-base-200/50 p-6 rounded-lg mb-6">
|
||||
<h3 className="text-xl font-semibold mb-4">AS Funding Information</h3>
|
||||
<p className="mb-4">Since you're not serving food or drinks, AS funding is not applicable for this event.</p>
|
||||
<div className="alert alert-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-5 w-5 stroke-current">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p>If you need to request AS funding for other purposes, please contact the AS office directly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.needs_as_funding && formData.food_drinks_being_served && (
|
||||
<ASFundingSection formData={formData} onDataChange={handleSectionDataChange} />
|
||||
)}
|
||||
|
||||
<div className="flex justify-between mt-8">
|
||||
<button className="btn btn-outline" onClick={() => setCurrentStep(4)}>
|
||||
Back
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={() => handleNextStep(6)}>
|
||||
Review Form
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
@ -730,18 +734,27 @@ const EventRequestForm: React.FC = () => {
|
|||
|
||||
<div className="divider my-6">Ready to Submit?</div>
|
||||
|
||||
<div className="alert alert-info mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-5 w-5 stroke-current">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<p>Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Important Note"
|
||||
message="Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel."
|
||||
icon="heroicons:information-circle"
|
||||
className="mb-6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-8">
|
||||
<button className="btn btn-outline" onClick={() => setCurrentStep(5)}>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
onClick={() => {
|
||||
// Skip the AS Funding section if not needed
|
||||
if (!formData.food_drinks_being_served || !formData.needs_as_funding) {
|
||||
setCurrentStep(4); // Go back to TAP Form section
|
||||
} else {
|
||||
setCurrentStep(5); // Go back to AS Funding section
|
||||
}
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
|
@ -784,21 +797,20 @@ const EventRequestForm: React.FC = () => {
|
|||
}}
|
||||
className="space-y-6"
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
className="alert alert-error shadow-lg"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>{error}</span>
|
||||
<CustomAlert
|
||||
type="error"
|
||||
title="Error"
|
||||
message={error}
|
||||
icon="heroicons:exclamation-triangle"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="w-full mb-6">
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm';
|
|||
import type { InvoiceItem } from './InvoiceBuilder';
|
||||
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||
import { FlyerTypes, LogoOptions, EventRequestStatus } from '../../../schemas/pocketbase';
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
|
||||
// Create a standalone component that can be used to show the preview as a modal
|
||||
export const EventRequestFormPreviewModal: React.FC = () => {
|
||||
|
@ -376,9 +377,13 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
|
|||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="alert alert-info mb-4">
|
||||
<div>No invoice items have been added yet.</div>
|
||||
</div>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Information"
|
||||
message="The actual invoice items will be available to coordinators after submission."
|
||||
icon="heroicons:information-circle"
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-4 mb-4">
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,20 @@ import { motion } from 'framer-motion';
|
|||
import type { EventRequestFormData } from './EventRequestForm';
|
||||
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||
import { FlyerTypes, LogoOptions } from '../../../schemas/pocketbase';
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
|
||||
// Enhanced animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.07,
|
||||
when: "beforeChildren"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Animation variants
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
|
@ -18,6 +30,37 @@ const itemVariants = {
|
|||
}
|
||||
};
|
||||
|
||||
// Input field hover animation
|
||||
const inputHoverVariants = {
|
||||
hover: {
|
||||
scale: 1.01,
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
// Checkbox animation
|
||||
const checkboxVariants = {
|
||||
checked: { scale: 1.05 },
|
||||
unchecked: { scale: 1 },
|
||||
hover: {
|
||||
backgroundColor: "rgba(var(--p), 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
},
|
||||
tap: { scale: 0.95 }
|
||||
};
|
||||
|
||||
// File upload animation
|
||||
const fileUploadVariants = {
|
||||
initial: { scale: 1 },
|
||||
hover: {
|
||||
scale: 1.02,
|
||||
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
},
|
||||
tap: { scale: 0.98 }
|
||||
};
|
||||
|
||||
// Flyer type options
|
||||
const FLYER_TYPES = [
|
||||
{ value: FlyerTypes.DIGITAL_WITH_SOCIAL, label: 'Digital flyer (with social media advertising: Facebook, Instagram, Discord)' },
|
||||
|
@ -55,6 +98,7 @@ interface PRSectionProps {
|
|||
|
||||
const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
|
||||
const [otherLogoFiles, setOtherLogoFiles] = useState<File[]>(formData.other_logos || []);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// Handle checkbox change for flyer types
|
||||
const handleFlyerTypeChange = (type: string) => {
|
||||
|
@ -83,48 +127,107 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
|
|||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-primary">PR Materials</h2>
|
||||
// Handle drag events for file upload
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
<div className="bg-base-300/50 p-4 rounded-lg mb-6">
|
||||
<p className="text-sm">
|
||||
If you need PR Materials, please don't forget that this form MUST be submitted at least 6 weeks in advance even if you aren't requesting AS funding or physical flyers. Also, please remember to ping PR in #-events on Slack once you've submitted this form.
|
||||
</p>
|
||||
</div>
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const newFiles = Array.from(e.dataTransfer.files) as File[];
|
||||
setOtherLogoFiles(newFiles);
|
||||
onDataChange({ other_logos: newFiles });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
|
||||
PR Materials
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Important Timeline"
|
||||
message="If you need PR Materials, please don't forget that this form MUST be submitted at least 6 weeks in advance even if you aren't requesting AS funding or physical flyers. Also, please remember to ping PR in #-events on Slack once you've submitted this form."
|
||||
className="mb-6"
|
||||
icon="heroicons:clock"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Type of material needed */}
|
||||
<motion.div variants={itemVariants} className="form-control bg-base-200/50 p-4 rounded-lg">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">Type of material needed?</span>
|
||||
<span className="label-text font-medium text-lg">Type of Material Needed</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="space-y-2 mt-2">
|
||||
|
||||
<div className="space-y-3 mt-3">
|
||||
{FLYER_TYPES.map((type) => (
|
||||
<label key={type.value} className="flex items-start gap-2 cursor-pointer hover:bg-base-300/30 p-2 rounded-md transition-colors">
|
||||
<motion.label
|
||||
key={type.value}
|
||||
className={`flex items-start gap-3 cursor-pointer p-3 rounded-lg transition-colors ${formData.flyer_type.includes(type.value)
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'hover:bg-base-300/30'
|
||||
}`}
|
||||
initial="unchecked"
|
||||
animate={formData.flyer_type.includes(type.value) ? "checked" : "unchecked"}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={checkboxVariants}
|
||||
style={{ margin: '4px' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary mt-1"
|
||||
checked={formData.flyer_type.includes(type.value)}
|
||||
onChange={() => handleFlyerTypeChange(type.value)}
|
||||
/>
|
||||
<span>{type.label}</span>
|
||||
</label>
|
||||
<span className="font-medium">{type.label}</span>
|
||||
</motion.label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Other flyer type input */}
|
||||
{formData.flyer_type.includes(FlyerTypes.OTHER) && (
|
||||
<div className="mt-3 pl-7">
|
||||
<input
|
||||
<motion.div
|
||||
className="mt-4 pl-8"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<motion.input
|
||||
type="text"
|
||||
className="input input-bordered w-full"
|
||||
className="input input-bordered input-primary w-full"
|
||||
placeholder="Please specify other material needed"
|
||||
value={formData.other_flyer_type}
|
||||
onChange={(e) => onDataChange({ other_flyer_type: e.target.value })}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
|
@ -134,110 +237,209 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
|
|||
type === FlyerTypes.PHYSICAL_WITH_ADVERTISING ||
|
||||
type === FlyerTypes.NEWSLETTER
|
||||
) && (
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">When do you need us to start advertising?</span>
|
||||
<span className="label-text font-medium text-lg">Advertising Start Date</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<input
|
||||
<p className="text-sm text-gray-500 mb-3">When do you need us to start advertising?</p>
|
||||
|
||||
<motion.input
|
||||
type="date"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
|
||||
value={formData.flyer_advertising_start_date}
|
||||
onChange={(e) => onDataChange({ flyer_advertising_start_date: e.target.value })}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Logos Required */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Logos Required</span>
|
||||
<span className="label-text font-medium text-lg">Logos Required</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 mt-3">
|
||||
{LOGO_OPTIONS.map((logo) => (
|
||||
<label key={logo.value} className="flex items-start gap-2 cursor-pointer">
|
||||
<motion.label
|
||||
key={logo.value}
|
||||
className={`flex items-start gap-3 cursor-pointer p-3 rounded-lg transition-colors ${formData.required_logos.includes(logo.value)
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'hover:bg-base-300/30'
|
||||
}`}
|
||||
initial="unchecked"
|
||||
animate={formData.required_logos.includes(logo.value) ? "checked" : "unchecked"}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
variants={checkboxVariants}
|
||||
style={{ margin: '4px' }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-primary mt-1"
|
||||
checked={formData.required_logos.includes(logo.value)}
|
||||
onChange={() => handleLogoChange(logo.value)}
|
||||
/>
|
||||
<span>{logo.label}</span>
|
||||
</label>
|
||||
<span className="font-medium">{logo.label}</span>
|
||||
</motion.label>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Logo file upload */}
|
||||
{formData.required_logos.includes(LogoOptions.OTHER) && (
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Please share your logo files here</span>
|
||||
<span className="label-text font-medium text-lg">Logo Files</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
|
||||
<motion.div
|
||||
className={`mt-2 border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? 'border-primary bg-primary/5' : 'border-base-300 hover:border-primary/50'
|
||||
}`}
|
||||
variants={fileUploadVariants}
|
||||
initial="initial"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById('logo-files')?.click()}
|
||||
>
|
||||
<input
|
||||
id="logo-files"
|
||||
type="file"
|
||||
className="file-input file-input-bordered w-full file-input-primary hover:file-input-ghost transition-all duration-300"
|
||||
className="hidden"
|
||||
onChange={handleLogoFileChange}
|
||||
accept="image/*"
|
||||
multiple
|
||||
required
|
||||
/>
|
||||
{otherLogoFiles.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm font-medium mb-1">Selected files:</p>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<motion.div
|
||||
className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary"
|
||||
whileHover={{ rotate: 15, scale: 1.1 }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
|
||||
{otherLogoFiles.length > 0 ? (
|
||||
<>
|
||||
<p className="font-medium text-primary">{otherLogoFiles.length} file(s) selected:</p>
|
||||
<div className="max-h-24 overflow-y-auto text-left w-full">
|
||||
<ul className="list-disc list-inside text-sm">
|
||||
{otherLogoFiles.map((file, index) => (
|
||||
<li key={index}>{file.name}</li>
|
||||
<li key={index} className="truncate">{file.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Click or drag to replace</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="font-medium">Drop your logo files here or click to browse</p>
|
||||
<p className="text-xs text-gray-500">Please upload transparent logo files (PNG preferred)</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Format */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">What format do you need it to be in?</span>
|
||||
<span className="label-text font-medium text-lg">Required Format</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<select
|
||||
<p className="text-sm text-gray-500 mb-3">What format do you need the materials to be in?</p>
|
||||
|
||||
<motion.select
|
||||
className="select select-bordered focus:select-primary transition-all duration-300"
|
||||
value={formData.advertising_format}
|
||||
onChange={(e) => onDataChange({ advertising_format: e.target.value })}
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
>
|
||||
<option value="">Select format</option>
|
||||
{FORMAT_OPTIONS.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</motion.select>
|
||||
</motion.div>
|
||||
|
||||
{/* Additional specifications */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Any other specifications and requests?</span>
|
||||
<span className="label-text font-medium text-lg">Additional Specifications</span>
|
||||
</label>
|
||||
<textarea
|
||||
<p className="text-sm text-gray-500 mb-3">Any other specifications and requests?</p>
|
||||
|
||||
<motion.textarea
|
||||
className="textarea textarea-bordered focus:textarea-primary transition-all duration-300 min-h-[120px]"
|
||||
value={formData.flyer_additional_requests}
|
||||
onChange={(e) => onDataChange({ flyer_additional_requests: e.target.value })}
|
||||
placeholder="Color scheme, overall design, examples to consider, etc."
|
||||
rows={4}
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Photography Needed */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Photography Needed?</span>
|
||||
<span className="label-text font-medium text-lg">Photography</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<p className="text-sm text-gray-500 mb-3">Do you need photography for your event?</p>
|
||||
|
||||
<div className="flex gap-6 mt-2">
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.photography_needed === true
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -245,9 +447,17 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
|
|||
onChange={() => onDataChange({ photography_needed: true })}
|
||||
required
|
||||
/>
|
||||
<span>Yes</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<span className="font-medium">Yes, we need photography</span>
|
||||
</motion.label>
|
||||
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.photography_needed === false
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -255,11 +465,11 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
|
|||
onChange={() => onDataChange({ photography_needed: false })}
|
||||
required
|
||||
/>
|
||||
<span>No</span>
|
||||
</label>
|
||||
<span className="font-medium">No, we don't need photography</span>
|
||||
</motion.label>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,8 +2,20 @@ import React, { useState } from 'react';
|
|||
import { motion } from 'framer-motion';
|
||||
import type { EventRequestFormData } from './EventRequestForm';
|
||||
import type { EventRequest } from '../../../schemas/pocketbase';
|
||||
import CustomAlert from '../universal/CustomAlert';
|
||||
|
||||
// Enhanced animation variants
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
when: "beforeChildren"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Animation variants
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
|
@ -17,6 +29,26 @@ const itemVariants = {
|
|||
}
|
||||
};
|
||||
|
||||
// Input field hover animation
|
||||
const inputHoverVariants = {
|
||||
hover: {
|
||||
scale: 1.01,
|
||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
// File upload animation
|
||||
const fileUploadVariants = {
|
||||
initial: { scale: 1 },
|
||||
hover: {
|
||||
scale: 1.02,
|
||||
boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)",
|
||||
transition: { duration: 0.2 }
|
||||
},
|
||||
tap: { scale: 0.98 }
|
||||
};
|
||||
|
||||
interface TAPFormSectionProps {
|
||||
formData: EventRequestFormData;
|
||||
onDataChange: (data: Partial<EventRequestFormData>) => void;
|
||||
|
@ -24,6 +56,7 @@ interface TAPFormSectionProps {
|
|||
|
||||
const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => {
|
||||
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// Handle room booking file upload
|
||||
const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -34,75 +67,169 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
|
|||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-primary">TAP Form Information</h2>
|
||||
// Handle drag events for file upload
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
<div className="bg-base-300/50 p-4 rounded-lg mb-6">
|
||||
<p className="text-sm">
|
||||
Please ensure you have ALL sections completed. If something is not available, let the coordinators know and be advised on how to proceed.
|
||||
</p>
|
||||
</div>
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
const file = e.dataTransfer.files[0];
|
||||
setRoomBookingFile(file);
|
||||
onDataChange({ room_booking: file });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
|
||||
TAP Form Information
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title="Important Information"
|
||||
message="Please ensure you have ALL sections completed. If something is not available, let the coordinators know and be advised on how to proceed."
|
||||
className="mb-6"
|
||||
icon="heroicons:information-circle"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Expected attendance */}
|
||||
<motion.div variants={itemVariants} className="form-control bg-base-200/50 p-4 rounded-lg">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">Expected attendance? Include a number NOT a range please.</span>
|
||||
<span className="label-text font-medium text-lg">Expected Attendance</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="relative mt-2">
|
||||
<input
|
||||
<motion.input
|
||||
type="number"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 w-full"
|
||||
className="input input-bordered focus:input-primary transition-all duration-300 w-full pr-20"
|
||||
value={formData.expected_attendance || ''}
|
||||
onChange={(e) => onDataChange({ expected_attendance: parseInt(e.target.value) || 0 })}
|
||||
min="0"
|
||||
placeholder="Enter expected attendance"
|
||||
required
|
||||
whileHover="hover"
|
||||
variants={inputHoverVariants}
|
||||
/>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-sm text-gray-400">
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-sm text-gray-400 bg-base-100 px-2 py-1 rounded">
|
||||
people
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-2">
|
||||
<p>PROGRAMMING FUNDS EVENTS FUNDED BY PROGRAMMING FUNDS MAY ONLY ADMIT UC SAN DIEGO STUDENTS, STAFF OR FACULTY AS GUESTS.</p>
|
||||
<p>ONLY UC SAN DIEGO UNDERGRADUATE STUDENTS MAY RECEIVE ITEMS FUNDED BY THE ASSOCIATED STUDENTS.</p>
|
||||
<p>EVENT FUNDING IS GRANTED UP TO A MAXIMUM OF $10.00 PER EXPECTED STUDENT ATTENDEE AND $5,000 PER EVENT</p>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="mt-4 p-4 bg-base-300/30 rounded-lg text-xs text-gray-400"
|
||||
initial={{ opacity: 0.8 }}
|
||||
whileHover={{ opacity: 1, scale: 1.01 }}
|
||||
>
|
||||
<ul className="space-y-2 list-disc list-inside">
|
||||
<li>PROGRAMMING FUNDS EVENTS FUNDED BY PROGRAMMING FUNDS MAY ONLY ADMIT UC SAN DIEGO STUDENTS, STAFF OR FACULTY AS GUESTS.</li>
|
||||
<li>ONLY UC SAN DIEGO UNDERGRADUATE STUDENTS MAY RECEIVE ITEMS FUNDED BY THE ASSOCIATED STUDENTS.</li>
|
||||
<li>EVENT FUNDING IS GRANTED UP TO A MAXIMUM OF $10.00 PER EXPECTED STUDENT ATTENDEE AND $5,000 PER EVENT</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Room booking confirmation */}
|
||||
<motion.div variants={itemVariants} className="form-control bg-base-200/50 p-4 rounded-lg">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">Room booking confirmation</span>
|
||||
<span className="label-text font-medium text-lg">Room Booking Confirmation</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
|
||||
<motion.div
|
||||
className={`mt-2 border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${isDragging ? 'border-primary bg-primary/5' : 'border-base-300 hover:border-primary/50'
|
||||
}`}
|
||||
variants={fileUploadVariants}
|
||||
initial="initial"
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => document.getElementById('room-booking-file')?.click()}
|
||||
>
|
||||
<input
|
||||
id="room-booking-file"
|
||||
type="file"
|
||||
className="file-input file-input-bordered file-input-primary w-full"
|
||||
className="hidden"
|
||||
onChange={handleRoomBookingFileChange}
|
||||
accept=".pdf,.png,.jpg,.jpeg"
|
||||
/>
|
||||
{roomBookingFile && (
|
||||
<p className="text-sm mt-2">
|
||||
Selected file: {roomBookingFile.name}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<motion.div
|
||||
className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center text-primary"
|
||||
whileHover={{ rotate: 15, scale: 1.1 }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
|
||||
{roomBookingFile ? (
|
||||
<>
|
||||
<p className="font-medium text-primary">File selected:</p>
|
||||
<p className="text-sm">{roomBookingFile.name}</p>
|
||||
<p className="text-xs text-gray-500">Click or drag to replace</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="font-medium">Drop your file here or click to browse</p>
|
||||
<p className="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG</p>
|
||||
</>
|
||||
)}
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Please upload a screenshot of your room booking confirmation. Accepted formats: PDF, PNG, JPG.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Food/Drinks */}
|
||||
<motion.div variants={itemVariants} className="form-control">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">Will you be serving food/drinks at your event?</span>
|
||||
<span className="label-text font-medium text-lg">Food & Drinks</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<p className="text-sm text-gray-500 mb-3">Will you be serving food or drinks at your event?</p>
|
||||
|
||||
<div className="flex gap-6 mt-2">
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.food_drinks_being_served === true
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -110,9 +237,17 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
|
|||
onChange={() => onDataChange({ food_drinks_being_served: true })}
|
||||
required
|
||||
/>
|
||||
<span>Yes</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<span className="font-medium">Yes, we'll have food/drinks</span>
|
||||
</motion.label>
|
||||
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.food_drinks_being_served === false
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
|
@ -120,31 +255,93 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
|
|||
onChange={() => onDataChange({ food_drinks_being_served: false })}
|
||||
required
|
||||
/>
|
||||
<span>No</span>
|
||||
</label>
|
||||
<span className="font-medium">No, no food/drinks</span>
|
||||
</motion.label>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* AS Funding Notice - only show if food/drinks are being served */}
|
||||
{/* AS Funding Question - only show if food/drinks are being served */}
|
||||
{formData.food_drinks_being_served && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
className="alert alert-info"
|
||||
variants={itemVariants}
|
||||
className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
|
||||
whileHover={{ y: -2 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-5 w-5 stroke-current">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 className="font-bold">Food and Drinks Information</h3>
|
||||
<div className="text-xs">
|
||||
If you're serving food or drinks, you'll be asked about AS funding in the next step. Please be prepared with vendor information and invoice details.
|
||||
</div>
|
||||
<label className="label">
|
||||
<span className="label-text font-medium text-lg">AS Funding Request</span>
|
||||
<span className="label-text-alt text-error">*</span>
|
||||
</label>
|
||||
<p className="text-sm text-gray-500 mb-3">Do you need funding from AS?</p>
|
||||
|
||||
<div className="flex gap-6 mt-2">
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.needs_as_funding === true
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
checked={formData.needs_as_funding === true}
|
||||
onChange={() => onDataChange({
|
||||
needs_as_funding: true,
|
||||
as_funding_required: true
|
||||
})}
|
||||
required
|
||||
/>
|
||||
<span className="font-medium">Yes, we need AS funding</span>
|
||||
</motion.label>
|
||||
|
||||
<motion.label
|
||||
className={`flex items-center gap-3 cursor-pointer p-4 rounded-lg transition-colors ${formData.needs_as_funding === false
|
||||
? 'bg-primary/20 border border-primary/50'
|
||||
: 'bg-base-100 hover:bg-primary/10'
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
checked={formData.needs_as_funding === false}
|
||||
onChange={() => onDataChange({
|
||||
needs_as_funding: false,
|
||||
as_funding_required: false
|
||||
})}
|
||||
required
|
||||
/>
|
||||
<span className="font-medium">No, we don't need funding</span>
|
||||
</motion.label>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Single information alert container that changes content based on selection */}
|
||||
{formData.food_drinks_being_served && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
className="mb-4"
|
||||
>
|
||||
<CustomAlert
|
||||
type="info"
|
||||
title={formData.needs_as_funding ? "AS Funding Information" : "Food and Drinks Information"}
|
||||
message={formData.needs_as_funding
|
||||
? "In the next step, you'll be asked to provide vendor information and invoice details for your AS funding request."
|
||||
: "Please make sure to follow all campus policies regarding food and drinks at your event."}
|
||||
className="mb-4"
|
||||
icon={formData.needs_as_funding ? "heroicons:currency-dollar" : "heroicons:cake"}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue