added an event request form

This commit is contained in:
chark1es 2025-02-20 01:24:05 -08:00
parent c9ae525c8b
commit 9019a97496
9 changed files with 1681 additions and 1 deletions

View file

@ -0,0 +1,323 @@
---
import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
// Form sections
import PRSection from "./Officer_EventRequestForm/PRSection";
import EventDetailsSection from "./Officer_EventRequestForm/EventDetailsSection";
import TAPSection from "./Officer_EventRequestForm/TAPSection";
import ASFundingSection from "./Officer_EventRequestForm/ASFundingSection";
const auth = Authentication.getInstance();
const update = Update.getInstance();
const fileManager = FileManager.getInstance();
---
<div class="w-full max-w-4xl mx-auto p-6">
<h1
class="text-3xl font-bold mb-8 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
Event Request Form
</h1>
<form id="eventRequestForm" class="space-y-8">
<div class="card bg-base-100/95 backdrop-blur-md shadow-lg">
<div class="card-body">
<h2 class="card-title text-xl">
Do you need graphics from our design team?
</h2>
<div class="space-y-4 mt-4">
<label class="label cursor-pointer justify-start gap-3">
<input
type="radio"
name="needsGraphics"
value="yes"
class="radio radio-primary"
/>
<span class="label-text">Yes (Continue to PR Section)</span>
</label>
<label class="label cursor-pointer justify-start gap-3">
<input
type="radio"
name="needsGraphics"
value="no"
class="radio radio-primary"
/>
<span class="label-text">No (Skip to Event Details)</span>
</label>
</div>
</div>
</div>
<div id="prSection" class="hidden">
<PRSection client:load />
</div>
<div id="eventDetailsSection">
<EventDetailsSection client:load />
</div>
<div id="tapSection">
<TAPSection
client:load
onDataChange={(data) => {
// This will be handled in the client-side script
document.dispatchEvent(
new CustomEvent("tap-section-change", {
detail: data,
}),
);
}}
/>
</div>
<div id="asFundingSection" class="hidden">
<ASFundingSection client:load />
</div>
<div class="flex justify-end space-x-4 mt-8">
<button
type="button"
id="saveAsDraft"
class="btn btn-ghost hover:bg-base-200 gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
></path>
</svg>
Save as Draft
</button>
<button
type="submit"
class="btn btn-primary gap-2 shadow-md hover:shadow-lg transition-all duration-300"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"
></path>
</svg>
Submit Request
</button>
</div>
</form>
</div>
<script>
import { Authentication } from "../../scripts/pocketbase/Authentication";
import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager";
// Form visibility logic
const form = document.getElementById("eventRequestForm") as HTMLFormElement;
const prSection = document.getElementById("prSection");
const asFundingSection = document.getElementById("asFundingSection");
const needsGraphicsRadios = document.getElementsByName("needsGraphics");
// Debug log for initial state
console.log("Initial ASFundingSection state:", {
element: asFundingSection,
isHidden: asFundingSection?.classList.contains("hidden"),
display: asFundingSection?.style.display,
});
// Handle TAPSection changes
document.addEventListener("tap-section-change", (event: any) => {
const data = event.detail;
console.log("TAP section change event received:", data);
if (asFundingSection) {
console.log("Found ASFundingSection element");
if (data.as_funding_required) {
console.log("Showing AS Funding section");
asFundingSection.classList.remove("hidden");
asFundingSection.style.removeProperty("display");
// Force a reflow
void asFundingSection.offsetHeight;
} else {
console.log("Hiding AS Funding section");
asFundingSection.classList.add("hidden");
asFundingSection.style.display = "none";
}
// Log the state after change
console.log("ASFundingSection state after change:", {
isHidden: asFundingSection.classList.contains("hidden"),
display: asFundingSection.style.display,
});
} else {
console.error("ASFundingSection element not found");
}
});
// Show/hide PR section based on radio selection
needsGraphicsRadios.forEach((radio) => {
radio.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
if (target.value === "yes" && prSection) {
prSection.classList.remove("hidden");
} else if (prSection) {
prSection.classList.add("hidden");
}
});
});
// Form submission handler
form?.addEventListener("submit", async (e) => {
e.preventDefault();
// Collect form data
const formData = new FormData(form);
const data: Record<string, any> = {};
// Convert FormData to a proper object with correct types
formData.forEach((value, key) => {
if (value instanceof File) {
// Skip file fields as they'll be handled separately
return;
}
data[key] = value;
});
try {
// Create event request record
const auth = Authentication.getInstance();
const update = Update.getInstance();
// Add user ID to the request
const userId = auth.getUserId();
if (userId) {
data.requested_user = userId;
}
// Create the record
const record = await update.updateFields(
"event_request",
data.id || "",
data,
);
// Handle file uploads if any
const fileManager = FileManager.getInstance();
const fileFields = ["room_booking", "invoice", "other_logos"];
for (const field of fileFields) {
const files = formData
.getAll(field)
.filter((f): f is File => f instanceof File);
if (files.length > 0) {
await fileManager.uploadFiles(
"event_request",
record.id,
field,
files,
);
}
}
// Show success message using a toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Event request submitted successfully!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
// Redirect to events management page
window.location.href = "/dashboard/events";
} catch (error) {
console.error("Error submitting form:", error);
// Show error toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Error submitting form. Please try again.</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
});
// Save as draft handler
document
.getElementById("saveAsDraft")
?.addEventListener("click", async () => {
// Similar to submit but mark as draft
const formData = new FormData(form);
const data: Record<string, any> = {};
// Convert FormData to a proper object with correct types
formData.forEach((value, key) => {
if (value instanceof File) {
// Skip file fields as they'll be handled separately
return;
}
data[key] = value;
});
data.status = "draft";
try {
const auth = Authentication.getInstance();
const update = Update.getInstance();
const userId = auth.getUserId();
if (userId) {
data.requested_user = userId;
}
await update.updateFields("event_request", data.id || "", data);
// Show success toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Draft saved successfully!</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} catch (error) {
console.error("Error saving draft:", error);
// Show error toast
const toast = document.createElement("div");
toast.className = "toast toast-end";
toast.innerHTML = `
<div class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
<span>Error saving draft. Please try again.</span>
</div>
`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
});
</script>

View file

@ -0,0 +1,464 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'react-hot-toast';
import InfoCard from './InfoCard';
import Tooltip from './Tooltip';
import { tooltips, infoNotes } from './tooltips';
import { Icon } from '@iconify/react';
interface InvoiceItem {
quantity: number;
item_name: string;
unit_cost: number;
}
interface InvoiceData {
items: InvoiceItem[];
tax: number;
tip: number;
total: number;
vendor: string;
}
interface ASFundingSectionProps {
onDataChange?: (data: any) => void;
}
const ASFundingSection: React.FC<ASFundingSectionProps> = ({ onDataChange }) => {
const [invoiceData, setInvoiceData] = useState<InvoiceData>({
items: [{ quantity: 0, item_name: '', unit_cost: 0 }],
tax: 0,
tip: 0,
total: 0,
vendor: ''
});
const handleItemChange = (index: number, field: keyof InvoiceItem, value: string | number) => {
const newItems = [...invoiceData.items];
newItems[index] = { ...newItems[index], [field]: value };
// Calculate new total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
setInvoiceData(prev => ({
...prev,
items: newItems,
total: newTotal
}));
// Notify parent with JSON string
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
items: newItems,
total: newTotal
})
});
};
const addItem = () => {
setInvoiceData(prev => ({
...prev,
items: [...prev.items, { quantity: 0, item_name: '', unit_cost: 0 }]
}));
toast('New item added', { icon: '' });
};
const removeItem = (index: number) => {
if (invoiceData.items.length > 1) {
const newItems = invoiceData.items.filter((_, i) => i !== index);
// Recalculate total
const itemsTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = itemsTotal + invoiceData.tax + invoiceData.tip;
setInvoiceData(prev => ({
...prev,
items: newItems,
total: newTotal
}));
// Notify parent with JSON string
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
items: newItems,
total: newTotal
})
});
toast('Item removed', { icon: '🗑️' });
}
};
const handleExtraChange = (field: 'tax' | 'tip' | 'vendor', value: string | number) => {
const numValue = field !== 'vendor' ? Number(value) : value;
// Calculate new total for tax/tip changes
const itemsTotal = invoiceData.items.reduce((sum, item) => sum + (item.quantity * item.unit_cost), 0);
const newTotal = field === 'tax' ?
itemsTotal + Number(value) + invoiceData.tip :
field === 'tip' ?
itemsTotal + invoiceData.tax + Number(value) :
itemsTotal + invoiceData.tax + invoiceData.tip;
setInvoiceData(prev => ({
...prev,
[field]: numValue,
total: field !== 'vendor' ? newTotal : prev.total
}));
// Notify parent with JSON string
onDataChange?.({
itemized_invoice: JSON.stringify({
...invoiceData,
[field]: numValue,
total: field !== 'vendor' ? newTotal : invoiceData.total
})
});
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="card bg-base-100/95 backdrop-blur-md shadow-lg hover:shadow-xl transition-all duration-300"
>
<div className="card-body">
<motion.h2
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2"
>
<Icon icon="mdi:cash" className="h-6 w-6" />
AS Funding Details
</motion.h2>
<div className="space-y-8">
<InfoCard
title={infoNotes.asFunding.title}
items={infoNotes.asFunding.items}
type="warning"
className="mb-6"
/>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:store" className="h-5 w-5 text-primary" />
Vendor Information
</span>
</label>
<Tooltip
title={tooltips.vendor.title}
description={tooltips.vendor.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="relative group">
<input
type="text"
placeholder="Enter vendor name and location"
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={invoiceData.vendor}
onChange={(e) => handleExtraChange('vendor', e.target.value)}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-400 group-hover:text-primary transition-colors duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:file-document-outline" className="h-5 w-5 text-primary" />
Itemized Invoice
</span>
</label>
<Tooltip
title={tooltips.invoice.title}
description={tooltips.invoice.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<AnimatePresence mode="popLayout">
<div className="space-y-4">
{invoiceData.items.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
className="flex gap-4 items-end bg-base-200/50 p-4 rounded-lg group hover:bg-base-200 transition-colors duration-300"
>
<div className="form-control flex-1">
<label className="label">
<span className="label-text">Quantity</span>
</label>
<input
type="number"
min="1"
className="input input-bordered w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={item.quantity || ''}
onChange={(e) => handleItemChange(index, 'quantity', Number(e.target.value))}
required
/>
</div>
<div className="form-control flex-[3]">
<label className="label">
<span className="label-text">Item Name</span>
</label>
<input
type="text"
className="input input-bordered w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={item.item_name}
onChange={(e) => handleItemChange(index, 'item_name', e.target.value)}
required
/>
</div>
<div className="form-control flex-1">
<label className="label">
<span className="label-text">Unit Cost ($)</span>
</label>
<div className="relative">
<input
type="number"
min="0"
step="0.01"
className="input input-bordered w-full pl-8 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={item.unit_cost || ''}
onChange={(e) => handleItemChange(index, 'unit_cost', Number(e.target.value))}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-400">$</span>
</div>
</div>
</div>
<motion.button
type="button"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="btn btn-ghost btn-square opacity-0 group-hover:opacity-100 transition-opacity duration-300"
onClick={() => removeItem(index)}
disabled={invoiceData.items.length === 1}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</motion.button>
</motion.div>
))}
</div>
</AnimatePresence>
<motion.button
type="button"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="btn btn-ghost mt-4 w-full group hover:bg-primary/10"
onClick={addItem}
>
<Icon icon="mdi:plus" className="h-6 w-6 mr-2 group-hover:text-primary transition-colors duration-300" />
Add Item
</motion.button>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="grid grid-cols-2 gap-4"
>
<div className="form-control">
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<Icon icon="mdi:percent" className="h-5 w-5 text-primary" />
Tax ($)
</span>
</label>
<Tooltip
title={tooltips.tax.title}
description={tooltips.tax.description}
position="top"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="relative group">
<input
type="number"
min="0"
step="0.01"
className="input input-bordered pl-10 w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={invoiceData.tax || ''}
onChange={(e) => handleExtraChange('tax', Number(e.target.value))}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
</div>
</div>
</div>
<div className="form-control">
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<Icon icon="mdi:hand-coin" className="h-5 w-5 text-primary" />
Tip ($)
</span>
</label>
<Tooltip
title={tooltips.tip.title}
description={tooltips.tip.description}
position="top"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="relative group">
<input
type="number"
min="0"
step="0.01"
className="input input-bordered pl-10 w-full transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={invoiceData.tip || ''}
onChange={(e) => handleExtraChange('tip', Number(e.target.value))}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
</div>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="form-control"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:calculator" className="h-5 w-5 text-primary" />
Total Amount
</span>
</label>
<Tooltip
title={tooltips.total.title}
description={tooltips.total.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="relative group">
<input
type="number"
className="input input-bordered pl-10 w-full font-bold bg-base-200/50 transition-all duration-300"
value={invoiceData.total.toFixed(2)}
disabled
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-400 group-hover:text-primary transition-colors duration-300">$</span>
</div>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:cloud-upload" className="h-5 w-5 text-primary" />
Upload Invoice
</span>
</label>
<Tooltip
title={tooltips.invoice.title}
description={tooltips.invoice.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<InfoCard
title={infoNotes.invoice.title}
items={infoNotes.invoice.items}
type="info"
className="mb-4"
/>
<div className="relative group">
<input
type="file"
name="invoice"
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"
className="file-input file-input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
onChange={(e) => {
onDataChange?.({ invoice: e.target.files?.[0] });
if (e.target.files?.[0]) {
toast('Invoice file uploaded', { icon: '📄' });
}
}}
required
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-gray-400 group-hover:text-primary transition-colors duration-300" 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>
</div>
</div>
</motion.div>
</div>
</div>
</motion.div>
);
};
export default ASFundingSection;

View file

@ -0,0 +1,149 @@
import React from 'react';
interface EventDetailsSectionProps {
onDataChange?: (data: any) => void;
}
const EventDetailsSection: React.FC<EventDetailsSectionProps> = ({ onDataChange }) => {
return (
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
<div className="card-body">
<h2 className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Event Details
</h2>
<div className="space-y-6 mt-4">
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
Event Name
</span>
</label>
<input
type="text"
name="name"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ name: e.target.value })}
required
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Event Description
</span>
</label>
<textarea
name="description"
className="textarea textarea-bordered h-32"
onChange={(e) => onDataChange?.({ description: e.target.value })}
required
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Event Start Date
</span>
</label>
<input
type="datetime-local"
name="start_date_time"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ start_date_time: e.target.value })}
required
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Event End Date
</span>
</label>
<input
type="datetime-local"
name="end_date_time"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ end_date_time: e.target.value })}
required
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Event Location
</span>
</label>
<input
type="text"
name="location"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ location: e.target.value })}
required
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
Room Booking Status
</span>
</label>
<div className="flex gap-4">
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="will_or_have_room_booking"
value="true"
className="radio radio-primary"
onChange={(e) => onDataChange?.({ will_or_have_room_booking: e.target.value === 'true' })}
required
/>
<span className="label-text">Yes</span>
</label>
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="will_or_have_room_booking"
value="false"
className="radio radio-primary"
onChange={(e) => onDataChange?.({ will_or_have_room_booking: e.target.value === 'true' })}
required
/>
<span className="label-text">No</span>
</label>
</div>
</div>
</div>
</div>
</div>
);
};
export default EventDetailsSection;

