improve event request form styling

This commit is contained in:
chark1es 2025-03-11 05:55:52 -07:00
parent 56ddba112a
commit 97ce397281
7 changed files with 1692 additions and 1027 deletions

View file

@ -3,418 +3,437 @@ import { motion } from 'framer-motion';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequestFormData } from './EventRequestForm';
import InvoiceBuilder from './InvoiceBuilder'; import InvoiceBuilder from './InvoiceBuilder';
import type { InvoiceData } from './InvoiceBuilder'; import type { InvoiceData, InvoiceItem } from './InvoiceBuilder';
import CustomAlert from '../universal/CustomAlert'; 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 = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 10 },
visible: { visible: {
opacity: 1, opacity: 1,
y: 0, y: 0,
transition: { transition: {
type: "spring", type: "spring",
stiffness: 300, stiffness: 500,
damping: 24 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 { interface ASFundingSectionProps {
formData: EventRequestFormData; formData: EventRequestFormData;
onDataChange: (data: Partial<EventRequestFormData>) => void; onDataChange: (data: Partial<EventRequestFormData>) => void;
} }
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataChange }) => { const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataChange }) => {
const [invoiceFile, setInvoiceFile] = useState<File | null>(formData.invoice);
const [invoiceFiles, setInvoiceFiles] = useState<File[]>(formData.invoice_files || []); const [invoiceFiles, setInvoiceFiles] = useState<File[]>(formData.invoice_files || []);
const [useJsonInput, setUseJsonInput] = useState(false); const [jsonInput, setJsonInput] = useState<string>('');
const [jsonInput, setJsonInput] = useState(''); const [jsonError, setJsonError] = useState<string>('');
const [jsonError, setJsonError] = useState(''); const [showJsonInput, setShowJsonInput] = useState<boolean>(false);
const [showExample, setShowExample] = useState(false); const [isDragging, setIsDragging] = useState(false);
// Example JSON for the user to reference // Handle invoice file upload
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)
const handleInvoiceFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInvoiceFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]; const newFiles = Array.from(e.target.files) as File[];
setInvoiceFile(file); setInvoiceFiles(newFiles);
onDataChange({ invoice: file }); 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 // Handle JSON input change
const handleJsonInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleJsonInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value; setJsonInput(e.target.value);
setJsonInput(value);
// Validate JSON as user types
if (value.trim()) {
try {
JSON.parse(value);
setJsonError(''); setJsonError('');
} catch (err) {
setJsonError('Invalid JSON format. Please check your syntax.');
}
} else {
setJsonError('');
}
}; };
// Show JSON example // Show JSON example
const showJsonExample = () => { const showJsonExample = () => {
// Toggle example visibility const example = {
setShowExample(!showExample); vendor: "Example Restaurant",
items: [
// If showing example, populate the textarea with the example JSON {
if (!showExample) { id: "item-1",
setJsonInput(JSON.stringify(jsonExample, null, 2)); 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 // Validate and apply JSON
const validateAndApplyJson = () => { const validateAndApplyJson = () => {
try { try {
// Parse the JSON input if (!jsonInput.trim()) {
const parsedJson = JSON.parse(jsonInput); setJsonError('JSON input is empty');
// 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);
return; return;
} }
// Calculate subtotal and total const data = JSON.parse(jsonInput);
const subtotal = parsedJson.items.reduce((sum: number, item: any) => sum + (item.quantity * item.unit_price), 0);
const total = subtotal + parsedJson.tax + parsedJson.tip;
// Convert the JSON to the format expected by InvoiceData // Validate structure
const invoiceData: InvoiceData = { if (!data.vendor) {
items: parsedJson.items.map((item: any, index: number) => ({ setJsonError('Vendor field is required');
id: `item-${index + 1}`, return;
description: item.item, }
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, quantity: item.quantity,
unitPrice: item.unit_price, unit_price: item.unitPrice,
amount: item.quantity * item.unit_price amount: item.amount
})), })),
subtotal: subtotal, subtotal: data.subtotal,
taxRate: subtotal ? (parsedJson.tax / subtotal) * 100 : 0, tax: data.taxAmount,
taxAmount: parsedJson.tax, tip: data.tipAmount,
tipPercentage: subtotal ? (parsedJson.tip / subtotal) * 100 : 0, total: data.total
tipAmount: parsedJson.tip, }, null, 2);
total: total,
vendor: parsedJson.vendor
};
// Update the form data // Apply the JSON data to the form
handleInvoiceDataChange(invoiceData); onDataChange({
invoiceData: data,
itemized_invoice: itemizedInvoice,
as_funding_required: true
});
// Update the itemized_invoice field with the complete JSON including calculated total toast.success('Invoice data applied successfully');
const completeJson = { setShowJsonInput(false);
...parsedJson, } catch (error) {
subtotal: subtotal, setJsonError('Invalid JSON format: ' + (error as Error).message);
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.');
} }
}; };
// 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 ( return (
<div className="space-y-6"> <motion.div
<h2 className="text-2xl font-bold mb-4 text-primary">AS Funding Information</h2> className="space-y-8"
initial="hidden"
<CustomAlert animate="visible"
type="warning" variants={containerVariants}
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}
> >
See example <motion.div variants={itemVariants}>
</button> <h2 className="text-3xl font-bold mb-4 text-primary bg-gradient-to-r from-primary to-primary-focus bg-clip-text text-transparent">
</div> AS Funding Details
</span> </h2>
</label> <p className="text-lg text-base-content/80 mb-6">
<textarea Please provide the necessary information for your Associated Students funding request.
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)
</p> </p>
</motion.div> </motion.div>
<motion.div variants={itemVariants}>
<CustomAlert <CustomAlert
type="warning" 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." 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" className="mb-4"
icon="heroicons:clock" 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> </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>
); );
}; };