View file

@ -0,0 +1,78 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Icon } from '@iconify/react';
interface InfoCardProps {
title: string;
items: readonly string[] | string[];
type?: 'info' | 'warning' | 'success';
icon?: React.ReactNode;
className?: string;
}
const defaultIcons = {
info: <Icon icon="mdi:information-outline" className="text-info shrink-0 w-6 h-6" />,
warning: <Icon icon="mdi:alert-outline" className="text-warning shrink-0 w-6 h-6" />,
success: <Icon icon="mdi:check-circle-outline" className="text-success shrink-0 w-6 h-6" />
};
const typeStyles = {
info: 'alert-info bg-info/10',
warning: 'alert-warning bg-warning/10',
success: 'alert-success bg-success/10'
};
export const InfoCard: React.FC<InfoCardProps> = ({
title,
items,
type = 'info',
icon,
className = ''
}) => {
const listVariants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
show: { opacity: 1, x: 0 }
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`alert ${typeStyles[type]} shadow-sm ${className}`}
>
{icon || defaultIcons[type]}
<div className="text-sm space-y-2">
<p className="font-medium">{title}</p>
<motion.ul
className="space-y-1 ml-1"
variants={listVariants}
initial="hidden"
animate="show"
>
{items.map((item, index) => (
<motion.li
key={index}
variants={itemVariants}
className="flex items-start gap-2"
>
<span className="text-base leading-6"></span>
<span>{item}</span>
</motion.li>
))}
</motion.ul>
</div>
</motion.div>
);
};
export default InfoCard;