View file

@ -2,8 +2,20 @@ import React from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; 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 = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { 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 { interface EventDetailsSectionProps {
formData: EventRequestFormData; formData: EventRequestFormData;
onDataChange: (data: Partial<EventRequestFormData>) => void; onDataChange: (data: Partial<EventRequestFormData>) => void;
@ -24,101 +45,156 @@ interface EventDetailsSectionProps {
const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onDataChange }) => { const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onDataChange }) => {
return ( return (
<div className="space-y-6"> <motion.div
<h2 className="text-2xl font-bold mb-4 text-primary">Event Details</h2> 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"> <motion.div variants={itemVariants}>
<p className="text-sm"> <CustomAlert
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. type="info"
</p> title="Coordinator Notification"
</div> 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 */} {/* 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"> <label className="label">
<span className="label-text font-medium text-lg">Event Name</span> <span className="label-text font-medium text-lg">Event Name</span>
<span className="label-text-alt text-error">*</span> <span className="label-text-alt text-error">*</span>
</label> </label>
<input <motion.input
type="text" type="text"
className="input input-bordered focus:input-primary transition-all duration-300 mt-2" className="input input-bordered focus:input-primary transition-all duration-300 mt-2"
value={formData.name} value={formData.name}
onChange={(e) => onDataChange({ name: e.target.value })} onChange={(e) => onDataChange({ name: e.target.value })}
placeholder="Enter event name" placeholder="Enter event name"
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
{/* Event Description */} {/* 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"> <label className="label">
<span className="label-text font-medium text-lg">Event Description</span> <span className="label-text font-medium text-lg">Event Description</span>
<span className="label-text-alt text-error">*</span> <span className="label-text-alt text-error">*</span>
</label> </label>
<textarea <motion.textarea
className="textarea textarea-bordered focus:textarea-primary transition-all duration-300 min-h-[120px] mt-2" className="textarea textarea-bordered focus:textarea-primary transition-all duration-300 min-h-[120px] mt-2"
value={formData.event_description} value={formData.event_description}
onChange={(e) => onDataChange({ event_description: e.target.value })} onChange={(e) => onDataChange({ event_description: e.target.value })}
placeholder="Provide a detailed description of your event" placeholder="Provide a detailed description of your event"
rows={4} rows={4}
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
{/* Date and Time Section */}
<motion.div
variants={itemVariants}
className="grid grid-cols-1 md:grid-cols-2 gap-6"
>
{/* Event Start Date */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<input <motion.input
type="datetime-local" 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} value={formData.start_date_time}
onChange={(e) => onDataChange({ start_date_time: e.target.value })} onChange={(e) => onDataChange({ start_date_time: e.target.value })}
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
{/* Event End Date */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<input <motion.input
type="datetime-local" 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} value={formData.end_date_time}
onChange={(e) => onDataChange({ end_date_time: e.target.value })} onChange={(e) => onDataChange({ end_date_time: e.target.value })}
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
</motion.div>
{/* Event Location */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<input <motion.input
type="text" 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} value={formData.location}
onChange={(e) => onDataChange({ location: e.target.value })} onChange={(e) => onDataChange({ location: e.target.value })}
placeholder="Enter event location" placeholder="Enter event location"
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
{/* Room Booking */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="flex gap-4"> <div className="flex gap-6 mt-2">
<label className="flex items-center gap-2 cursor-pointer"> <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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -126,9 +202,13 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
onChange={() => onDataChange({ will_or_have_room_booking: true })} onChange={() => onDataChange({ will_or_have_room_booking: true })}
required required
/> />
<span>Yes</span> <span className="font-medium">Yes, I have/will have a booking</span>
</label> </motion.label>
<label className="flex items-center gap-2 cursor-pointer"> <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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -136,11 +216,11 @@ const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ formData, onD
onChange={() => onDataChange({ will_or_have_room_booking: false })} onChange={() => onDataChange({ will_or_have_room_booking: false })}
required required
/> />
<span>No</span> <span className="font-medium">No, I don't need a booking</span>
</label> </motion.label>
</div> </div>
</motion.div> </motion.div>
</div> </motion.div>
); );
}; };

View file

@ -82,6 +82,9 @@ export interface EventRequestFormData {
formReviewed?: boolean; // Track if the form has been reviewed formReviewed?: boolean; // Track if the form has been reviewed
} }
// Add CustomAlert import
import CustomAlert from '../universal/CustomAlert';
const EventRequestForm: React.FC = () => { const EventRequestForm: React.FC = () => {
const [currentStep, setCurrentStep] = useState<number>(1); const [currentStep, setCurrentStep] = useState<number>(1);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
@ -164,10 +167,22 @@ const EventRequestForm: React.FC = () => {
// Handle form section data changes // Handle form section data changes
const handleSectionDataChange = (sectionData: Partial<EventRequestFormData>) => { const handleSectionDataChange = (sectionData: Partial<EventRequestFormData>) => {
setFormData(prevData => ({ // Ensure both needs_graphics and flyers_needed are synchronized
...prevData, if (sectionData.flyers_needed !== undefined && sectionData.needs_graphics === undefined) {
...sectionData 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 // 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(), end_date_time: new Date(formData.end_date_time).toISOString(),
event_description: formData.event_description, event_description: formData.event_description,
flyers_needed: formData.flyers_needed, 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, flyer_type: formData.flyer_type,
other_flyer_type: formData.other_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_advertising_start_date: formData.flyer_advertising_start_date ? new Date(formData.flyer_advertising_start_date).toISOString() : '',
flyer_additional_requests: formData.flyer_additional_requests, flyer_additional_requests: formData.flyer_additional_requests,
photography_needed: formData.photography_needed,
required_logos: formData.required_logos, required_logos: formData.required_logos,
advertising_format: formData.advertising_format, advertising_format: formData.advertising_format,
will_or_have_room_booking: formData.will_or_have_room_booking, will_or_have_room_booking: formData.will_or_have_room_booking,
expected_attendance: formData.expected_attendance, expected_attendance: formData.expected_attendance,
as_funding_required: formData.as_funding_required, // Add these fields explicitly to match the schema
food_drinks_being_served: formData.food_drinks_being_served, needs_graphics: formData.needs_graphics,
// Store the itemized_invoice as a string for backward compatibility needs_as_funding: formData.needs_as_funding,
itemized_invoice: formData.itemized_invoice,
// Store the invoice data as a properly formatted JSON object // Store the invoice data as a properly formatted JSON object
invoice_data: { invoice_data: {
items: formData.invoiceData.items.map(item => ({ items: formData.invoiceData.items.map(item => ({
@ -417,18 +435,30 @@ const EventRequestForm: React.FC = () => {
// Validate TAP Form Section // Validate TAP Form Section
const validateTAPFormSection = () => { const validateTAPFormSection = () => {
if (!formData.expected_attendance) { // Verify that all required fields are filled
toast.error('Please enter the expected attendance'); 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; 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'); toast.error('Please upload your room booking confirmation');
return false; return false;
} }
if (formData.food_drinks_being_served === null || formData.food_drinks_being_served === undefined) { if (!formData.food_drinks_being_served && formData.food_drinks_being_served !== false) {
toast.error('Please specify if food/drinks will be served'); 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; return false;
} }
@ -495,7 +525,18 @@ const EventRequestForm: React.FC = () => {
isValid = validateASFundingSection(); 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 // Set the current step
setCurrentStep(nextStep); setCurrentStep(nextStep);
@ -507,7 +548,6 @@ const EventRequestForm: React.FC = () => {
formReviewed: true formReviewed: true
})); }));
} }
}
}; };
// Handle form submission with validation // Handle form submission with validation
@ -653,55 +693,19 @@ const EventRequestForm: React.FC = () => {
variants={containerVariants} variants={containerVariants}
className="space-y-6" className="space-y-6"
> >
{formData.food_drinks_being_served && (
<div className="bg-base-200/50 p-6 rounded-lg mb-6"> <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> <h3 className="text-xl font-semibold mb-4">AS Funding Details</h3>
<div className="flex flex-col sm:flex-row gap-4"> <p className="mb-4">Please provide the necessary information for your AS funding request.</p>
<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>
</div> </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} /> <ASFundingSection formData={formData} onDataChange={handleSectionDataChange} />
)}
<div className="flex justify-between mt-8"> <div className="flex justify-between mt-8">
<button className="btn btn-outline" onClick={() => setCurrentStep(4)}> <button className="btn btn-outline" onClick={() => setCurrentStep(4)}>
Back Back
</button> </button>
<button className="btn btn-primary" onClick={() => handleNextStep(6)}> <button className="btn btn-primary" onClick={() => handleNextStep(6)}>
Review Form Next
</button> </button>
</div> </div>
</motion.div> </motion.div>
@ -730,18 +734,27 @@ const EventRequestForm: React.FC = () => {
<div className="divider my-6">Ready to Submit?</div> <div className="divider my-6">Ready to Submit?</div>
<div className="alert alert-info mb-6"> <CustomAlert
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-5 w-5 stroke-current"> type="info"
<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> title="Important Note"
</svg> message="Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel."
<div> icon="heroicons:information-circle"
<p>Once submitted, you'll need to notify PR and/or Coordinators in the #-events Slack channel.</p> className="mb-6"
</div> />
</div>
</div> </div>
<div className="flex justify-between mt-8"> <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 Back
</button> </button>
<button <button
@ -784,21 +797,20 @@ const EventRequestForm: React.FC = () => {
}} }}
className="space-y-6" className="space-y-6"
> >
<AnimatePresence mode="wait">
{error && ( {error && (
<motion.div <motion.div
initial={{ opacity: 0, y: -20, scale: 0.95 }} initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }} 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"> <CustomAlert
<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" /> type="error"
</svg> title="Error"
<span>{error}</span> message={error}
icon="heroicons:exclamation-triangle"
/>
</motion.div> </motion.div>
)} )}
</AnimatePresence>
{/* Progress indicator */} {/* Progress indicator */}
<div className="w-full mb-6"> <div className="w-full mb-6">

View file

@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm';
import type { InvoiceItem } from './InvoiceBuilder'; import type { InvoiceItem } from './InvoiceBuilder';
import type { EventRequest } from '../../../schemas/pocketbase'; import type { EventRequest } from '../../../schemas/pocketbase';
import { FlyerTypes, LogoOptions, EventRequestStatus } 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 // Create a standalone component that can be used to show the preview as a modal
export const EventRequestFormPreviewModal: React.FC = () => { export const EventRequestFormPreviewModal: React.FC = () => {
@ -376,9 +377,13 @@ const EventRequestFormPreview: React.FC<EventRequestFormPreviewProps> = ({
</table> </table>
</div> </div>
) : ( ) : (
<div className="alert alert-info mb-4"> <CustomAlert
<div>No invoice items have been added yet.</div> type="info"
</div> 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"> <div className="mt-4 mb-4">

View file

@ -3,8 +3,20 @@ import { motion } from 'framer-motion';
import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; import type { EventRequest } from '../../../schemas/pocketbase';
import { FlyerTypes, LogoOptions } 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 = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { 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 // Flyer type options
const FLYER_TYPES = [ const FLYER_TYPES = [
{ value: FlyerTypes.DIGITAL_WITH_SOCIAL, label: 'Digital flyer (with social media advertising: Facebook, Instagram, Discord)' }, { 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 PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
const [otherLogoFiles, setOtherLogoFiles] = useState<File[]>(formData.other_logos || []); const [otherLogoFiles, setOtherLogoFiles] = useState<File[]>(formData.other_logos || []);
const [isDragging, setIsDragging] = useState(false);
// Handle checkbox change for flyer types // Handle checkbox change for flyer types
const handleFlyerTypeChange = (type: string) => { const handleFlyerTypeChange = (type: string) => {
@ -83,48 +127,107 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
} }
}; };
return ( // Handle drag events for file upload
<div className="space-y-6"> const handleDragOver = (e: React.DragEvent) => {
<h2 className="text-2xl font-bold mb-4 text-primary">PR Materials</h2> e.preventDefault();
setIsDragging(true);
};
<div className="bg-base-300/50 p-4 rounded-lg mb-6"> const handleDragLeave = (e: React.DragEvent) => {
<p className="text-sm"> e.preventDefault();
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. setIsDragging(false);
</p> };
</div>
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 */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="space-y-2 mt-2">
<div className="space-y-3 mt-3">
{FLYER_TYPES.map((type) => ( {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 <input
type="checkbox" type="checkbox"
className="checkbox checkbox-primary mt-1" className="checkbox checkbox-primary mt-1"
checked={formData.flyer_type.includes(type.value)} checked={formData.flyer_type.includes(type.value)}
onChange={() => handleFlyerTypeChange(type.value)} onChange={() => handleFlyerTypeChange(type.value)}
/> />
<span>{type.label}</span> <span className="font-medium">{type.label}</span>
</label> </motion.label>
))} ))}
</div> </div>
{/* Other flyer type input */} {/* Other flyer type input */}
{formData.flyer_type.includes(FlyerTypes.OTHER) && ( {formData.flyer_type.includes(FlyerTypes.OTHER) && (
<div className="mt-3 pl-7"> <motion.div
<input className="mt-4 pl-8"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<motion.input
type="text" type="text"
className="input input-bordered w-full" className="input input-bordered input-primary w-full"
placeholder="Please specify other material needed" placeholder="Please specify other material needed"
value={formData.other_flyer_type} value={formData.other_flyer_type}
onChange={(e) => onDataChange({ other_flyer_type: e.target.value })} onChange={(e) => onDataChange({ other_flyer_type: e.target.value })}
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</div> </motion.div>
)} )}
</motion.div> </motion.div>
@ -134,110 +237,209 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
type === FlyerTypes.PHYSICAL_WITH_ADVERTISING || type === FlyerTypes.PHYSICAL_WITH_ADVERTISING ||
type === FlyerTypes.NEWSLETTER 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<input <p className="text-sm text-gray-500 mb-3">When do you need us to start advertising?</p>
<motion.input
type="date" 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} value={formData.flyer_advertising_start_date}
onChange={(e) => onDataChange({ flyer_advertising_start_date: e.target.value })} onChange={(e) => onDataChange({ flyer_advertising_start_date: e.target.value })}
required required
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
)} )}
{/* Logos Required */} {/* 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"> <label className="label">
<span className="label-text font-medium">Logos Required</span> <span className="label-text font-medium text-lg">Logos Required</span>
</label> </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) => ( {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 <input
type="checkbox" type="checkbox"
className="checkbox checkbox-primary mt-1" className="checkbox checkbox-primary mt-1"
checked={formData.required_logos.includes(logo.value)} checked={formData.required_logos.includes(logo.value)}
onChange={() => handleLogoChange(logo.value)} onChange={() => handleLogoChange(logo.value)}
/> />
<span>{logo.label}</span> <span className="font-medium">{logo.label}</span>
</label> </motion.label>
))} ))}
</div> </div>
</motion.div> </motion.div>
{/* Logo file upload */} {/* Logo file upload */}
{formData.required_logos.includes(LogoOptions.OTHER) && ( {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"> <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> <span className="label-text-alt text-error">*</span>
</label> </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 <input
id="logo-files"
type="file" 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} onChange={handleLogoFileChange}
accept="image/*" accept="image/*"
multiple multiple
required required
/> />
{otherLogoFiles.length > 0 && (
<div className="mt-2"> <div className="flex flex-col items-center justify-center gap-3">
<p className="text-sm font-medium mb-1">Selected files:</p> <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"> <ul className="list-disc list-inside text-sm">
{otherLogoFiles.map((file, index) => ( {otherLogoFiles.map((file, index) => (
<li key={index}>{file.name}</li> <li key={index} className="truncate">{file.name}</li>
))} ))}
</ul> </ul>
</div> </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> </motion.div>
)} )}
{/* Format */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </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" className="select select-bordered focus:select-primary transition-all duration-300"
value={formData.advertising_format} value={formData.advertising_format}
onChange={(e) => onDataChange({ advertising_format: e.target.value })} onChange={(e) => onDataChange({ advertising_format: e.target.value })}
required required
whileHover="hover"
variants={inputHoverVariants}
> >
<option value="">Select format</option> <option value="">Select format</option>
{FORMAT_OPTIONS.map(option => ( {FORMAT_OPTIONS.map(option => (
<option key={option.value} value={option.value}>{option.label}</option> <option key={option.value} value={option.value}>{option.label}</option>
))} ))}
</select> </motion.select>
</motion.div> </motion.div>
{/* Additional specifications */} {/* 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"> <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> </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]" className="textarea textarea-bordered focus:textarea-primary transition-all duration-300 min-h-[120px]"
value={formData.flyer_additional_requests} value={formData.flyer_additional_requests}
onChange={(e) => onDataChange({ flyer_additional_requests: e.target.value })} onChange={(e) => onDataChange({ flyer_additional_requests: e.target.value })}
placeholder="Color scheme, overall design, examples to consider, etc." placeholder="Color scheme, overall design, examples to consider, etc."
rows={4} rows={4}
whileHover="hover"
variants={inputHoverVariants}
/> />
</motion.div> </motion.div>
{/* Photography Needed */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="flex gap-4"> <p className="text-sm text-gray-500 mb-3">Do you need photography for your event?</p>
<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 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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -245,9 +447,17 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
onChange={() => onDataChange({ photography_needed: true })} onChange={() => onDataChange({ photography_needed: true })}
required required
/> />
<span>Yes</span> <span className="font-medium">Yes, we need photography</span>
</label> </motion.label>
<label className="flex items-center gap-2 cursor-pointer">
<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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -255,11 +465,11 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
onChange={() => onDataChange({ photography_needed: false })} onChange={() => onDataChange({ photography_needed: false })}
required required
/> />
<span>No</span> <span className="font-medium">No, we don't need photography</span>
</label> </motion.label>
</div> </div>
</motion.div> </motion.div>
</div> </motion.div>
); );
}; };

View file

@ -2,8 +2,20 @@ import React, { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; 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 = { const itemVariants = {
hidden: { opacity: 0, y: 20 }, hidden: { opacity: 0, y: 20 },
visible: { 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 { interface TAPFormSectionProps {
formData: EventRequestFormData; formData: EventRequestFormData;
onDataChange: (data: Partial<EventRequestFormData>) => void; onDataChange: (data: Partial<EventRequestFormData>) => void;
@ -24,6 +56,7 @@ interface TAPFormSectionProps {
const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => { const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => {
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking); const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking);
const [isDragging, setIsDragging] = useState(false);
// Handle room booking file upload // Handle room booking file upload
const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -34,75 +67,169 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
} }
}; };
return ( // Handle drag events for file upload
<div className="space-y-6"> const handleDragOver = (e: React.DragEvent) => {
<h2 className="text-2xl font-bold mb-4 text-primary">TAP Form Information</h2> e.preventDefault();
setIsDragging(true);
};
<div className="bg-base-300/50 p-4 rounded-lg mb-6"> const handleDragLeave = (e: React.DragEvent) => {
<p className="text-sm"> e.preventDefault();
Please ensure you have ALL sections completed. If something is not available, let the coordinators know and be advised on how to proceed. setIsDragging(false);
</p> };
</div>
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 */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="relative mt-2"> <div className="relative mt-2">
<input <motion.input
type="number" 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 || ''} value={formData.expected_attendance || ''}
onChange={(e) => onDataChange({ expected_attendance: parseInt(e.target.value) || 0 })} onChange={(e) => onDataChange({ expected_attendance: parseInt(e.target.value) || 0 })}
min="0" min="0"
placeholder="Enter expected attendance" placeholder="Enter expected attendance"
required 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 people
</div> </div>
</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> <motion.div
<p>ONLY UC SAN DIEGO UNDERGRADUATE STUDENTS MAY RECEIVE ITEMS FUNDED BY THE ASSOCIATED STUDENTS.</p> className="mt-4 p-4 bg-base-300/30 rounded-lg text-xs text-gray-400"
<p>EVENT FUNDING IS GRANTED UP TO A MAXIMUM OF $10.00 PER EXPECTED STUDENT ATTENDEE AND $5,000 PER EVENT</p> initial={{ opacity: 0.8 }}
</div> 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> </motion.div>
{/* Room booking confirmation */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </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 <input
id="room-booking-file"
type="file" type="file"
className="file-input file-input-bordered file-input-primary w-full" className="hidden"
onChange={handleRoomBookingFileChange} onChange={handleRoomBookingFileChange}
accept=".pdf,.png,.jpg,.jpeg" accept=".pdf,.png,.jpg,.jpeg"
/> />
{roomBookingFile && (
<p className="text-sm mt-2"> <div className="flex flex-col items-center justify-center gap-3">
Selected file: {roomBookingFile.name} <motion.div
</p> 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> </div>
</motion.div> </motion.div>
</motion.div>
{/* Food/Drinks */} {/* 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"> <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> <span className="label-text-alt text-error">*</span>
</label> </label>
<div className="flex gap-4"> <p className="text-sm text-gray-500 mb-3">Will you be serving food or drinks at your event?</p>
<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 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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -110,9 +237,17 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
onChange={() => onDataChange({ food_drinks_being_served: true })} onChange={() => onDataChange({ food_drinks_being_served: true })}
required required
/> />
<span>Yes</span> <span className="font-medium">Yes, we'll have food/drinks</span>
</label> </motion.label>
<label className="flex items-center gap-2 cursor-pointer">
<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 <input
type="radio" type="radio"
className="radio radio-primary" className="radio radio-primary"
@ -120,31 +255,93 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
onChange={() => onDataChange({ food_drinks_being_served: false })} onChange={() => onDataChange({ food_drinks_being_served: false })}
required required
/> />
<span>No</span> <span className="font-medium">No, no food/drinks</span>
</label> </motion.label>
</div> </div>
</motion.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 && ( {formData.food_drinks_being_served && (
<motion.div <motion.div
initial={{ opacity: 0, height: 0 }} variants={itemVariants}
animate={{ opacity: 1, height: 'auto' }} className="form-control bg-base-200/50 p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300"
exit={{ opacity: 0, height: 0 }} whileHover={{ y: -2 }}
className="alert alert-info" 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"> <label className="label">
<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> <span className="label-text font-medium text-lg">AS Funding Request</span>
</svg> <span className="label-text-alt text-error">*</span>
<div> </label>
<h3 className="font-bold">Food and Drinks Information</h3> <p className="text-sm text-gray-500 mb-3">Do you need funding from AS?</p>
<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 className="flex gap-6 mt-2">
</div> <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> </div>
</motion.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>
); );
}; };