View file

@ -0,0 +1,232 @@
import React, { useState } from 'react';
interface PRSectionProps {
onDataChange?: (data: any) => void;
}
const PRSection: React.FC<PRSectionProps> = ({ onDataChange }) => {
const [selectedTypes, setSelectedTypes] = useState<string[]>([]);
const [selectedLogos, setSelectedLogos] = useState<string[]>([]);
const flyerTypes = [
{ value: 'digital_with_social', label: 'Digital flyer (with social media advertising: Facebook, Instagram, Discord)' },
{ value: 'digital_no_social', label: 'Digital flyer (with NO social media advertising)' },
{ value: 'physical_with_advertising', label: 'Physical flyer (with advertising)' },
{ value: 'physical_no_advertising', label: 'Physical flyer (with NO advertising)' },
{ value: 'newsletter', label: 'Newsletter (IEEE, ECE, IDEA)' },
{ value: 'other', label: 'Other' }
];
const logoOptions = [
{ value: 'IEEE', label: 'IEEE' },
{ value: 'AS', label: 'AS' },
{ value: 'HKN', label: 'HKN' },
{ value: 'TESC', label: 'TESC' },
{ value: 'PIB', label: 'PIB' },
{ value: 'TNT', label: 'TNT' },
{ value: 'SWE', label: 'SWE' },
{ value: 'OTHER', label: 'OTHER' }
];
const handleTypeChange = (value: string) => {
const newTypes = selectedTypes.includes(value)
? selectedTypes.filter(type => type !== value)
: [...selectedTypes, value];
setSelectedTypes(newTypes);
if (onDataChange) {
onDataChange({ flyer_type: newTypes });
}
};
const handleLogoChange = (value: string) => {
const newLogos = selectedLogos.includes(value)
? selectedLogos.filter(logo => logo !== value)
: [...selectedLogos, value];
setSelectedLogos(newLogos);
if (onDataChange) {
onDataChange({ required_logos: newLogos });
}
};
return (
<div className="card bg-base-100/95 backdrop-blur-md shadow-lg">
<div className="card-body">
<h2 className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" 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>
PR Materials
</h2>
<div className="space-y-4 mt-4">
<label className="form-control w-full">
<div className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Type of material needed
</span>
</div>
<div className="space-y-2">
{flyerTypes.map(type => (
<label key={type.value} className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="checkbox"
className="checkbox checkbox-primary"
checked={selectedTypes.includes(type.value)}
onChange={() => handleTypeChange(type.value)}
/>
<span className="label-text">{type.label}</span>
</label>
))}
</div>
</label>
</div>
{selectedTypes.length > 0 && (
<>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Advertising Start Date
</span>
</label>
<input
type="datetime-local"
name="flyer_advertising_start_date"
className="input input-bordered w-full"
onChange={(e) => onDataChange?.({ flyer_advertising_start_date: e.target.value })}
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" 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>
Logos Required
</span>
</label>
<div className="space-y-2">
{logoOptions.map(logo => (
<label key={logo.value} className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="checkbox"
className="checkbox checkbox-primary"
checked={selectedLogos.includes(logo.value)}
onChange={() => handleLogoChange(logo.value)}
/>
<span className="label-text">{logo.label}</span>
</label>
))}
</div>
</div>
{selectedLogos.includes('OTHER') && (
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" 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>
Upload Logo Files
</span>
</label>
<input
type="file"
name="other_logos"
multiple
accept="image/*"
className="file-input file-input-bordered w-full"
onChange={(e) => onDataChange?.({ other_logos: e.target.files })}
/>
</div>
)}
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" 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>
Format Needed
</span>
</label>
<select
name="advertising_format"
className="select select-bordered w-full"
onChange={(e) => onDataChange?.({ advertising_format: e.target.value })}
>
<option value="">Select a format...</option>
<option value="pdf">PDF</option>
<option value="png">PNG</option>
<option value="jpeg">JPG</option>
<option value="does_not_matter">DOES NOT MATTER</option>
</select>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Additional Specifications
</span>
</label>
<textarea
name="flyer_additional_requests"
className="textarea textarea-bordered h-32"
placeholder="Color scheme, overall design, examples to consider..."
onChange={(e) => onDataChange?.({ flyer_additional_requests: e.target.value })}
/>
</div>
<div className="form-control w-full">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Photography Needed
</span>
</label>
<div className="flex gap-4">
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="photography_needed"
value="true"
className="radio radio-primary"
onChange={(e) => onDataChange?.({ photography_needed: e.target.value === 'true' })}
/>
<span className="label-text">Yes</span>
</label>
<label className="label cursor-pointer justify-start gap-3 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="photography_needed"
value="false"
className="radio radio-primary"
onChange={(e) => onDataChange?.({ photography_needed: e.target.value === 'true' })}
/>
<span className="label-text">No</span>
</label>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default PRSection;

View file

@ -0,0 +1,262 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { toast } from 'react-hot-toast';
import InfoCard from './InfoCard';
import Tooltip from './Tooltip';
import { tooltips, infoNotes } from './tooltips';
import { Icon } from '@iconify/react';
interface TAPSectionProps {
onDataChange?: (data: any) => void;
onASFundingChange?: (enabled: boolean) => void;
}
const TAPSection: React.FC<TAPSectionProps> = ({ onDataChange, onASFundingChange }) => {
const [expectedAttendance, setExpectedAttendance] = useState<number>(0);
const [roomBooking, setRoomBooking] = useState<string>('');
const [needsASFunding, setNeedsASFunding] = useState<boolean>(false);
const [needsFoodDrinks, setNeedsFoodDrinks] = useState<boolean>(false);
const handleAttendanceChange = (value: number) => {
setExpectedAttendance(value);
if (value > 100) {
toast('Large attendance detected! Please ensure proper room capacity.', {
icon: '⚠️',
duration: 4000
});
}
onDataChange?.({ expected_attendance: value });
};
const handleRoomBookingChange = (value: string) => {
setRoomBooking(value);
onDataChange?.({ room_booking: value });
};
const handleASFundingChange = (enabled: boolean) => {
console.log('AS Funding change:', enabled);
setNeedsASFunding(enabled);
if (!enabled) {
setNeedsFoodDrinks(false);
onDataChange?.({ needs_food_drinks: false });
}
onASFundingChange?.(enabled);
console.log('Sending data change:', { as_funding_required: enabled });
onDataChange?.({ as_funding_required: enabled });
toast(enabled ? 'AS Funding enabled - please fill out funding details.' : 'AS Funding disabled', {
icon: enabled ? '💰' : '❌',
duration: 3000
});
};
const handleFoodDrinksChange = (enabled: boolean) => {
setNeedsFoodDrinks(enabled);
onDataChange?.({ needs_food_drinks: enabled });
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="card bg-base-100/95 backdrop-blur-md shadow-lg hover:shadow-xl transition-all duration-300"
>
<div className="card-body">
<motion.h2
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
className="card-title text-xl mb-6 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent flex items-center gap-2"
>
<Icon icon="mdi:clipboard-text-outline" className="h-6 w-6" />
TAP Form
</motion.h2>
<div className="space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:account-group" className="h-5 w-5 text-primary" />
Expected Attendance
</span>
</label>
<Tooltip
title={tooltips.attendance.title}
description={tooltips.attendance.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="relative group">
<input
type="number"
min="0"
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={expectedAttendance || ''}
onChange={(e) => handleAttendanceChange(Number(e.target.value))}
required
/>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:office-building-outline" className="h-5 w-5 text-primary" />
Room Booking
</span>
</label>
<Tooltip
title={tooltips.room.title}
description={tooltips.room.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<InfoCard
title={infoNotes.room.title}
items={infoNotes.room.items}
type="info"
className="mb-4"
/>
<div className="relative group">
<input
type="text"
placeholder="Enter room number and building (e.g. EBU1 2315)"
className="input input-bordered w-full pl-12 transition-all duration-300 focus:ring-2 focus:ring-primary/20"
value={roomBooking}
onChange={(e) => handleRoomBookingChange(e.target.value)}
required
/>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="form-control w-full"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:cash" className="h-5 w-5 text-primary" />
AS Funding
</span>
</label>
<Tooltip
title={tooltips.asFunding.title}
description={tooltips.asFunding.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="flex flex-col gap-2">
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="as_funding"
className="radio radio-primary"
checked={needsASFunding}
onChange={() => handleASFundingChange(true)}
/>
<span className="label-text">Yes, I need AS Funding</span>
</label>
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="as_funding"
className="radio radio-primary"
checked={!needsASFunding}
onChange={() => handleASFundingChange(false)}
/>
<span className="label-text">No, I don't need AS Funding</span>
</label>
</div>
</motion.div>
<AnimatePresence mode="wait">
{needsASFunding && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="form-control w-full overflow-hidden"
>
<div className="flex items-center justify-between mb-2">
<label className="label">
<span className="label-text font-medium text-lg flex items-center gap-2">
<Icon icon="mdi:food" className="h-5 w-5 text-primary" />
Food/Drinks
</span>
</label>
<Tooltip
title={tooltips.food.title}
description={tooltips.food.description}
position="left"
>
<div className="badge badge-primary badge-outline p-3 cursor-help">
<Icon icon="mdi:information-outline" className="h-4 w-4" />
</div>
</Tooltip>
</div>
<div className="flex flex-col gap-2">
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="food_drinks"
className="radio radio-primary"
checked={needsFoodDrinks}
onChange={() => handleFoodDrinksChange(true)}
/>
<span className="label-text">Yes, I need food/drinks</span>
</label>
<label className="label cursor-pointer justify-start gap-4 hover:bg-base-200/50 p-4 rounded-lg transition-colors duration-300">
<input
type="radio"
name="food_drinks"
className="radio radio-primary"
checked={!needsFoodDrinks}
onChange={() => handleFoodDrinksChange(false)}
/>
<span className="label-text">No, I don't need food/drinks</span>
</label>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
);
};
export default TAPSection;

View file

@ -0,0 +1,66 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Icon } from '@iconify/react';
interface TooltipProps {
title: string;
description: string;
children: React.ReactNode;
className?: string;
position?: 'top' | 'bottom' | 'left' | 'right';
}
const positionStyles = {
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
right: 'left-full top-1/2 -translate-y-1/2 ml-2'
};
const arrowStyles = {
top: 'bottom-[-6px] left-1/2 -translate-x-1/2 border-t-base-300 border-l-transparent border-r-transparent border-b-transparent',
bottom: 'top-[-6px] left-1/2 -translate-x-1/2 border-b-base-300 border-l-transparent border-r-transparent border-t-transparent',
left: 'right-[-6px] top-1/2 -translate-y-1/2 border-l-base-300 border-t-transparent border-b-transparent border-r-transparent',
right: 'left-[-6px] top-1/2 -translate-y-1/2 border-r-base-300 border-t-transparent border-b-transparent border-l-transparent'
};
export const Tooltip: React.FC<TooltipProps> = ({
title,
description,
children,
className = '',
position = 'top'
}) => {
const [isVisible, setIsVisible] = React.useState(false);
return (
<div
className={`relative inline-block ${className}`}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
onFocus={() => setIsVisible(true)}
onBlur={() => setIsVisible(false)}
>
{children}
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.1 }}
className={`absolute z-50 min-w-[320px] max-w-md p-4 bg-base-100 border border-base-300 rounded-lg shadow-lg ${positionStyles[position]}`}
>
<div className={`absolute w-0 h-0 border-4 ${arrowStyles[position]}`} />
<div className="space-y-2">
<p className="font-medium text-base">{title}</p>
<p className="text-sm leading-relaxed text-base-content/80">{description}</p>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default Tooltip;

View file

@ -0,0 +1,99 @@
export const tooltips = {
attendance: {
title: "Expected Attendance",
description:
"Enter the total number of expected attendees. This helps us plan resources and funding appropriately.",
maxLimit: "Maximum funding is $10 per student, up to $5,000 per event.",
eligibility:
"Only UCSD students, staff, and faculty are eligible to attend.",
},
room: {
title: "Room Booking",
description:
"Enter the room number and building where your event will be held. Make sure the room capacity matches your expected attendance.",
format: "Format: Building Room# (e.g. EBU1 2315)",
requirements: "Room must be booked through the appropriate UCSD channels.",
},
asFunding: {
title: "AS Funding",
description:
"Associated Students can provide funding for your event. Select this option if you need financial support.",
maxAmount: "Maximum funding varies based on event type and attendance.",
requirements: "Must submit request at least 6 weeks before event.",
},
food: {
title: "Food & Drinks",
description:
"Indicate if you plan to serve food or drinks at your event. This requires additional approvals and documentation.",
requirements:
"Must use approved vendors and follow food safety guidelines.",
timing: "Food orders must be finalized 2 weeks before event.",
},
vendor: {
title: "Vendor Information",
description:
"Enter the name and location of the vendor you plan to use for food/drinks.",
requirements: "Must be an approved AS Funding vendor.",
format: "Format: Vendor Name - Location",
},
invoice: {
title: "Invoice Details",
description: "Provide itemized details of your planned purchases.",
requirements:
"All items must be clearly listed with quantities and unit costs.",
format: "Official invoices required 2 weeks before event.",
},
tax: {
title: "Sales Tax",
description: "Enter the total sales tax amount from your invoice.",
note: "California sales tax is typically 7.75%",
},
tip: {
title: "Gratuity",
description: "Enter the tip amount if applicable.",
note: "Maximum 15% for delivery orders.",
},
total: {
title: "Total Amount",
description: "The total cost including items, tax, and tip.",
note: "Cannot exceed your approved funding amount.",
},
} as const;
export const infoNotes = {
funding: {
title: "Funding Guidelines",
items: [
"Events funded by programming funds may only admit UC San Diego students, staff or faculty as guests.",
"Only UC San Diego undergraduate students may receive items funded by the Associated Students.",
"Event funding is granted up to $10 per student, with a maximum of $5,000 per event.",
"Submit all documentation at least 6 weeks before the event.",
],
},
room: {
title: "Room Booking Format",
items: [
"Use the format: Building Room# (e.g. EBU1 2315)",
"Make sure the room capacity matches your expected attendance",
"Book through the appropriate UCSD channels",
"Include any special equipment needs in your request",
],
},
asFunding: {
title: "AS Funding Requirements",
items: [
"Please make sure the restaurant is a valid AS Funding food vendor!",
"Make sure to include all items, prices, and additional costs.",
"We don't recommend paying out of pocket as reimbursements can be complex.",
"Submit all documentation at least 6 weeks before the event.",
],
},
invoice: {
title: "Invoice Requirements",
items: [
"Official food invoices will be required 2 weeks before the start of your event.",
"Format: EventName_OrderLocation_DateOfEvent",
"Example: QPWorkathon#1_PapaJohns_01/06/2025",
],
},
} as const;

View file

@ -36,6 +36,13 @@ sections:
component: "Officer_ReimbursementManagement" component: "Officer_ReimbursementManagement"
class: "text-info hover:text-info-focus" class: "text-info hover:text-info-focus"
eventRequestForm:
title: "Event Request Form"
icon: "heroicons:document-text"
role: "general"
component: "Officer_EventRequestForm"
class: "text-info hover:text-info-focus"
# Sponsor Menu # Sponsor Menu
sponsorDashboard: sponsorDashboard:
title: "Sponsor Dashboard" title: "Sponsor Dashboard"
@ -74,7 +81,7 @@ categories:
officer: officer:
title: "Officer Menu" title: "Officer Menu"
sections: ["eventManagement"] sections: ["eventManagement", "eventRequestForm"]
role: "general" role: "general"
executive: executive: