= ({ it
- {formatPrice(item.unit_price, {
+ {formatPrice((item.unit_price || 0), {
currency: cart.region?.currency_code,
})}
diff --git a/apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummaryTotals.tsx b/apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummaryTotals.tsx
index c590155a1..10d6068a5 100644
--- a/apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummaryTotals.tsx
+++ b/apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummaryTotals.tsx
@@ -21,12 +21,23 @@ const CheckoutOrderSummaryTotalsItem: FC =
amount,
className,
region,
-}) => (
-
-
{label}
- {formatPrice(amount || 0, { currency: region?.currency_code })}
-
-);
+}) => {
+ console.log('💰 CheckoutOrderSummaryTotalsItem Debug:', {
+ label,
+ amount,
+ currency: region?.currency_code
+ });
+
+ // Medusa stores prices in dollars
+ const amountInDollars = amount || 0;
+
+ return (
+
+
{label}
+ {formatPrice(amountInDollars, { currency: region?.currency_code })}
+
+ );
+};
export const CheckoutOrderSummaryTotals: FC = ({ shippingOptions, cart }) => {
const shippingMethods = cart.shipping_methods || [];
@@ -37,6 +48,17 @@ export const CheckoutOrderSummaryTotals: FC = (
const cartTotal = cart.total ?? 0;
const total = hasShippingMethod ? cartTotal : cartTotal + estimatedShipping;
+ console.log('💰 CheckoutOrderSummaryTotals Debug:', {
+ itemSubtotal: cart.item_subtotal,
+ total: cart.total,
+ shippingTotal: cart.shipping_total,
+ taxTotal: cart.tax_total,
+ discountTotal: cart.discount_total,
+ estimatedShipping,
+ hasShippingMethod,
+ finalTotal: total
+ });
+
return (
diff --git a/apps/storefront/app/components/checkout/MedusaStripeAddress/MedusaStripeAddress.tsx b/apps/storefront/app/components/checkout/MedusaStripeAddress/MedusaStripeAddress.tsx
index cdc476143..5d95f9e40 100644
--- a/apps/storefront/app/components/checkout/MedusaStripeAddress/MedusaStripeAddress.tsx
+++ b/apps/storefront/app/components/checkout/MedusaStripeAddress/MedusaStripeAddress.tsx
@@ -43,7 +43,17 @@ export const MedusaStripeAddress: FC
= ({
setAddress,
}) => {
const { env } = useEnv();
- const { cart } = useCheckout();
+
+ // Add null check for checkout context
+ let checkoutData;
+ try {
+ checkoutData = useCheckout();
+ } catch (error) {
+ console.error('Checkout context error:', error);
+ return Loading checkout...
;
+ }
+
+ const { cart } = checkoutData;
const { region } = useRegion();
const handleChange = (
diff --git a/apps/storefront/app/components/checkout/checkout-fields/ShippingOptionsRadioGroup/ShippingOptionsRadioGroupOption.tsx b/apps/storefront/app/components/checkout/checkout-fields/ShippingOptionsRadioGroup/ShippingOptionsRadioGroupOption.tsx
index fbaf6ad26..01911d23d 100644
--- a/apps/storefront/app/components/checkout/checkout-fields/ShippingOptionsRadioGroup/ShippingOptionsRadioGroupOption.tsx
+++ b/apps/storefront/app/components/checkout/checkout-fields/ShippingOptionsRadioGroup/ShippingOptionsRadioGroupOption.tsx
@@ -38,7 +38,7 @@ export const ShippingOptionsRadioGroupOption: FC
-
{formatPrice(shippingOption.amount, { currency: region.currency_code })}
+
{formatPrice((shippingOption.amount || 0), { currency: region.currency_code })}
= ({
+ className,
+ backgroundClassName,
+ actionsClassName,
+ chefName = "Chef Luis Velez",
+ tagline = "CULINARY EXPERIENCES & PRIVATE DINING",
+ description = "Transform your special occasions into unforgettable culinary experiences. From intimate cooking classes to elegant plated dinners, I bring restaurant-quality cuisine directly to your home.",
+ image = {
+ url: '/assets/images/chef_scallops_home.PNG',
+ alt: 'Chef Luis Velez preparing an elegant dish'
+ },
+ actions = [
+ {
+ label: 'Browse Our Menus',
+ url: '/menus',
+ },
+ {
+ label: 'Request an Event',
+ url: '/request',
+ },
+ ]
+}) => {
+ return (
+ <>
+ {image?.url &&
}
+
+
+
+
+
{tagline}
+
{chefName}
+
+ {description}
+
+
+
+ {!!actions?.length && (
+
+ )}
+
+
+ >
+ );
+};
+
+export default ChefHero;
\ No newline at end of file
diff --git a/apps/storefront/app/components/chef/ExperienceTypes.tsx b/apps/storefront/app/components/chef/ExperienceTypes.tsx
new file mode 100644
index 000000000..eab8a89c9
--- /dev/null
+++ b/apps/storefront/app/components/chef/ExperienceTypes.tsx
@@ -0,0 +1,206 @@
+import { Container } from '@app/components/common/container/Container';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import { Image } from '@app/components/common/images/Image';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface ExperienceTypesProps {
+ className?: string;
+ title?: string;
+ description?: string;
+}
+
+interface ExperienceType {
+ id: string;
+ name: string;
+ price: string;
+ description: string;
+ highlights: string[];
+ icon: string;
+ idealFor: string;
+ duration: string;
+}
+
+const experienceTypes: ExperienceType[] = [
+ {
+ id: 'buffet_style',
+ name: 'Buffet Style',
+ price: '$99.99',
+ description: 'Perfect for larger gatherings and casual entertaining. A variety of dishes served buffet-style, allowing guests to mingle and enjoy at their own pace.',
+ highlights: [
+ 'Multiple dishes and appetizers',
+ 'Self-service dining style',
+ 'Great for mingling',
+ 'Flexible timing'
+ ],
+ icon: 'https://images.unsplash.com/photo-1504674900247-0877df9cc836?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=100&q=80',
+ idealFor: 'Birthday parties, family gatherings, casual celebrations',
+ duration: '2.5 hours'
+ },
+ {
+ id: 'cooking_class',
+ name: 'Cooking Class',
+ price: '$119.99',
+ description: 'An interactive culinary experience where you learn professional techniques while preparing a delicious meal together.',
+ highlights: [
+ 'Hands-on cooking instruction',
+ 'Learn professional techniques',
+ 'Interactive experience',
+ 'Take home new skills'
+ ],
+ icon: 'https://images.unsplash.com/photo-1556908114-574ce6b1d42a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=100&q=80',
+ idealFor: 'Date nights, team building, skill development',
+ duration: '3 hours'
+ },
+ {
+ id: 'plated_dinner',
+ name: 'Plated Dinner',
+ price: '$149.99',
+ description: 'An elegant, restaurant-quality dining experience with multiple courses served individually. Perfect for special occasions.',
+ highlights: [
+ 'Multi-course tasting menu',
+ 'Restaurant-quality presentation',
+ 'Full-service dining',
+ 'Premium ingredients'
+ ],
+ icon: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=100&q=80',
+ idealFor: 'Anniversaries, proposals, formal celebrations',
+ duration: '4 hours'
+ }
+];
+
+interface ExperienceCardProps {
+ experience: ExperienceType;
+ className?: string;
+ featured?: boolean;
+}
+
+const ExperienceCard: FC
= ({ experience, className, featured = false }) => {
+ return (
+
+ {featured && (
+
+
+ Most Popular
+
+
+ )}
+
+
+
+
+
+
+
+
+ {experience.name}
+
+
+ {experience.price}
+
+
per person
+
+
+
+ {experience.description}
+
+
+
+
What's Included:
+
+ {experience.highlights.map((highlight, index) => (
+
+
+ {highlight}
+
+ ))}
+
+
+
+
+
+ Duration:
+ {experience.duration}
+
+
+ Ideal for: {experience.idealFor}
+
+
+
+
+
+
+ );
+};
+
+export const ExperienceTypes: FC = ({
+ className,
+ title = "Choose Your Culinary Experience",
+ description = "Each experience is carefully crafted to match the occasion. All prices are per person with no hidden fees or deposits required."
+}) => {
+ return (
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {experienceTypes.map((experience, index) => (
+
+ ))}
+
+
+
+
+
+ Not sure which experience is right for you?
+
+
+ Let us help you choose the perfect culinary experience for your occasion.
+ Let's start with selecting a menu...
+
+
+
+
+
+ );
+};
+
+export default ExperienceTypes;
\ No newline at end of file
diff --git a/apps/storefront/app/components/chef/FeaturedMenus.tsx b/apps/storefront/app/components/chef/FeaturedMenus.tsx
new file mode 100644
index 000000000..dac5cdeab
--- /dev/null
+++ b/apps/storefront/app/components/chef/FeaturedMenus.tsx
@@ -0,0 +1,79 @@
+import { Container } from '@app/components/common/container/Container';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import { MenuCarousel } from '@app/components/menu/MenuCarousel';
+import type { StoreMenuDTO } from '@app/../types/menus';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface FeaturedMenusProps {
+ className?: string;
+ title?: string;
+ description?: string;
+ menus: StoreMenuDTO[];
+ maxDisplay?: number;
+}
+
+export const FeaturedMenus: FC = ({
+ className,
+ title = "Featured Menu Collections",
+ description = "Discover our carefully crafted menu templates, each designed to create memorable culinary experiences for your special occasions.",
+ menus,
+ maxDisplay
+}) => {
+ return (
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ {menus.length > 0 ? (
+ <>
+
+
+
+ >
+ ) : (
+
+
+
+ Menus Coming Soon
+
+
+ We're crafting exceptional menu templates for your culinary experiences.
+ In the meantime, you can request a custom event.
+
+
+
+
+ )}
+
+ );
+};
+
+export default FeaturedMenus;
\ No newline at end of file
diff --git a/apps/storefront/app/components/chef/HowItWorks.tsx b/apps/storefront/app/components/chef/HowItWorks.tsx
new file mode 100644
index 000000000..e6ed151ce
--- /dev/null
+++ b/apps/storefront/app/components/chef/HowItWorks.tsx
@@ -0,0 +1,211 @@
+import { Container } from '@app/components/common/container/Container';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface HowItWorksProps {
+ className?: string;
+ title?: string;
+ description?: string;
+}
+
+interface ProcessStep {
+ step: number;
+ title: string;
+ description: string;
+ timeline: string;
+ details: string[];
+ icon: string;
+}
+
+const processSteps: ProcessStep[] = [
+ {
+ step: 1,
+ title: "Browse & Request",
+ description: "Explore our menu collections and choose your preferred experience type. Submit a request with your event details.",
+ timeline: "5 minutes",
+ details: [
+ "Browse available menu templates",
+ "Select experience type (Buffet, Cooking Class, or Plated Dinner)",
+ "Choose your date, time, and party size",
+ "Provide event location and special requirements"
+ ],
+ icon: "🍽️"
+ },
+ {
+ step: 2,
+ title: "Chef Review & Approval",
+ description: "Chef Luis reviews your request and confirms availability. You'll receive a detailed proposal with menu customizations.",
+ timeline: "24-48 hours",
+ details: [
+ "Chef reviews your event requirements",
+ "Availability and logistics confirmation",
+ "Menu customization based on preferences",
+ "Detailed proposal with final pricing"
+ ],
+ icon: "👨🍳"
+ },
+ {
+ step: 3,
+ title: "Book & Purchase",
+ description: "Once approved, your event becomes available for purchase. Buy tickets for your guests with secure payment processing.",
+ timeline: "Immediate",
+ details: [
+ "Event becomes available as a product",
+ "Secure online ticket purchasing",
+ "Automatic confirmation emails",
+ "Guest management tools"
+ ],
+ icon: "🎫"
+ },
+ {
+ step: 4,
+ title: "Experience & Enjoy",
+ description: "Chef Luis arrives at your location with all ingredients and equipment. Relax and enjoy your personalized culinary experience.",
+ timeline: "Event day",
+ details: [
+ "Chef arrives with all necessary equipment",
+ "Fresh, premium ingredients sourced locally",
+ "Professional preparation and service",
+ "Cleanup included in service"
+ ],
+ icon: "🎉"
+ }
+];
+
+interface StepCardProps {
+ step: ProcessStep;
+ isLast?: boolean;
+ className?: string;
+}
+
+const StepCard: FC = ({ step, isLast = false, className }) => {
+ return (
+
+ {/* Connection line to next step */}
+ {!isLast && (
+
+ )}
+
+
+
+ {/* Step number and icon */}
+
+
+ {step.step}
+
+
{step.icon}
+
+
+ {/* Step title and timeline */}
+
+
+ {step.title}
+
+
+ {step.timeline}
+
+
+
+ {/* Description */}
+
+ {step.description}
+
+
+ {/* Details list */}
+
+
Key Points:
+
+ {step.details.map((detail, index) => (
+
+
+ {detail}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+export const HowItWorks: FC = ({
+ className,
+ title = "How It Works",
+ description = "From browsing menus to enjoying your culinary experience, we've made the process simple and transparent. Here's how your culinary journey unfolds:"
+}) => {
+ return (
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+ {processSteps.map((step, index) => (
+
+ ))}
+
+
+ {/* Call to action section */}
+
+
+
+ Ready to Start Your Culinary Journey?
+
+
+ Begin by exploring our menu collections or jump straight to requesting
+ your custom culinary experience. No commitments until the chef approves your event.
+
+
+
+
+
+ {/* FAQ section */}
+
+
+ Frequently Asked Questions
+
+
+
+
How far in advance should I book?
+
We recommend booking 1-2 weeks in advance, especially for weekend events. However, we can often accommodate shorter notice requests.
+
+
+
+
What's included in the service?
+
All ingredients, equipment, preparation, service, and cleanup are included. You just provide the location and we handle the rest.
+
+
+
+
Can menus be customized?
+
Absolutely! Chef Elena works with you to customize any menu based on dietary restrictions, preferences, and seasonal availability.
+
+
+
+
+ );
+};
+
+export default HowItWorks;
\ No newline at end of file
diff --git a/apps/storefront/app/components/common/remix-hook-form/field-groups/QuantitySelector.tsx b/apps/storefront/app/components/common/remix-hook-form/field-groups/QuantitySelector.tsx
index 52ce5216c..9f78a53e5 100644
--- a/apps/storefront/app/components/common/remix-hook-form/field-groups/QuantitySelector.tsx
+++ b/apps/storefront/app/components/common/remix-hook-form/field-groups/QuantitySelector.tsx
@@ -10,9 +10,10 @@ interface QuantitySelectorProps {
className?: string;
formId?: string;
onChange?: (quantity: number) => void;
+ customInventoryQuantity?: number; // New prop for custom inventory quantity
}
-export const QuantitySelector: FC = ({ className, variant, maxInventory = 10, onChange }) => {
+export const QuantitySelector: FC = ({ className, variant, maxInventory = 10, onChange, customInventoryQuantity }) => {
const formContext = useRemixFormContext();
if (!formContext) {
@@ -22,8 +23,19 @@ export const QuantitySelector: FC = ({ className, variant
const { control } = formContext;
- const variantInventory =
- variant?.manage_inventory && !variant.allow_backorder ? variant.inventory_quantity || 0 : maxInventory;
+ const variantInventory = customInventoryQuantity !== undefined
+ ? customInventoryQuantity
+ : (variant?.manage_inventory && !variant.allow_backorder ? variant.inventory_quantity || 0 : maxInventory);
+
+ // Debug logging for inventory calculation issues
+ if (customInventoryQuantity !== undefined && variant?.inventory_quantity === 0) {
+ console.log('🎫 QuantitySelector using custom inventory quantity:', {
+ variantId: variant?.id,
+ variantInventoryQuantity: variant?.inventory_quantity,
+ customInventoryQuantity,
+ calculatedVariantInventory: variantInventory
+ });
+ }
const optionsArray = [...Array(Math.min(variantInventory, maxInventory))].map((_, index) => ({
label: `${index + 1}`,
@@ -35,15 +47,15 @@ export const QuantitySelector: FC = ({ className, variant
name="quantity"
control={control}
render={({ field }) => (
-
+
Quantity
-
Qty
+
Tickets
{
const value = parseInt(e.target.value, 10);
@@ -53,7 +65,7 @@ export const QuantitySelector: FC = ({ className, variant
>
{optionsArray.map((option) => (
- {option.label}
+ {option.label} {option.value === 1 ? 'Ticket' : 'Tickets'}
))}
diff --git a/apps/storefront/app/components/event-request/ContactDetails.tsx b/apps/storefront/app/components/event-request/ContactDetails.tsx
new file mode 100644
index 000000000..adf3704e5
--- /dev/null
+++ b/apps/storefront/app/components/event-request/ContactDetails.tsx
@@ -0,0 +1,180 @@
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface ContactDetailsProps {
+ className?: string;
+}
+
+export const ContactDetails: FC
= ({ className }) => {
+ const { watch, setValue, formState: { errors } } = useFormContext();
+ const firstName = watch('firstName');
+ const lastName = watch('lastName');
+ const email = watch('email');
+ const phone = watch('phone');
+
+ // Format phone number as user types
+ const formatPhoneNumber = (value: string) => {
+ // Remove all non-digits
+ const digits = value.replace(/\D/g, '');
+
+ // Format as (XXX) XXX-XXXX
+ if (digits.length <= 3) {
+ return digits;
+ } else if (digits.length <= 6) {
+ return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
+ } else {
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6, 10)}`;
+ }
+ };
+
+ const handlePhoneChange = (value: string) => {
+ const formatted = formatPhoneNumber(value);
+ setValue('phone', formatted, { shouldValidate: true });
+ };
+
+ const handleInputChange = (field: keyof EventRequestFormData, value: string) => {
+ setValue(field, value, { shouldValidate: true });
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Contact Information
+
+
+ Please provide your contact details so Chef Luis can reach you about your event request.
+
+
+
+ {/* Name fields */}
+
+
+
+ First Name *
+
+
handleInputChange('firstName', e.target.value)}
+ placeholder="Enter your first name"
+ className={clsx(
+ "w-full px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.firstName
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ />
+ {errors.firstName && (
+
+ {errors.firstName.message}
+
+ )}
+
+
+
+
+ Last Name *
+
+
handleInputChange('lastName', e.target.value)}
+ placeholder="Enter your last name"
+ className={clsx(
+ "w-full px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.lastName
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ />
+ {errors.lastName && (
+
+ {errors.lastName.message}
+
+ )}
+
+
+
+ {/* Email field */}
+
+
+ Email Address *
+
+
handleInputChange('email', e.target.value)}
+ placeholder="Enter your email address"
+ className={clsx(
+ "w-full px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.email
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ />
+
+ Used for event confirmations and communication with Chef Luis
+
+ {errors.email && (
+
+ {errors.email.message}
+
+ )}
+
+
+ {/* Phone field */}
+
+
+ Phone Number (Optional)
+
+
handlePhoneChange(e.target.value)}
+ placeholder="(555) 123-4567"
+ maxLength={14}
+ className={clsx(
+ "w-full px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.phone
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ />
+
+ For quick communication and day-of-event coordination
+
+ {errors.phone && (
+
+ {errors.phone.message}
+
+ )}
+
+
+ {/* Contact summary */}
+ {firstName && lastName && email && (
+
+
+ Contact Summary
+
+
+
Name: {firstName} {lastName}
+
Email: {email}
+ {phone &&
Phone: {phone}
}
+
+
+ )}
+
+ {/* Hidden form fields */}
+
+
+
+
+
+ );
+};
+
+export default ContactDetails;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/DateTimeForm.tsx b/apps/storefront/app/components/event-request/DateTimeForm.tsx
new file mode 100644
index 000000000..69f4382dd
--- /dev/null
+++ b/apps/storefront/app/components/event-request/DateTimeForm.tsx
@@ -0,0 +1,255 @@
+import { useState, useEffect } from 'react';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface DateTimeFormProps {
+ className?: string;
+}
+
+// Time slots available for booking
+const TIME_SLOTS = [
+ '10:00', '10:30', '11:00', '11:30',
+ '12:00', '12:30', '13:00', '13:30',
+ '14:00', '14:30', '15:00', '15:30',
+ '16:00', '16:30', '17:00', '17:30',
+ '18:00', '18:30', '19:00', '19:30',
+ '20:00', '20:30'
+];
+
+// Popular time slots for quick selection
+const POPULAR_TIMES = ['12:00', '17:00', '18:00', '19:00'];
+
+export const DateTimeForm: FC = ({ className }) => {
+ const { watch, setValue, formState: { errors } } = useFormContext();
+ const selectedDate = watch('requestedDate');
+ const selectedTime = watch('requestedTime');
+
+ // Calculate minimum date (7 days from now)
+ const getMinDate = () => {
+ const today = new Date();
+ today.setDate(today.getDate() + 7);
+ return today.toISOString().split('T')[0];
+ };
+
+ // Calculate maximum date (6 months from now)
+ const getMaxDate = () => {
+ const maxDate = new Date();
+ maxDate.setMonth(maxDate.getMonth() + 6);
+ return maxDate.toISOString().split('T')[0];
+ };
+
+ const [minDate] = useState(getMinDate());
+ const [maxDate] = useState(getMaxDate());
+
+ // Helpers to parse a YYYY-MM-DD string as a LOCAL date (avoid UTC off-by-one)
+ const parseLocalDate = (dateString: string) => {
+ const [y, m, d] = dateString.split('-').map(Number);
+ return new Date(y, (m || 1) - 1, d || 1);
+ };
+
+ // Format date for display (local)
+ const formatDateForDisplay = (dateString: string) => {
+ if (!dateString) return '';
+ const date = parseLocalDate(dateString);
+ return date.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ // Format time for display
+ const formatTimeForDisplay = (timeString: string) => {
+ if (!timeString) return '';
+ const [hours, minutes] = timeString.split(':');
+ const hour = parseInt(hours);
+ const ampm = hour >= 12 ? 'PM' : 'AM';
+ const displayHour = hour % 12 || 12;
+ return `${displayHour}:${minutes} ${ampm}`;
+ };
+
+ // Check if selected date is weekend (local)
+ const isWeekend = (dateString: string) => {
+ if (!dateString) return false;
+ const date = parseLocalDate(dateString);
+ const day = date.getDay();
+ return day === 0 || day === 6; // Sunday or Saturday
+ };
+
+ const handleDateChange = (date: string) => {
+ setValue('requestedDate', date, { shouldValidate: true });
+ };
+
+ const handleTimeChange = (time: string) => {
+ setValue('requestedTime', time, { shouldValidate: true });
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Select Your Preferred Date & Time
+
+
+ Choose when you'd like Chef Luis to arrive. He typically needs about 2 hours before guests sit down to eat. Events require minimum 7 days advance notice.
+
+
+
+ {/* Date Selection */}
+
+
+
+ Preferred Date
+
+
+
handleDateChange(e.target.value)}
+ className={clsx(
+ "w-full text-lg px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.requestedDate
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ />
+
+ {/* Date display and info */}
+ {selectedDate && (
+
+
+ Selected: {formatDateForDisplay(selectedDate)}
+
+ {isWeekend(selectedDate) && (
+
+ 💡 Weekend events are very popular! Consider booking early.
+
+ )}
+
+ )}
+
+ {/* Error message */}
+ {errors.requestedDate && (
+
+ {errors.requestedDate.message}
+
+ )}
+
+
+
+ {/* Time Selection */}
+
+
+
+ Preferred Start Time
+
+
+ {/* Popular times quick selection */}
+
+
Popular times:
+
+ {POPULAR_TIMES.map((time) => (
+ handleTimeChange(time)}
+ className={clsx(
+ "px-3 py-2 rounded-lg text-sm font-medium transition-colors",
+ selectedTime === time
+ ? "bg-accent-500 text-white"
+ : "bg-accent-100 text-accent-700 hover:bg-accent-200"
+ )}
+ >
+ {formatTimeForDisplay(time)}
+
+ ))}
+
+
+
+ {/* Full time dropdown */}
+
handleTimeChange(e.target.value)}
+ className={clsx(
+ "w-full text-lg px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500",
+ errors.requestedTime
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
+ : "border-gray-300"
+ )}
+ >
+ Select a time...
+ {TIME_SLOTS.map((time) => (
+
+ {formatTimeForDisplay(time)}
+
+ ))}
+
+
+ {/* Time display and info */}
+ {selectedTime && (
+
+
+ Selected: {formatTimeForDisplay(selectedTime)}
+
+
+ This is the chef arrival time. Plan for dining to start roughly 2 hours later.
+
+
+ )}
+
+ {/* Error message */}
+ {errors.requestedTime && (
+
+ {errors.requestedTime.message}
+
+ )}
+
+
+
+ {/* Date & Time Summary */}
+ {selectedDate && selectedTime && (
+
+
+
+ Event Schedule
+
+
+
+ Date: {formatDateForDisplay(selectedDate)}
+
+
+ Start Time: {formatTimeForDisplay(selectedTime)}
+
+
+
+
+ )}
+
+ {/* Important scheduling information */}
+
+
+ Scheduling Information
+
+
+ • Minimum 7 days advance notice required
+ • Events can be scheduled up to 6 months in advance
+ • Start times available from 10:00 AM to 8:30 PM
+ • Chef Luis will confirm availability within 24 hours
+ • Alternative dates may be suggested if requested time is unavailable
+
+
+
+ {/* Hidden form fields */}
+
+
+
+ );
+};
+
+export default DateTimeForm;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/EventRequestForm.tsx b/apps/storefront/app/components/event-request/EventRequestForm.tsx
new file mode 100644
index 000000000..d1c44e9f8
--- /dev/null
+++ b/apps/storefront/app/components/event-request/EventRequestForm.tsx
@@ -0,0 +1,465 @@
+import { useState, useEffect } from 'react';
+import { Button } from '@app/components/common/buttons/Button';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import type { StoreMenuDTO } from '@app/../types/menus';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { eventRequestSchema } from '@app/routes/request._index';
+import { useActionData } from 'react-router';
+import clsx from 'clsx';
+import type { FC } from 'react';
+import { Disclosure } from '@headlessui/react';
+import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon';
+
+// Real form step components
+import { MenuSelector } from './MenuSelector';
+import { EventTypeSelector } from './EventTypeSelector';
+import { PartySizeSelector } from './PartySizeSelector';
+import { DateTimeForm } from './DateTimeForm';
+import { LocationForm } from './LocationForm';
+import { ContactDetails } from './ContactDetails';
+import { SpecialRequests } from './SpecialRequests';
+import { RequestSummary } from './RequestSummary';
+
+export interface EventRequestFormProps {
+ menus: StoreMenuDTO[];
+ initialValues?: Partial;
+}
+
+// Type for action response
+interface ActionResponse {
+ success?: boolean;
+ eventId?: string;
+ redirectTo?: string;
+ message?: string;
+ errors?: any;
+}
+
+const STEPS = [
+ { id: 1, title: 'Experience & Menu', subtitle: 'Choose your culinary experience, select a menu template, and tell us how many guests' },
+ { id: 2, title: 'Schedule, Contact & Location', subtitle: 'Select date/time, provide contact info, and enter the event address' },
+ { id: 3, title: 'Special Requests', subtitle: 'Any dietary restrictions or notes?' },
+ { id: 4, title: 'Review & Submit', subtitle: 'Confirm your event details' },
+];
+
+export const EventRequestForm: FC = ({
+ menus,
+ initialValues = {}
+}) => {
+ const [currentStep, setCurrentStep] = useState(1);
+ const actionData = useActionData() as ActionResponse;
+
+ const form = useRemixForm({
+ resolver: zodResolver(eventRequestSchema),
+ defaultValues: {
+ currentStep: 1,
+ partySize: 4, // Default party size
+ ...initialValues,
+ },
+ mode: 'onChange', // Validate on change for better UX
+ });
+
+ // Log action data for debugging
+ useEffect(() => {
+ console.log('🎯 FORM: useEffect triggered with actionData:', actionData);
+
+ if (actionData) {
+ console.log('🎯 FORM: Action data received:', actionData);
+ }
+ }, [actionData]);
+
+ const nextStep = () => {
+ if (currentStep < STEPS.length) {
+ setCurrentStep(currentStep + 1);
+ }
+ };
+
+ const prevStep = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const canProceed = () => {
+ const values = form.getValues();
+ const errors = form.formState.errors;
+
+ switch (currentStep) {
+ case 1:
+ // Experience required, party size required
+ return (
+ !!values.eventType &&
+ !errors.eventType &&
+ !!values.menuId &&
+ values.partySize >= 2 &&
+ values.partySize <= 50 &&
+ !errors.partySize
+ );
+ case 2:
+ // Date/time, contact, and location required
+ return (
+ !!values.requestedDate &&
+ !!values.requestedTime &&
+ !errors.requestedDate &&
+ !errors.requestedTime &&
+ !!values.firstName &&
+ !!values.lastName &&
+ !!values.email &&
+ !errors.firstName &&
+ !errors.lastName &&
+ !errors.email &&
+ (!values.phone || !errors.phone) &&
+ !!values.locationAddress &&
+ values.locationAddress.length >= 10 &&
+ !errors.locationAddress
+ );
+ case 3:
+ // Special requests optional but must be valid if provided
+ return !errors.specialRequirements && !errors.notes;
+ case 4:
+ // No validation errors on final step
+ return Object.keys(errors).length === 0;
+ default:
+ return false;
+ }
+ };
+
+ const isAllComplete = () => {
+ const v = form.getValues();
+ const e = form.formState.errors;
+ const step1 = !!v.menuId && !!v.eventType && !e.eventType && v.partySize >= 2 && v.partySize <= 50 && !e.partySize;
+ const step2Date = !!v.requestedDate && !!v.requestedTime && !e.requestedDate && !e.requestedTime;
+ const step2Contact = !!v.firstName && !!v.lastName && !!v.email && !e.firstName && !e.lastName && !e.email && (!v.phone || !e.phone);
+ const step2Location = !!v.locationAddress && v.locationAddress.length >= 10 && !e.locationAddress;
+ return step1 && step2Date && step2Contact && step2Location; // step3 (special requests) is optional
+ };
+
+ const renderSectionHeader = (label: string, opts?: { complete?: boolean; optional?: boolean }) => (
+
+
{label}
+
+ {opts?.optional ? 'Optional' : opts?.complete ? 'Complete' : 'Incomplete'}
+
+
+ );
+
+ const renderDisclosure = (
+ args: {
+ defaultOpen?: boolean;
+ header: React.ReactNode;
+ children: React.ReactNode;
+ }
+ ) => (
+
+ {({ open }) => (
+
+
+
+
+
+ {args.children}
+
+
+ )}
+
+ );
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return (
+
+ {(() => {
+ const v = form.getValues();
+ const e = form.formState.errors;
+ const isEventTypeComplete = !!v.eventType && !e.eventType;
+ const isPartySizeComplete = v.partySize >= 2 && v.partySize <= 50 && !e.partySize;
+ const isMenuSelected = !!v.menuId;
+
+ return (
+ <>
+ {renderDisclosure({
+ defaultOpen: true,
+ header: renderSectionHeader('Select a Menu', { complete: isMenuSelected }),
+ children:
,
+ })}
+
+ {renderDisclosure({
+ defaultOpen: false,
+ header: renderSectionHeader('Experience Type', { complete: isEventTypeComplete }),
+ children:
,
+ })}
+
+ {renderDisclosure({
+ defaultOpen: false,
+ header: renderSectionHeader('Number of Guests', { complete: isPartySizeComplete }),
+ children:
,
+ })}
+ >
+ );
+ })()}
+
+ );
+ case 2:
+ return (
+
+ {(() => {
+ const v = form.getValues();
+ const e = form.formState.errors;
+ const isDateComplete = !!v.requestedDate && !!v.requestedTime && !e.requestedDate && !e.requestedTime;
+ const isContactComplete = !!v.firstName && !!v.lastName && !!v.email && !e.firstName && !e.lastName && !e.email && (!v.phone || !e.phone);
+ const isLocationComplete = !!v.locationAddress && v.locationAddress.length >= 10 && !e.locationAddress;
+
+ return (
+ <>
+ {renderDisclosure({
+ defaultOpen: false,
+ header: renderSectionHeader('Date & Time', { complete: isDateComplete }),
+ children: ,
+ })}
+
+ {renderDisclosure({
+ defaultOpen: false,
+ header: renderSectionHeader('Contact Information', { complete: isContactComplete }),
+ children: ,
+ })}
+
+ {renderDisclosure({
+ defaultOpen: false,
+ header: renderSectionHeader('Event Address', { complete: isLocationComplete }),
+ children: ,
+ })}
+ >
+ );
+ })()}
+
+ );
+ case 3:
+ return ;
+ case 4:
+ return (
+ {
+ setCurrentStep(step);
+ // brief timeout to allow render then expand intended section
+ setTimeout(() => {
+ const sectionMap: Record = {
+ // Step 1
+ menu: ['Select a Menu'],
+ experience: ['Experience Type'],
+ guests: ['Number of Guests'],
+ // Step 2
+ date: ['Date & Time'],
+ contact: ['Contact Information'],
+ location: ['Event Address'],
+ // Step 3
+ special: ['Special Requests'],
+ };
+
+ const labels = section && sectionMap[section];
+ if (!labels) return;
+ // Find disclosure button by header text and click to open
+ labels.forEach((text) => {
+ const btn = Array.from(document.querySelectorAll('button'))
+ .find((b) => b.textContent?.trim().startsWith(text));
+ if (btn) (btn as HTMLButtonElement).click();
+ });
+ }, 0);
+ }}
+ onSubmit={() => {
+ console.log('🎯 FORM: Submit button clicked, triggering form submission');
+ console.log('🎯 FORM: Form values before submit:', form.getValues());
+ console.log('🎯 FORM: Form errors before submit:', form.formState.errors);
+ console.log('🎯 FORM: Form isValid:', form.formState.isValid);
+ console.log('🎯 FORM: Form isSubmitting:', form.formState.isSubmitting);
+
+ // Force update hidden inputs with current values
+ const formValues = form.getValues();
+ Object.entries(formValues).forEach(([key, value]) => {
+ const input = document.querySelector(`input[name="${key}"]`) as HTMLInputElement;
+ if (input && input.type === 'hidden') {
+ let processedValue = String(value || '');
+
+ if (key === 'requestedDate' && value) {
+ const requestedTime = formValues.requestedTime || '12:00';
+ const dateTime = new Date(`${value}T${requestedTime}:00`);
+ processedValue = dateTime.toISOString();
+ }
+
+ input.value = processedValue;
+ }
+ });
+
+ const form_element = document.querySelector('form') as HTMLFormElement;
+ if (form_element) {
+ const formData = new FormData(form_element);
+ console.log('📤 FORM: Actual FormData being submitted:', Array.from(formData.entries()));
+ form_element.requestSubmit();
+ }
+ }}
+ isSubmitting={form.formState.isSubmitting}
+ />
+ );
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {/* Progress Indicator */}
+
+
+ {STEPS.map((step, index) => (
+
+
= step.id
+ ? 'bg-accent-500 text-white'
+ : 'bg-gray-200 text-gray-600'
+ )}
+ onClick={() => setCurrentStep(step.id)}
+ >
+ {step.id}
+
+ {index < STEPS.length - 1 && (
+
step.id ? 'bg-accent-500' : 'bg-gray-200'
+ )}
+ />
+ )}
+
+ ))}
+
+
+
+
+ {STEPS[currentStep - 1].title}
+
+
+ {STEPS[currentStep - 1].subtitle}
+
+
+
+
+ {/* Form Content */}
+
+
+
+
+ );
+};
+
+export default EventRequestForm;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/EventTypeSelector.tsx b/apps/storefront/app/components/event-request/EventTypeSelector.tsx
new file mode 100644
index 000000000..b2001bcec
--- /dev/null
+++ b/apps/storefront/app/components/event-request/EventTypeSelector.tsx
@@ -0,0 +1,177 @@
+import { Button } from '@app/components/common/buttons/Button';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import { getEventTypeDisplayName } from '@libs/constants/pricing';
+import clsx from 'clsx';
+import type { FC } from 'react';
+import React from 'react';
+
+export interface EventTypeSelectorProps {
+ className?: string;
+}
+
+interface ExperienceType {
+ id: 'cooking_class' | 'plated_dinner' | 'buffet_style';
+ name: string;
+ description: string;
+ highlights: string[];
+ idealFor: string;
+ duration: string;
+ icon: string;
+ isMostPopular?: boolean;
+}
+
+const experienceTypes: ExperienceType[] = [
+ {
+ id: 'cooking_class',
+ name: 'Cooking Class',
+ description: 'Interactive culinary experience where you learn professional techniques',
+ highlights: [
+ 'Hands-on instruction',
+ 'Learn techniques',
+ 'Interactive experience'
+ ],
+ idealFor: 'Date nights, team building',
+ duration: '3 hours',
+ icon: '👨🍳'
+ },
+ {
+ id: 'plated_dinner',
+ name: 'Plated Dinner',
+ description: 'Elegant, restaurant-quality dining with multiple courses',
+ highlights: [
+ 'Multi-course menu',
+ 'Restaurant-quality',
+ 'Full-service dining'
+ ],
+ idealFor: 'Anniversaries, formal celebrations',
+ duration: '4 hours',
+ icon: '🍽️',
+ isMostPopular: true
+ },
+ {
+ id: 'buffet_style',
+ name: 'Buffet Style',
+ description: 'Perfect for larger gatherings with variety of dishes',
+ highlights: [
+ 'Multiple dishes',
+ 'Self-service style',
+ 'Great for mingling'
+ ],
+ idealFor: 'Birthday parties, family gatherings',
+ duration: '2.5 hours',
+ icon: '🥘'
+ }
+];
+
+export const EventTypeSelector: FC
= ({ className }) => {
+ const { watch, setValue } = useFormContext();
+ const selectedEventType = watch('eventType');
+
+ // Set default value to plated_dinner if no selection
+ React.useEffect(() => {
+ if (!selectedEventType) {
+ setValue('eventType', 'plated_dinner', { shouldValidate: true });
+ }
+ }, [selectedEventType, setValue]);
+
+ const handleEventTypeSelect = (eventType: ExperienceType['id']) => {
+ setValue('eventType', eventType, { shouldValidate: true });
+ };
+
+ const selectedExperience = selectedEventType
+ ? experienceTypes.find(e => e.id === selectedEventType)
+ : experienceTypes.find(e => e.id === 'plated_dinner');
+
+ return (
+
+ {/* Header */}
+
+
+ Select Your Culinary Experience
+
+
+ Choose the experience type that best fits your occasion.
+
+
+
+ {/* Experience Type Selector */}
+
+
+ {/* Dropdown Selection */}
+
+
+ Experience Type
+
+ handleEventTypeSelect(e.target.value as ExperienceType['id'])}
+ className="w-full px-4 py-3 border-2 rounded-lg focus:ring-2 focus:ring-accent-500 focus:border-accent-500 border-gray-300"
+ >
+ {experienceTypes.map((experience) => (
+
+ {experience.name} {experience.isMostPopular ? '(Most Popular)' : ''}
+
+ ))}
+
+
+
+ {/* Experience Details Card */}
+ {selectedExperience && (
+
+ {/* Most Popular Badge */}
+ {selectedExperience.isMostPopular && (
+
+
+ Most Popular
+
+
+ )}
+
+
+ {/* Icon */}
+
+ {selectedExperience.icon}
+
+
+ {/* Title and Duration */}
+
+
+ {selectedExperience.name}
+
+
{selectedExperience.duration}
+
+
+ {/* Description */}
+
+ {selectedExperience.description}
+
+
+ {/* What's included */}
+
+
What's Included:
+
+ {selectedExperience.highlights.map((highlight, index) => (
+
+
+ {highlight}
+
+ ))}
+
+
+
+ {/* Ideal for */}
+
+
Ideal For:
+
{selectedExperience.idealFor}
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default EventTypeSelector;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/LocationForm.tsx b/apps/storefront/app/components/event-request/LocationForm.tsx
new file mode 100644
index 000000000..4157df592
--- /dev/null
+++ b/apps/storefront/app/components/event-request/LocationForm.tsx
@@ -0,0 +1,195 @@
+import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+interface Address {
+ address1: string;
+ address2: string;
+ city: string;
+ province: string;
+ countryCode: string;
+ postalCode: string;
+}
+
+interface AddressData {
+ address: Address;
+ completed: boolean;
+}
+
+export interface LocationFormProps {
+ className?: string;
+}
+
+export const LocationForm: FC = ({ className }) => {
+ const { watch, setValue } = useFormContext();
+
+ // Address-only structure (contact details collected separately)
+ const [address, setAddress] = useState({
+ address: {
+ address1: '',
+ address2: '',
+ city: '',
+ province: '',
+ countryCode: 'us',
+ postalCode: '',
+ },
+ completed: false,
+ });
+
+ const handleAddressChange = (field: keyof Address, value: string) => {
+ const newAddress = {
+ ...address,
+ address: {
+ ...address.address,
+ [field]: value,
+ },
+ };
+ setAddress(newAddress);
+
+ // Required address fields
+ const requiredFields: (keyof Address)[] = ['address1', 'city', 'province', 'postalCode'];
+ newAddress.completed = requiredFields.every((f) => !!newAddress.address[f]);
+
+ // Format address for form submission
+ const formattedAddress = [
+ newAddress.address.address1,
+ newAddress.address.address2,
+ newAddress.address.city,
+ newAddress.address.province,
+ newAddress.address.postalCode,
+ newAddress.address.countryCode,
+ ]
+ .filter(Boolean)
+ .join(', ');
+
+ setValue('locationAddress', formattedAddress, { shouldValidate: true });
+ };
+
+ return (
+
+ {/* Header */}
+
+
Event Location
+
Please provide the address where your culinary experience will take place.
+
+
+ {/* Address Form */}
+
+
Event Location Address
+
+
+
+
+ {/* Address Guidelines */}
+
+
📍 Address Guidelines
+
+ • Please provide the complete address where the event will take place
+ • Include apartment/unit number if applicable
+ • Chef Luis will arrive with all necessary equipment and ingredients
+ • Travel within 30 miles is included in the service
+
+
+
+ {/* Selected address summary */}
+ {address.address.address1 && (
+
+
+
+
Event Location
+
+ {address.address.address1}
+ {address.address.address2 && `, ${address.address.address2}`}
+ {address.address.city && `, ${address.address.city}`}
+ {address.address.province && `, ${address.address.province}`}
+ {address.address.postalCode && ` ${address.address.postalCode}`}
+
+
+
{
+ setAddress({
+ address: {
+ address1: '',
+ address2: '',
+ city: '',
+ province: '',
+ countryCode: 'us',
+ postalCode: '',
+ },
+ completed: false,
+ });
+ setValue('locationAddress', '');
+ }}
+ className="text-accent-600 text-sm hover:text-accent-700 flex-shrink-0 ml-4"
+ >
+ Change
+
+
+
+ )}
+
+ {/* Hidden form fields */}
+
+
+ );
+};
+
+export default LocationForm;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/MenuSelector.tsx b/apps/storefront/app/components/event-request/MenuSelector.tsx
new file mode 100644
index 000000000..60056ac9c
--- /dev/null
+++ b/apps/storefront/app/components/event-request/MenuSelector.tsx
@@ -0,0 +1,202 @@
+import { useEffect, useState } from 'react';
+import { Image } from '@app/components/common/images/Image';
+import { Button } from '@app/components/common/buttons/Button';
+import type { StoreMenuDTO } from '@app/../types/menus';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import clsx from 'clsx';
+import type { FC } from 'react';
+import { ScrollArrowButtons } from '@app/components/common/buttons/ScrollArrowButtons';
+import { useScrollArrows } from '@app/hooks/useScrollArrows';
+import { MenuGridSkeleton } from '@app/components/menu/MenuGridSkeleton';
+import { Modal } from '@app/components/common/modals/Modal';
+
+export interface MenuSelectorProps {
+ menus: StoreMenuDTO[];
+}
+
+interface MenuCardProps {
+ menu: StoreMenuDTO;
+ isSelected: boolean;
+ onSelect: (menuId: string) => void;
+ onPreview: (menu: StoreMenuDTO) => void;
+}
+
+const MenuCard: FC = ({ menu, isSelected, onSelect, onPreview }) => {
+ const courseCount = menu.courses?.length || 0;
+ const estimatedTime = '3-4 hours'; // Default estimate
+ const courseNames = (menu.courses || [])
+ .map((c) => c.name)
+ .filter((n): n is string => !!n && n.length > 0)
+ .slice(0, 3);
+
+ return (
+ onSelect(menu.id)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') onSelect(menu.id);
+ }}
+ >
+ {/* Selection indicator */}
+
+
+ {/* Menu image */}
+
+
+
+
+ {/* Menu details */}
+
+
+
{menu.name}
+
+ {courseCount} courses • {estimatedTime}
+
+
+
+ {courseNames.length > 0 && (
+
+ {courseNames.map((n, idx) => (
+
+
+ {n}
+
+ ))}
+
+ )}
+
+
+
+ {
+ e.stopPropagation();
+ onPreview(menu);
+ }}
+ className="text-accent-700 hover:text-accent-800"
+ >
+ View full menu
+
+
+
+ );
+};
+
+export const MenuSelector: FC = ({ menus }) => {
+ const { watch, setValue } = useFormContext();
+ const selectedMenuId = watch('menuId');
+ const [preview, setPreview] = useState(null);
+
+ // Ensure there is always a selected menu (first by default)
+ useEffect(() => {
+ if (!selectedMenuId && menus?.length) {
+ setValue('menuId', menus[0].id, { shouldValidate: true });
+ }
+ }, [menus, selectedMenuId, setValue]);
+
+ const { scrollableDivRef, ...scrollArrowProps } = useScrollArrows({
+ buffer: 100,
+ resetOnDepChange: [menus],
+ });
+
+ if (!menus) return ;
+
+ const handleSelect = (menuId: string) => setValue('menuId', menuId, { shouldValidate: true });
+
+ return (
+
+ {/* Header */}
+
+
Select a Menu Template
+
Scroll horizontally or use the arrows to choose a menu.
+
+
+ {/* Carousel */}
+
+
+ {menus.map((menu) => (
+
+ setPreview(m)}
+ />
+
+ ))}
+
+
+
+
+ {/* Selected indicator */}
+ {selectedMenuId && (
+
+ Selected menu: {menus.find((m) => m.id === selectedMenuId)?.name}
+
+ )}
+
+ {/* Menu preview modal */}
+ {preview && (
+
setPreview(null)}>
+
+
{preview.name}
+
+ {preview.courses?.length || 0} courses • 3-4 hours
+
+
+
+ {(preview.courses || []).map((course, idx) => (
+
+
{course.name || `Course ${idx + 1}`}
+
+ {(course.dishes || []).map((d, i) => (
+
+
+ {d.name}
+
+ ))}
+
+
+ ))}
+
+
+
+ setPreview(null)}>Close
+
+
+
+ )}
+
+ );
+};
+
+export default MenuSelector;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/PartySizeSelector.tsx b/apps/storefront/app/components/event-request/PartySizeSelector.tsx
new file mode 100644
index 000000000..56e078cfc
--- /dev/null
+++ b/apps/storefront/app/components/event-request/PartySizeSelector.tsx
@@ -0,0 +1,192 @@
+import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import { PRICING_STRUCTURE, getEventTypeDisplayName } from '@libs/constants/pricing';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface PartySizeSelectorProps {
+ className?: string;
+}
+
+const PARTY_SIZE_PRESETS = [2, 4, 6, 8, 10, 12];
+const MIN_PARTY_SIZE = 2;
+const MAX_PARTY_SIZE = 50;
+
+export const PartySizeSelector: FC = ({ className }) => {
+ const { watch, setValue, formState: { errors } } = useFormContext();
+ const partySize = watch('partySize') || 4;
+ const eventType = watch('eventType');
+
+ const [inputValue, setInputValue] = useState(partySize.toString());
+
+ // Calculate pricing based on selected event type
+ const getPrice = () => {
+ if (!eventType) return null;
+ return PRICING_STRUCTURE[eventType];
+ };
+
+ const price = getPrice();
+ const totalPrice = price ? price * partySize : 0;
+
+ const handlePartySizeChange = (newSize: number) => {
+ if (newSize >= MIN_PARTY_SIZE && newSize <= MAX_PARTY_SIZE) {
+ setValue('partySize', newSize, { shouldValidate: true });
+ setInputValue(newSize.toString());
+ }
+ };
+
+ const handleInputChange = (value: string) => {
+ setInputValue(value);
+ const numValue = parseInt(value);
+ if (!isNaN(numValue) && numValue >= MIN_PARTY_SIZE && numValue <= MAX_PARTY_SIZE) {
+ setValue('partySize', numValue, { shouldValidate: true });
+ }
+ };
+
+ const incrementSize = () => {
+ if (partySize < MAX_PARTY_SIZE) {
+ handlePartySizeChange(partySize + 1);
+ }
+ };
+
+ const decrementSize = () => {
+ if (partySize > MIN_PARTY_SIZE) {
+ handlePartySizeChange(partySize - 1);
+ }
+ };
+
+ const getEventTypeName = () => {
+ return eventType ? getEventTypeDisplayName(eventType) : 'Selected Experience';
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ How Many Guests Will Attend?
+
+
+ Select the number of guests for your culinary experience.
+
+
+
+ {/* Party size selector */}
+
+
+ {/* Manual input with +/- buttons */}
+
+
+ Number of Guests
+
+
+
+
MIN_PARTY_SIZE
+ ? "border-accent-500 text-accent-600 hover:bg-accent-50"
+ : "border-gray-300 text-gray-400 cursor-not-allowed"
+ )}
+ >
+ -
+
+
+
+
handleInputChange(e.target.value)}
+ className="w-20 text-center text-2xl font-bold text-primary-900 border-none bg-transparent focus:outline-none"
+ />
+
guests
+
+
+
= MAX_PARTY_SIZE}
+ className={clsx(
+ "w-12 h-12 rounded-full border-2 flex items-center justify-center text-lg font-semibold transition-colors",
+ partySize < MAX_PARTY_SIZE
+ ? "border-accent-500 text-accent-600 hover:bg-accent-50"
+ : "border-gray-300 text-gray-400 cursor-not-allowed"
+ )}
+ >
+ +
+
+
+
+
+ Minimum {MIN_PARTY_SIZE} guests, maximum {MAX_PARTY_SIZE} guests
+
+
+
+ {/* Quick selection presets */}
+
+
Quick Selection:
+
+ {PARTY_SIZE_PRESETS.map((size) => (
+ handlePartySizeChange(size)}
+ className={clsx(
+ "px-4 py-2 rounded-lg text-sm font-medium transition-colors border-2",
+ partySize === size
+ ? "bg-accent-500 text-white border-accent-500"
+ : "bg-white text-primary-700 border-gray-200 hover:border-accent-300 hover:bg-accent-50"
+ )}
+ >
+ {size} guests
+
+ ))}
+
+
+
+
+
+ {/* Pricing display */}
+ {eventType && price && (
+
+
+
+ Pricing Estimate
+
+
+
+ {getEventTypeName()}
+
+
+ ${price.toFixed(2)} per person
+
+
+ Total: ${totalPrice.toFixed(2)} for {partySize} guests
+
+
+
+
+ )}
+
+ {/* Error message */}
+ {errors.partySize && (
+
+
+ {errors.partySize.message}
+
+
+ )}
+
+ {/* Hidden form field */}
+
+
+ );
+};
+
+export default PartySizeSelector;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/RequestSummary.tsx b/apps/storefront/app/components/event-request/RequestSummary.tsx
new file mode 100644
index 000000000..9763a9dc8
--- /dev/null
+++ b/apps/storefront/app/components/event-request/RequestSummary.tsx
@@ -0,0 +1,262 @@
+import { Button } from '@app/components/common/buttons/Button';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import { PRICING_STRUCTURE, getEventTypeDisplayName } from '@libs/constants/pricing';
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface RequestSummaryProps {
+ className?: string;
+ menus: StoreMenuDTO[];
+ onEditStep: (step: number, section?: string) => void;
+ onSubmit: () => void;
+ isSubmitting: boolean;
+}
+
+export const RequestSummary: FC = ({
+ className,
+ menus,
+ onEditStep,
+ onSubmit,
+ isSubmitting
+}) => {
+ const { watch } = useFormContext();
+
+ // Get all form data
+ const formData = {
+ menuId: watch('menuId'),
+ eventType: watch('eventType'),
+ partySize: watch('partySize'),
+ requestedDate: watch('requestedDate'),
+ requestedTime: watch('requestedTime'),
+ locationAddress: watch('locationAddress'),
+ firstName: watch('firstName'),
+ lastName: watch('lastName'),
+ email: watch('email'),
+ phone: watch('phone'),
+ specialRequirements: watch('specialRequirements'),
+ notes: watch('notes'),
+ };
+
+ // Get selected menu
+ const selectedMenu = formData.menuId
+ ? menus.find(menu => menu.id === formData.menuId)
+ : null;
+
+ // Calculate pricing
+ const pricePerPerson = formData.eventType ? PRICING_STRUCTURE[formData.eventType] : 0;
+ const totalPrice = pricePerPerson * (formData.partySize || 0);
+
+ // Format date (parse YYYY-MM-DD as local to avoid UTC offset issues)
+ const formatDateForDisplay = (dateString: string) => {
+ if (!dateString) return '';
+ const [y, m, d] = dateString.split('-').map(Number);
+ const date = new Date(y, (m || 1) - 1, d || 1);
+ return date.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ };
+
+ const formatTimeForDisplay = (timeString: string) => {
+ if (!timeString) return '';
+ const [hours, minutes] = timeString.split(':');
+ const hour = parseInt(hours);
+ const ampm = hour >= 12 ? 'PM' : 'AM';
+ const displayHour = hour % 12 || 12;
+ return `${displayHour}:${minutes} ${ampm}`;
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Review Your Event Request
+
+
+ Please review all details before submitting your request to Chef Luis.
+
+
+
+ {/* Summary sections */}
+
+ {/* Menu Selection */}
+
+
+
Menu Selection
+ onEditStep(1, 'menu')} className="text-accent-600 text-sm">Edit
+
+ {selectedMenu ? (
+
+
+
{selectedMenu.name}
+
+ {selectedMenu.courses && selectedMenu.courses.length > 0 && (
+
+
Courses ({selectedMenu.courses.length}):
+
+ {selectedMenu.courses.map((course: any, index: number) => (
+
+ {course.name}
+
+ ))}
+
+
+ )}
+
+ ) : (
+
Custom menu - Chef Luis will design a unique experience for you
+ )}
+
+
+ {/* Experience Details */}
+
+
+
Experience Details
+ onEditStep(1, 'experience')} className="text-accent-600 text-sm">Edit
+
+
+
+
Experience Type
+
{formData.eventType ? getEventTypeDisplayName(formData.eventType) : 'Not selected'}
+
+
+
Number of Guests
+
{formData.partySize || 0} guests
+
+
+
+
+ {/* Date & Time */}
+
+
+
Date & Time
+ onEditStep(2, 'date')} className="text-accent-600 text-sm">Edit
+
+
+
+
Preferred Date
+
{formData.requestedDate ? formatDateForDisplay(formData.requestedDate) : 'Not selected'}
+
+
+
Start Time
+
{formData.requestedTime ? formatTimeForDisplay(formData.requestedTime) : 'Not selected'}
+
+
+
+
+ {/* Location */}
+
+
+
Location
+ onEditStep(2, 'location')} className="text-accent-600 text-sm">Edit
+
+
+
+
Event Location
+
Your Location
+
+ {formData.locationAddress && (
+
+
Address
+
{formData.locationAddress}
+
+ )}
+
+
+
+ {/* Contact Information */}
+
+
+
Contact Information
+ onEditStep(2, 'contact')} className="text-accent-600 text-sm">Edit
+
+
+
+
Name
+
{formData.firstName && formData.lastName ? `${formData.firstName} ${formData.lastName}` : 'Not provided'}
+
+
+
Email
+
{formData.email || 'Not provided'}
+
+ {formData.phone && (
+
+
Phone
+
{formData.phone}
+
+ )}
+
+
+
+ {/* Special Requests */}
+ {(formData.specialRequirements || formData.notes) && (
+
+
+
Special Requests
+ onEditStep(3, 'special')} className="text-accent-600 text-sm">Edit
+
+
+ {formData.specialRequirements && (
+
+
Dietary Requirements
+
{formData.specialRequirements}
+
+ )}
+ {formData.notes && (
+
+
Additional Notes
+
{formData.notes}
+
+ )}
+
+
+ )}
+
+ {/* Pricing Summary */}
+ {formData.eventType && formData.partySize && (
+
+
Pricing Estimate
+
+
+ {getEventTypeDisplayName(formData.eventType)} × {formData.partySize} guests
+ ${pricePerPerson.toFixed(2)} × {formData.partySize}
+
+
+
+ Total Estimated Cost
+ ${totalPrice.toFixed(2)}
+
+
+
+
* Final pricing will be confirmed by Chef Luis and may include adjustments for location, special requirements, or menu customizations.
+
+ )}
+
+
+ {/* Submit button */}
+
+
+ {isSubmitting ? (
+
+
+
+
+
+ Submitting Request...
+
+ ) : (
+ 'Submit Event Request to Chef Luis'
+ )}
+
+
No payment required now - you'll receive a secure payment link after Chef Luis confirms your event
+
+
+ );
+};
+
+export default RequestSummary;
\ No newline at end of file
diff --git a/apps/storefront/app/components/event-request/SpecialRequests.tsx b/apps/storefront/app/components/event-request/SpecialRequests.tsx
new file mode 100644
index 000000000..0f1cb50df
--- /dev/null
+++ b/apps/storefront/app/components/event-request/SpecialRequests.tsx
@@ -0,0 +1,140 @@
+import { useState } from 'react';
+import { useFormContext } from 'react-hook-form';
+import type { EventRequestFormData } from '@app/routes/request._index';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface SpecialRequestsProps {
+ className?: string;
+}
+
+// Common dietary restrictions
+const DIETARY_RESTRICTIONS = [
+ 'Vegetarian',
+ 'Vegan',
+ 'Gluten-Free',
+ 'Dairy-Free',
+ 'Nut Allergies',
+ 'Seafood Allergies',
+ 'Shellfish Allergies',
+ 'Egg Allergies',
+ 'Keto/Low-Carb',
+ 'Paleo',
+ 'Halal',
+ 'Kosher',
+];
+
+export const SpecialRequests: FC = ({ className }) => {
+ const {
+ watch,
+ setValue,
+ formState: { errors },
+ } = useFormContext();
+ const notes = watch('notes');
+ const specialRequirements = watch('specialRequirements');
+
+ const [selectedDietaryRestrictions, setSelectedDietaryRestrictions] = useState([]);
+
+ const handleDietaryRestrictionToggle = (restriction: string) => {
+ const newRestrictions = selectedDietaryRestrictions.includes(restriction)
+ ? selectedDietaryRestrictions.filter((r) => r !== restriction)
+ : [...selectedDietaryRestrictions, restriction];
+
+ setSelectedDietaryRestrictions(newRestrictions);
+
+ // Persist selection summary in specialRequirements
+ const restrictionsText = newRestrictions.length > 0 ? `Dietary Restrictions: ${newRestrictions.join(', ')}` : '';
+ setValue('specialRequirements', restrictionsText, { shouldValidate: true });
+ };
+
+ const handleNotesChange = (value: string) => {
+ setValue('notes', value, { shouldValidate: true });
+ };
+
+ return (
+
+ {/* Header */}
+
+
Special Requests & Dietary Needs
+
Help Chef Luis customize your experience by sharing any dietary restrictions or special requests.
+
+
+ {/* Dietary restrictions */}
+
+
Dietary Restrictions & Allergies
+
Select any dietary restrictions or allergies that apply to your guests:
+
+
+ {DIETARY_RESTRICTIONS.map((restriction) => {
+ const isSelected = selectedDietaryRestrictions.includes(restriction);
+ return (
+ handleDietaryRestrictionToggle(restriction)}
+ className={clsx(
+ 'px-3 py-2 rounded-lg text-sm font-medium transition-colors border-2 text-center',
+ isSelected
+ ? 'bg-accent-500 text-white border-accent-500'
+ : 'bg-white text-primary-700 border-gray-200 hover:border-accent-300 hover:bg-accent-50'
+ )}
+ >
+ {restriction}
+
+ );
+ })}
+
+
+ {selectedDietaryRestrictions.length > 0 && (
+
+
Selected Dietary Restrictions:
+
{selectedDietaryRestrictions.join(', ')}
+
+ )}
+
+
+ {/* Single free-text section */}
+
+
Additional Notes & Special Requests
+
+
+ {/* Summary of special requests */}
+ {(specialRequirements || notes) && (
+
+
Special Requests Summary
+
+ {specialRequirements && (
+
+
Dietary Requirements:
+
{specialRequirements}
+
+ )}
+ {notes && (
+
+
Additional Notes:
+
{notes}
+
+ )}
+
+
+ )}
+
+ {/* Hidden field keeps dietary restriction summary */}
+
+
+
+ );
+};
+
+export default SpecialRequests;
\ No newline at end of file
diff --git a/apps/storefront/app/components/layout/footer/Footer.tsx b/apps/storefront/app/components/layout/footer/Footer.tsx
index efc636ebd..613797e55 100644
--- a/apps/storefront/app/components/layout/footer/Footer.tsx
+++ b/apps/storefront/app/components/layout/footer/Footer.tsx
@@ -44,11 +44,11 @@ export const Footer = () => {
-
Coffee & Community
+
Culinary Experiences
- Barrio Coffee is a specialty coffee roaster and cafe located in East Austin. We offer freshly roasted
- beans with an experienced balance of quality flavors. Come enjoy our custom house-blends and our
- assortment of single origin coffees.
+ Chef Velez offers premium private chef experiences including cooking classes, plated dinners, and
+ buffet-style events. Restaurant-quality cuisine crafted in your home with professional service and
+ unforgettable flavors.
diff --git a/apps/storefront/app/components/menu/MenuCarousel.tsx b/apps/storefront/app/components/menu/MenuCarousel.tsx
new file mode 100644
index 000000000..d75956090
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuCarousel.tsx
@@ -0,0 +1,115 @@
+import { ScrollArrowButtons } from '@app/components/common/buttons/ScrollArrowButtons';
+import { useScrollArrows } from '@app/hooks/useScrollArrows';
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import clsx from 'clsx';
+import { type FC, memo, useEffect } from 'react';
+import { NavLink } from 'react-router';
+import { MenuGridSkeleton } from './MenuGridSkeleton';
+import type { MenuListItemProps } from './MenuListItem';
+import { MenuListItem } from './MenuListItem';
+
+export interface MenuCarouselProps {
+ menus?: StoreMenuDTO[];
+ className?: string;
+ renderItem?: FC
;
+}
+
+export const MenuRow: FC<{ menus: StoreMenuDTO[] }> = memo(({ menus }) => {
+ return (
+ <>
+ {menus.map((menu) => (
+
+
+ {({ isTransitioning }) => }
+
+ {/* Quick actions under each card for mobile ergonomics */}
+
+
+ View details
+
+
+ Request this
+
+
+
+ ))}
+ >
+ );
+});
+
+export const MenuCarousel: FC = ({ menus, className }) => {
+ const { scrollableDivRef, ...scrollArrowProps } = useScrollArrows({
+ buffer: 100,
+ resetOnDepChange: [menus],
+ });
+
+ if (!menus) return ;
+
+ // Motion polish: parallax image and scale centered card
+ useEffect(() => {
+ const container = scrollableDivRef.current;
+ if (!container) return;
+
+ let rafId: number | null = null;
+
+ const updateMotion = () => {
+ rafId = null;
+ const rect = container.getBoundingClientRect();
+ const containerCenter = rect.left + rect.width / 2;
+
+ const cards = container.querySelectorAll('[data-card]');
+ cards.forEach((card) => {
+ const cardRect = card.getBoundingClientRect();
+ const cardCenter = cardRect.left + cardRect.width / 2;
+ const delta = (cardCenter - containerCenter) / rect.width; // -1..1 approximately
+ const clamped = Math.max(-1, Math.min(1, delta));
+
+ const scale = 1 + (1 - Math.min(1, Math.abs(clamped) * 2)) * 0.05; // up to +5% in center
+ const parallaxX = -clamped * 24; // px
+
+ card.style.setProperty('--scale', scale.toFixed(3));
+ card.style.setProperty('--parallax-x', `${parallaxX.toFixed(1)}px`);
+ });
+ };
+
+ const onScroll = () => {
+ if (rafId == null) {
+ rafId = requestAnimationFrame(updateMotion);
+ }
+ };
+
+ updateMotion();
+ container.addEventListener('scroll', onScroll, { passive: true });
+ window.addEventListener('resize', onScroll, { passive: true });
+ return () => {
+ container.removeEventListener('scroll', onScroll);
+ window.removeEventListener('resize', onScroll);
+ if (rafId != null) cancelAnimationFrame(rafId);
+ };
+ }, [menus, scrollableDivRef]);
+
+ return (
+
+ );
+};
+
+export default MenuCarousel;
\ No newline at end of file
diff --git a/apps/storefront/app/components/menu/MenuGrid.tsx b/apps/storefront/app/components/menu/MenuGrid.tsx
new file mode 100644
index 000000000..3ae4314e0
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuGrid.tsx
@@ -0,0 +1,64 @@
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import clsx from 'clsx';
+import type { FC } from 'react';
+import { NavLink, useNavigation } from 'react-router';
+import { MenuGridSkeleton } from './MenuGridSkeleton';
+import { MenuListHeader, type MenuListHeaderProps } from './MenuListHeader';
+import { MenuListItem } from './MenuListItem';
+
+export interface MenuListProps {
+ menus?: StoreMenuDTO[];
+ className?: string;
+ heading?: string;
+ actions?: import('@libs/types').CustomAction[];
+}
+
+export const MenuGrid: FC = ({
+ heading,
+ actions,
+ menus,
+ className = 'grid grid-cols-1 gap-y-6 @md:grid-cols-2 gap-x-4 @2xl:!grid-cols-3 @4xl:!grid-cols-4 @4xl:gap-x-4 justify-items-stretch items-stretch',
+}) => {
+ const navigation = useNavigation();
+ const isLoading = navigation.state !== 'idle';
+
+ if (!menus) return ;
+
+ return (
+
+
+
+
+ {menus?.map((menu, index) => (
+
+ {({ isTransitioning }) => (
+
+ )}
+
+ ))}
+
+
+ );
+};
+
+// required for lazy loading this component
+export default MenuGrid;
\ No newline at end of file
diff --git a/apps/storefront/app/components/menu/MenuGridSkeleton.tsx b/apps/storefront/app/components/menu/MenuGridSkeleton.tsx
new file mode 100644
index 000000000..fc63b2b14
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuGridSkeleton.tsx
@@ -0,0 +1,39 @@
+import type { FC } from 'react';
+
+export interface MenuGridSkeletonProps {
+ length?: number;
+}
+
+export const MenuGridSkeleton: FC = ({ length = 6 }) => {
+ return (
+
+ {Array.from({ length }).map((_, index) => (
+
+
+ {/* Image skeleton */}
+
+
+ {/* Content skeleton */}
+
+
+
+ ))}
+
+ );
+};
\ No newline at end of file
diff --git a/apps/storefront/app/components/menu/MenuListHeader.tsx b/apps/storefront/app/components/menu/MenuListHeader.tsx
new file mode 100644
index 000000000..6b0405d73
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuListHeader.tsx
@@ -0,0 +1,25 @@
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import type { CustomAction } from '@libs/types';
+import type { FC } from 'react';
+
+export interface MenuListHeaderProps {
+ heading?: string;
+ actions?: CustomAction[];
+}
+
+export const MenuListHeader: FC = ({ heading, actions }) => {
+ if (!heading && !actions?.length) return null;
+
+ return (
+
+ {heading && (
+
+ {heading}
+
+ )}
+ {actions?.length && (
+
+ )}
+
+ );
+};
\ No newline at end of file
diff --git a/apps/storefront/app/components/menu/MenuListItem.tsx b/apps/storefront/app/components/menu/MenuListItem.tsx
new file mode 100644
index 000000000..8c5b02053
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuListItem.tsx
@@ -0,0 +1,85 @@
+import { Image } from '@app/components/common/images/Image';
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface MenuListItemProps {
+ menu: StoreMenuDTO;
+ isTransitioning?: boolean;
+ className?: string;
+}
+
+export const MenuListItem: FC = ({
+ menu,
+ isTransitioning = false,
+ className
+}) => {
+ const courseCount = menu.courses?.length || 0;
+ const estimatedTime = "3-4 hours"; // Default estimate since not in data model yet
+
+ // Generate a description from the first few dishes
+ const description = menu.courses
+ .slice(0, 2)
+ .map(course =>
+ course.dishes.slice(0, 2).map(dish => dish.name).join(', ')
+ )
+ .join(' • ') || 'A carefully crafted menu experience';
+
+ return (
+
+ {/* Menu Image */}
+
+
+
+
+ {/* Menu Content */}
+
+
+
+ {menu.name}
+
+
+ {courseCount} course{courseCount !== 1 ? 's' : ''} • {estimatedTime}
+
+
+
+ {/* Description */}
+
+ {description}
+
+
+ {/* Footer */}
+
+
+ From $99.99 per person
+
+
+
+
+
+ {/* Hover overlay */}
+
+
+ );
+};
\ No newline at end of file
diff --git a/apps/storefront/app/components/menu/MenuListWithPagination.tsx b/apps/storefront/app/components/menu/MenuListWithPagination.tsx
new file mode 100644
index 000000000..aacd20fda
--- /dev/null
+++ b/apps/storefront/app/components/menu/MenuListWithPagination.tsx
@@ -0,0 +1,22 @@
+import type { PaginationConfig } from '@app/components/common/Pagination';
+import { PaginationWithContext } from '@app/components/common/Pagination/pagination-with-context';
+import { MenuGrid, type MenuListProps } from '@app/components/menu/MenuGrid';
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import type { FC } from 'react';
+
+export interface MenuListWithPaginationProps extends MenuListProps {
+ menus?: StoreMenuDTO[];
+ paginationConfig?: PaginationConfig;
+ context: string;
+}
+
+export const MenuListWithPagination: FC = ({
+ context,
+ paginationConfig,
+ ...props
+}) => (
+
+
+ {paginationConfig &&
}
+
+);
\ No newline at end of file
diff --git a/apps/storefront/app/components/product/EventProductDetails.tsx b/apps/storefront/app/components/product/EventProductDetails.tsx
new file mode 100644
index 000000000..65920f773
--- /dev/null
+++ b/apps/storefront/app/components/product/EventProductDetails.tsx
@@ -0,0 +1,463 @@
+import { Button } from '@app/components/common/buttons/Button';
+import { Container } from '@app/components/common/container/Container';
+import { Grid } from '@app/components/common/grid/Grid';
+import { GridColumn } from '@app/components/common/grid/GridColumn';
+import { SubmitButton } from '@app/components/common/remix-hook-form/buttons/SubmitButton';
+import { QuantitySelector } from '@app/components/common/remix-hook-form/field-groups/QuantitySelector';
+import { ProductPrice } from '@app/components/product/ProductPrice';
+import { Share } from '@app/components/share';
+import { useCart } from '@app/hooks/useCart';
+import { useRegion } from '@app/hooks/useRegion';
+import { FetcherKeys } from '@libs/util/fetcher-keys';
+import { formatPrice, getVariantPrices } from '@libs/util/prices';
+import { isEventProduct, parseEventSku, getEventVariant } from '@libs/util/products';
+import { StoreProduct } from '@medusajs/types';
+import { useCallback, useRef } from 'react';
+import { useFetcher } from 'react-router';
+import { RemixFormProvider, useRemixForm } from 'remix-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import clsx from 'clsx';
+
+// Schema for add to cart form
+const addToCartSchema = z.object({
+ productId: z.string(),
+ options: z.record(z.string()),
+ quantity: z.number().min(1),
+});
+
+type AddToCartFormData = z.infer;
+
+export interface EventProductDetailsProps {
+ product: StoreProduct;
+ chefEvent?: any; // Will be fetched from backend
+ menu?: any; // Will be fetched from backend
+}
+
+/**
+ * Enhanced product details component for event products
+ * Displays event-specific information like date, time, location, party size
+ */
+export const EventProductDetails = ({ product, chefEvent, menu }: EventProductDetailsProps) => {
+ const formRef = useRef(null);
+ const addToCartFetcher = useFetcher({ key: FetcherKeys.cart.createLineItem });
+ const { toggleCartDrawer } = useCart();
+ const { region } = useRegion();
+
+ const eventVariant = getEventVariant(product);
+ const eventInfo = eventVariant?.sku ? parseEventSku(eventVariant.sku) : null;
+
+ if (!isEventProduct(product) || !eventInfo) {
+ console.log('Not rendering EventProductDetails - not an event product or no event info');
+ return null; // Not an event product, don't render
+ }
+
+ // Check if event is sold out
+ const soldOut = !eventVariant || (eventVariant.inventory_quantity || 0) <= 0;
+ const isAddingToCart = ['submitting', 'loading'].includes(addToCartFetcher.state);
+
+ // Get inventory quantity with fallback
+ const getInventoryQuantity = () => {
+ if (eventVariant?.inventory_quantity !== undefined && eventVariant?.inventory_quantity !== null) {
+ if (eventVariant.inventory_quantity === 0 && eventVariant.manage_inventory) {
+ return chefEvent?.partySize || 10; // Use party size as fallback
+ }
+ return eventVariant.inventory_quantity;
+ }
+ // Fallback: if manage_inventory is true but no quantity, assume it's available
+ if (eventVariant?.manage_inventory) {
+ return 10; // Default to 10 tickets available
+ }
+ return 0;
+ };
+
+ const inventoryQuantity = getInventoryQuantity();
+ const isSoldOut = inventoryQuantity <= 0;
+
+ // Get the option values for the event variant
+ const getEventVariantOptions = () => {
+ if (!eventVariant?.options) return {};
+
+ const options: Record = {};
+ eventVariant.options.forEach(option => {
+ if (option.option_id && option.value) {
+ options[option.option_id] = option.value;
+ }
+ });
+
+ return options;
+ };
+
+ const eventVariantOptions = getEventVariantOptions();
+
+ // Debug logging
+ console.log('💰 EventProductDetails PRICING Debug:', {
+ productId: product.id,
+ productTitle: product.title,
+ eventVariant: eventVariant ? {
+ id: eventVariant.id,
+ sku: eventVariant.sku,
+ inventory_quantity: eventVariant.inventory_quantity,
+ manage_inventory: eventVariant.manage_inventory,
+ calculated_price: eventVariant.calculated_price ? {
+ calculated_amount: eventVariant.calculated_price.calculated_amount,
+ currency_code: eventVariant.calculated_price.currency_code,
+ calculated_amount_in_dollars: (eventVariant.calculated_price.calculated_amount || 0) / 100
+ } : null
+ } : null,
+ calculatedInventoryQuantity: inventoryQuantity,
+ isSoldOut: isSoldOut,
+ eventVariantOptions: eventVariantOptions,
+ eventInfo: eventInfo,
+ chefEvent: chefEvent ? {
+ id: chefEvent.id,
+ eventType: chefEvent.eventType,
+ partySize: chefEvent.partySize,
+ totalPrice: chefEvent.totalPrice,
+ expectedPricePerPerson: chefEvent.totalPrice ? chefEvent.totalPrice / chefEvent.partySize : null
+ } : null,
+ menu: menu
+ });
+
+ // Setup form with remix-hook-form
+ const form = useRemixForm({
+ resolver: zodResolver(addToCartSchema),
+ defaultValues: {
+ productId: product.id,
+ options: eventVariantOptions,
+ quantity: 1,
+ },
+ });
+
+ // Format event date and time from product description
+ const formatEventDateTime = () => {
+ // Extract date and time from product description
+ // Format: "Private chef event for {name} on {date} at {time}"
+ const description = product.description || '';
+ const dateMatch = description.match(/on ([^,]+)/);
+ const timeMatch = description.match(/at (\d{1,2}:\d{2})/);
+
+ if (dateMatch && timeMatch) {
+ // Simple date formatting without luxon
+ const dateStr = dateMatch[1];
+ const timeStr = timeMatch[1];
+
+ return {
+ date: dateStr, // Will format properly when we have the actual data
+ time: timeStr
+ };
+ }
+
+ return null;
+ };
+
+ const eventDateTime = formatEventDateTime();
+ const pricePerPerson = eventVariant ? (getVariantPrices(eventVariant).original || 0) : 0;
+ const totalPrice = pricePerPerson * (chefEvent?.partySize || 0);
+
+ // Handle add to cart submission
+ const handleAddToCart = useCallback(() => {
+ console.log('Add to cart submitted:', {
+ productId: product.id,
+ variantId: eventVariant?.id,
+ inventoryQuantity: eventVariant?.inventory_quantity,
+ eventVariantOptions: eventVariantOptions,
+ formData: {
+ productId: product.id,
+ options: eventVariantOptions,
+ quantity: 1
+ }
+ });
+ toggleCartDrawer(true);
+ }, [toggleCartDrawer, product.id, eventVariant?.id, eventVariant?.inventory_quantity, eventVariantOptions]);
+
+ return (
+
+
+
+
+
+
+ {/* Hero Section with Enhanced Design */}
+
+ {/* Decorative background elements */}
+
+
+
+
+
+
+
+
Private Chef Event
+
Exclusive Experience
+
+
+
+ {eventDateTime && (
+
+
+
+
+
+
Date
+
{eventDateTime.date}
+
+
+
+
+
+
+
+
+
Time
+
{eventDateTime.time}
+
+
+
+
+ )}
+
+
+
+ {/* Event Details Section with Enhanced Design */}
+
+
+
+
+
+ Event Type
+
+ {eventInfo.type === 'cooking_class' && "Chef's Cooking Class"}
+ {eventInfo.type === 'plated_dinner' && 'Plated Dinner Service'}
+ {eventInfo.type === 'buffet_style' && 'Buffet Style Service'}
+
+
+
+
+ Price per Person
+
+ {formatPrice(pricePerPerson, { currency: 'usd' })}
+
+
+
+
+
+ {/* Menu Information with Enhanced Design */}
+ {menu && (
+
+
+
+
+ Menu
+ {menu.title}
+
+ {menu.courses && menu.courses.length > 0 && (
+
+ Courses
+ {menu.courses.length}
+
+ )}
+
+
+ )}
+
+ {/* Chef Information with Enhanced Design */}
+
+
+
+
+
+
Chef Luis Velez
+
15+ years of culinary experience
+
Specializing in Mediterranean cuisine
+
+
+
+
+
+
+
+ {/* Event Ticket Purchase Section with Enhanced Design */}
+
+ {/* Decorative elements */}
+
+
+
+
+
+
Purchase Event Tickets
+
+
+
+ Secure your spot for this exclusive chef experience.
+ Tickets are limited to the event party size.
+
+
+
+
+ Available Tickets
+
+ {inventoryQuantity} remaining
+
+
+
+
+ Price per Ticket
+
+ {formatPrice(pricePerPerson, { currency: 'usd' })}
+
+
+
+
+
+
+
+ {Object.entries(eventVariantOptions).map(([optionId, value]) => (
+
+ ))}
+
+
+ {!isSoldOut && (
+
+
+ Number of Tickets
+ Max: {inventoryQuantity}
+
+
+
+ )}
+
+
+ {isAddingToCart ? 'Adding to Cart...' : isSoldOut ? 'Sold Out' : 'Purchase Tickets'}
+
+
+
+
+
+
+
+ {/* Share Section with Enhanced Design */}
+
+
+
+ Invite friends and family to join this exclusive culinary experience.
+
+
+
+
+ {/* Important Information with Enhanced Design */}
+
+
+
+
Important Information
+
+
+
+
+ All dietary restrictions will be accommodated
+
+
+
+ Chef will arrive 30 minutes before event time
+
+
+
+ Full payment required to confirm booking
+
+
+
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/apps/storefront/app/components/reviews/ProductReviewList.tsx b/apps/storefront/app/components/reviews/ProductReviewList.tsx
index ec50f6274..958a99be1 100644
--- a/apps/storefront/app/components/reviews/ProductReviewList.tsx
+++ b/apps/storefront/app/components/reviews/ProductReviewList.tsx
@@ -60,7 +60,7 @@ export const ProductReviewList: FC = ({ productReviews }
{review.response && review.response.content && (
-
Barrio's Response
+
Chef's Response
{review.response.created_at && (
({
export const useCheckout = () => {
const context = useContext(CheckoutContext);
+
+ if (!context) {
+ throw new Error('useCheckout must be used within a CheckoutProvider');
+ }
+
+ if (!context.state) {
+ throw new Error('CheckoutProvider state is null');
+ }
+
const nextStep = useNextStep(context.state);
const { state } = context;
const fetchers = useFetchers();
diff --git a/apps/storefront/app/providers/root-providers.tsx b/apps/storefront/app/providers/root-providers.tsx
index d08a2aa1e..263d6af7c 100644
--- a/apps/storefront/app/providers/root-providers.tsx
+++ b/apps/storefront/app/providers/root-providers.tsx
@@ -1,6 +1,9 @@
import { StorefrontProvider, storefrontInitialState } from '@app/providers/storefront-provider';
import { FC, PropsWithChildren } from 'react';
+import { TooltipProvider } from '@medusajs/ui';
export const RootProviders: FC = ({ children }) => (
- {children}
+
+ {children}
+
);
diff --git a/apps/storefront/app/root.tsx b/apps/storefront/app/root.tsx
index 35106bc06..64554d178 100644
--- a/apps/storefront/app/root.tsx
+++ b/apps/storefront/app/root.tsx
@@ -19,8 +19,8 @@ import '@app/styles/global.css';
import { useRootLoaderData } from './hooks/useRootLoaderData';
export const getRootMeta: MetaFunction = ({ data }) => {
- const title = 'Barrio Store';
- const description = 'Discover our artisan-roasted coffee, crafted with care and delivered to your door.';
+ const title = 'Chef Velez';
+ const description = 'Private chef experiences: cooking classes, plated dinners, and buffet-style events.';
const ogTitle = title;
const ogDescription = description;
const ogImage = '';
diff --git a/apps/storefront/app/routes/_index.tsx b/apps/storefront/app/routes/_index.tsx
index 9b9d56f15..463aeaffe 100644
--- a/apps/storefront/app/routes/_index.tsx
+++ b/apps/storefront/app/routes/_index.tsx
@@ -1,255 +1,222 @@
-import { ActionList } from '@app/components/common/actions-list/ActionList';
import { Container } from '@app/components/common/container';
import { Image } from '@app/components/common/images/Image';
-import { GridCTA } from '@app/components/sections/GridCTA';
-import Hero from '@app/components/sections/Hero';
-import { ListItems } from '@app/components/sections/ListItems';
-import ProductList from '@app/components/sections/ProductList';
-import { SideBySide } from '@app/components/sections/SideBySide';
+import { ChefHero } from '@app/components/chef/ChefHero';
+import { FeaturedMenus } from '@app/components/chef/FeaturedMenus';
+import { ExperienceTypes } from '@app/components/chef/ExperienceTypes';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import { fetchMenus } from '@libs/util/server/data/menus.server';
import { getMergedPageMeta } from '@libs/util/page';
import type { LoaderFunctionArgs, MetaFunction } from 'react-router';
+import { useLoaderData } from 'react-router';
export const loader = async (args: LoaderFunctionArgs) => {
- return {};
+ try {
+ // Fetch menus for the featured menus section
+ const menusData = await fetchMenus({ limit: 3 });
+ return {
+ menus: menusData.menus || [],
+ success: true
+ };
+ } catch (error) {
+ console.error('Failed to load menus for homepage:', error);
+ return {
+ menus: [],
+ success: false
+ };
+ }
};
-export const meta: MetaFunction = getMergedPageMeta;
+export const meta: MetaFunction = ({ data }) => {
+ return [
+ { title: 'Chef Luis Velez - Premium Culinary Experiences' },
+ {
+ name: 'description',
+ content: 'Transform your special occasions with Chef Luis\'s premium culinary experiences. From intimate cooking classes to elegant plated dinners, bringing restaurant-quality cuisine to your home.'
+ },
+ { property: 'og:title', content: 'Chef Luis Velez - Premium Culinary Experiences' },
+ {
+ property: 'og:description',
+ content: 'Professional chef services for cooking classes, plated dinners, and buffet-style events. Personalized culinary experiences in your home.'
+ },
+ { property: 'og:type', content: 'website' },
+ { name: 'keywords', content: 'private chef, cooking classes, plated dinner, culinary experiences, chef services, private dining' },
+ ];
+};
export default function IndexRoute() {
+ const { menus } = useLoaderData();
+
return (
<>
-
-
+
+ {/* Chef Hero Section */}
+
- COFFEE & COMMUNITY
- BARRIO
-
- Discover our artisan-roasted coffee, crafted with care and delivered to your door. At Barrio, we’re more
- than a coffee roastery—we’re a neighborhood.
-
-
- }
- actions={[
- {
- label: 'Discover Our Blends',
- url: '/categories/blends',
- },
- ]}
- image={{
- url: '/assets/images/barrio-banner.png',
- alt: 'Barrio background',
- }}
/>
-
-
-
-
+ {/* Experience Types Section */}
+
+
+ {/* Featured Menus Section */}
+
+
+ {/* Chef Story Section */}
+
+
+ {/* Chef Image */}
+
+
+
+ {/* Decorative element */}
+
+
+
-
-
-
- Building Community
-
+ {/* Chef Content */}
+
+
+
+ Meet Chef Luis
+
+
+ Culinary Artistry
+
+
+
+
+
+ With over 15 years of culinary excellence, Chef Luis Velez brings world-class expertise
+ from Michelin-starred restaurants directly to your home.
+
+
+ Trained in classical French techniques with a modern innovative approach, he creates
+ unforgettable dining experiences tailored to your special occasions.
+
+
+
+
+
+ 15+ Years Experience
+
+
+ Michelin Trained
+
+
+ Local Sourcing
+
+
-
- one cup at a time
-
+ {/* Chef Background Section
-
-
-
-
+ >
+
+
15+ Years of Culinary Excellence
+
+ From Michelin-starred restaurants to intimate home kitchens, Chef Luis brings world-class
+ culinary expertise directly to your table. Trained in classical French techniques with a
+ modern innovative approach.
+
+
+
+ */}
-
+ {/* Testimonials Section */}
+
+
+
+ What Our Guests Say
+
+
+
-
- SUBSCRIBE & SAVE
-
- Sit back, let us take care of your coffee
-
+
+
+
⭐⭐⭐⭐⭐
+
+ "Chef Luis created the most incredible anniversary dinner for us. Every course was a masterpiece,
+ and the cooking class was so much fun!"
+
+
— Sarah & Michael K.
+
Plated Dinner Experience
+
-
+
+
⭐⭐⭐⭐⭐
+
+ "The cooking class was amazing! Chef Velez taught us so much and we had a blast.
+ Can't wait to book another experience."
+
+
— Jennifer L.
+
Cooking Class Experience
- }
- actions={[
- {
- label: 'Get your coffee',
- url: '/products',
- },
- ]}
- image={{
- url: '/assets/images/barrio-banner.png',
- alt: 'Barrio background',
- }}
- />
-
-
-
+
+
⭐⭐⭐⭐⭐
+
+ "Perfect for our family gathering! The buffet style worked perfectly for our group
+ and everything was absolutely delicious."
+
+
— The Rodriguez Family
+
Buffet Style Experience
+
-
-
- The Art of Roasting
-
-
- at Barrio
-
-
- Crafting with Care
-
-
-
+
+
+
- }
- right={
-
- At Barrio, our roasting process is a carefully honed craft, combining traditional techniques with a modern,
- sustainable approach. Each batch of coffee is roasted in small quantities to ensure precise control over
- every stage of the process, allowing the unique characteristics of the beans to shine through.
-
-
- We start by selecting high-quality, ethically sourced beans from farmers who share our commitment to
- sustainability and community. The roasting process begins with a slow, even heat that coaxes out the natural
- flavors, developing rich aromas and deep, complex profiles. Every bean undergoes a transformation, revealing
- its distinct notes—whether it's the bright acidity of a light roast, the balanced sweetness of a medium
- roast, or the bold, rich depth of a dark roast.
-
-
- Our goal is to honor the origin of each coffee, preserving its natural flavors while adding our own touch of
- expertise. The result? A perfectly roasted coffee that reflects the heart of our community—vibrant, diverse,
- and full of life. At Barrio, every roast tells a story, and every cup connects you to the hands that
- nurtured it.
-
- }
- />
-
- FIND YOUR COMMUNITY
- BARRIO
- Ship, Share & Connect Over Coffee
+
+
+
READY TO CREATE MEMORIES?
+
Book Your Experience
+
+ Transform your next special occasion into an unforgettable culinary journey.
+ From intimate dinners to group celebrations, every experience is crafted with care.
+
- }
- />
+
+
>
);
}
diff --git a/apps/storefront/app/routes/about-us.tsx b/apps/storefront/app/routes/about-us.tsx
index bf68f54f2..7c04a3b77 100644
--- a/apps/storefront/app/routes/about-us.tsx
+++ b/apps/storefront/app/routes/about-us.tsx
@@ -3,29 +3,8 @@ import Hero from '@app/components/sections/Hero';
import { getMergedPageMeta } from '@libs/util/page';
import type { LoaderFunctionArgs, MetaFunction } from 'react-router';
-const locations: LocationProps[] = [
- {
- title: 'Barrio South Lamar',
- addressLines: ['1105 S. Lamar Blvd', 'Austin, TX 78704'],
- phone: '(512) 906-0010',
- hours: ['Open Daily — 7am to 7pm'],
- imageUrl: '/assets/images/location-1.png',
- },
- {
- title: 'Barrio Sonterra',
- addressLines: ['700 E. Sonterra Blvd. Suite #1113', 'San Antonio, TX 78258'],
- phone: '(210) 530-8740',
- hours: ['Mon thru Fri — 6am to 7pm', 'Sat — 7am to 7pm', 'Sun — 7am to 6pm'],
- imageUrl: '/assets/images/location-2.png',
- },
- {
- title: 'Barrio Deep Ellum',
- addressLines: ['2369 Main Street', 'Dallas, TX 75226'],
- phone: '(469) 248-3440',
- hours: ['Sun thru Thu — 7am to 7pm', 'Fri thru Sat — 7am to 8pm'],
- imageUrl: '/assets/images/location-3.png',
- },
-];
+// Remove coffee shop location blocks; not applicable for chef About page.
+const locations: LocationProps[] = [];
export const loader = async (args: LoaderFunctionArgs) => {
return {};
@@ -80,28 +59,27 @@ export default function IndexRoute() {
className="min-h-[400px] !max-w-full bg-accent-50 sm:rounded-3xl p-6 sm:p-10 md:p-[88px] md:px-[88px]"
content={
-
ABOUT US
+
ABOUT THE CHEF
- Our Story
+ Chef Velez
- At Barrio Coffee Roastery, we’re more than just a coffee business—we’re a community. Inspired by the
- essence of a "barrio," a close-knit neighborhood where people gather, share, and connect, we aim to
- bring that sense of belonging and warmth to every cup of coffee we roast. From the moment we started,
- our passion has been to create exceptional coffee that{' '}
- brings people together, one sip at a time.
+ Chef Velez is a private chef specializing in premium at-home culinary experiences—cooking classes,
+ plated dinners, and buffet-style events. With years of professional experience, Chef Velez crafts
+ unforgettable menus using fresh, seasonal ingredients and provides a seamless, restaurant-quality
+ experience in your home.
}
actionsClassName="!flex-row w-full justify-center !font-base"
actions={[
{
- label: 'Shop Our Coffee',
- url: '/products',
+ label: 'View Menus',
+ url: '/menus',
},
{
- label: 'Join the Barrio Community',
- url: '#',
+ label: 'Request an Event',
+ url: '/request',
},
]}
/>
@@ -109,7 +87,7 @@ export default function IndexRoute() {
- Find your people, find your Barrio
+ Experiences crafted with passion, precision, and hospitality
{locations.map((location) => (
diff --git a/apps/storefront/app/routes/about.tsx b/apps/storefront/app/routes/about.tsx
new file mode 100644
index 000000000..5ffea906a
--- /dev/null
+++ b/apps/storefront/app/routes/about.tsx
@@ -0,0 +1,61 @@
+import { Container } from '@app/components/common/container';
+import Hero from '@app/components/sections/Hero';
+import { getMergedPageMeta } from '@libs/util/page';
+import type { LoaderFunctionArgs, MetaFunction } from 'react-router';
+
+export const loader = async (_args: LoaderFunctionArgs) => {
+ return {};
+};
+
+export const meta: MetaFunction = getMergedPageMeta;
+
+export default function AboutChefRoute() {
+ return (
+ <>
+
+
+ ABOUT THE CHEF
+
+ Chef Velez
+
+
+ Chef Velez is a private chef specializing in premium at-home culinary experiences—cooking classes,
+ plated dinners, and buffet-style events. With years of professional experience, Chef Velez crafts
+ unforgettable menus using fresh, seasonal ingredients and provides a seamless, restaurant-quality
+ experience in your home.
+
+
+ }
+ actionsClassName="!flex-row w-full justify-center !font-base"
+ actions={[
+ { label: 'View Menus', url: '/menus' },
+ { label: 'Request an Event', url: '/request' },
+ ]}
+ />
+
+
+
+
+
+
Philosophy
+
+ Every event is a chance to create connection through food. From intimate dinners to interactive classes,
+ I design experiences that are warm, professional, and tailored to your tastes.
+
+
+
+
Experiences
+
+ Choose from cooking classes, plated dinners, or buffet-style events. All ingredients and equipment are
+ provided—so you can relax and enjoy.
+
+
+
+
+ >
+ );
+}
+
diff --git a/apps/storefront/app/routes/api.cart.line-items.create.ts b/apps/storefront/app/routes/api.cart.line-items.create.ts
index 02f7041f2..8ca7db00b 100644
--- a/apps/storefront/app/routes/api.cart.line-items.create.ts
+++ b/apps/storefront/app/routes/api.cart.line-items.create.ts
@@ -11,23 +11,44 @@ import { z } from 'zod';
export const createLineItemSchema = z.object({
productId: z.string().min(1, 'Product ID is required'),
- options: z.record(z.string()).default({}),
quantity: z.coerce.number().int().min(1, 'Quantity must be at least 1'),
});
type CreateLineItemFormData = z.infer;
export async function action({ request }: ActionFunctionArgs) {
- const { errors, data: validatedFormData } = await getValidatedFormData(
- request,
- zodResolver(createLineItemSchema),
- );
-
- if (errors) {
- return data({ errors }, { status: 400 });
+ // Read form data once
+ const formData = await request.formData();
+
+ // Extract fields manually
+ const productId = formData.get('productId') as string;
+ const quantityStr = formData.get('quantity') as string;
+
+ // Parse and validate
+ if (!productId) {
+ return data({ errors: { root: { message: 'Product ID is required' } } as FieldErrors }, { status: 400 });
+ }
+
+ const quantity = parseInt(quantityStr, 10);
+ if (isNaN(quantity) || quantity < 1) {
+ return data({ errors: { root: { message: 'Quantity must be at least 1' } } as FieldErrors }, { status: 400 });
+ }
+
+ // Extract options from form data
+ const options: Record = {};
+ for (const [key, value] of formData.entries()) {
+ if (key.startsWith('options.') && typeof value === 'string') {
+ const optionId = key.replace('options.', '');
+ options[optionId] = value;
+ }
}
- const { productId, options, quantity } = validatedFormData;
+ console.log('Cart API Debug:', {
+ productId,
+ options,
+ quantity,
+ formDataEntries: Array.from(formData.entries())
+ });
const region = await getSelectedRegion(request.headers);
@@ -40,9 +61,44 @@ export async function action({ request }: ActionFunctionArgs) {
return data({ errors: { root: { message: 'Product not found.' } } as FieldErrors }, { status: 400 });
}
+ console.log('Product Debug:', {
+ productId: product.id,
+ productTitle: product.title,
+ variants: product.variants?.map(v => ({
+ id: v.id,
+ sku: v.sku,
+ options: v.options?.map(o => ({
+ option_id: o.option_id,
+ value: o.value
+ }))
+ }))
+ });
+
const variant = getVariantBySelectedOptions(product.variants || [], options);
- if (!variant) {
+ console.log('Variant Match Debug:', {
+ options,
+ foundVariant: variant ? {
+ id: variant.id,
+ sku: variant.sku,
+ options: variant.options?.map(o => ({
+ option_id: o.option_id,
+ value: o.value
+ }))
+ } : null
+ });
+
+ // If no variant found with options, try to get the first variant for products with only one variant (like event products)
+ const finalVariant = variant || (product.variants?.length === 1 ? product.variants[0] : null);
+
+ console.log('Final Variant Debug:', {
+ variantFromOptions: variant ? variant.id : null,
+ singleVariant: product.variants?.length === 1 ? product.variants[0]?.id : null,
+ finalVariantId: finalVariant?.id,
+ productVariantCount: product.variants?.length
+ });
+
+ if (!finalVariant) {
return data(
{
errors: {
@@ -58,7 +114,7 @@ export async function action({ request }: ActionFunctionArgs) {
const responseHeaders = new Headers();
const { cart } = await addToCart(request, {
- variantId: variant.id!,
+ variantId: finalVariant.id!,
quantity,
});
diff --git a/apps/storefront/app/routes/checkout.success.tsx b/apps/storefront/app/routes/checkout.success.tsx
index 35e5adfb7..fae1582a5 100644
--- a/apps/storefront/app/routes/checkout.success.tsx
+++ b/apps/storefront/app/routes/checkout.success.tsx
@@ -69,7 +69,7 @@ export default function CheckoutSuccessRoute() {
- {formatPrice(item.unit_price, {
+ {formatPrice((item.unit_price || 0), {
currency: order.currency_code,
})}
@@ -81,7 +81,7 @@ export default function CheckoutSuccessRoute() {
Subtotal
- {formatPrice(order.item_subtotal, {
+ {formatPrice((order.item_subtotal || 0), {
currency: order.currency_code,
})}
@@ -101,7 +101,7 @@ export default function CheckoutSuccessRoute() {
Shipping
- {formatPrice(order.shipping_total, {
+ {formatPrice((order.shipping_total || 0), {
currency: order.currency_code,
})}
@@ -110,7 +110,7 @@ export default function CheckoutSuccessRoute() {
Taxes
- {formatPrice(order.tax_total, {
+ {formatPrice((order.tax_total || 0), {
currency: order.currency_code,
})}
@@ -119,7 +119,7 @@ export default function CheckoutSuccessRoute() {
Total
- {formatPrice(order.total, {
+ {formatPrice((order.total || 0), {
currency: order.currency_code,
})}
diff --git a/apps/storefront/app/routes/how-it-works.tsx b/apps/storefront/app/routes/how-it-works.tsx
new file mode 100644
index 000000000..a5044fdbd
--- /dev/null
+++ b/apps/storefront/app/routes/how-it-works.tsx
@@ -0,0 +1,298 @@
+import { Container } from '@app/components/common/container/Container';
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import { getMergedPageMeta } from '@libs/util/page';
+import type { LoaderFunctionArgs, MetaFunction } from 'react-router';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export const loader = async (args: LoaderFunctionArgs) => {
+ return {};
+};
+
+export const meta: MetaFunction
= ({ data }) => {
+ return [
+ { title: 'How It Works - Chef Luis Velez' },
+ {
+ name: 'description',
+ content: 'Learn how our culinary experience booking process works. From browsing menus to enjoying your event, we make it simple and transparent.'
+ },
+ { property: 'og:title', content: 'How It Works - Chef Luis Velez' },
+ {
+ property: 'og:description',
+ content: 'Step-by-step guide to booking your personalized culinary experience with Chef Luis Velez.'
+ },
+ { property: 'og:type', content: 'website' },
+ { name: 'keywords', content: 'how it works, booking process, private chef, culinary experience, chef services' },
+ ];
+};
+
+interface ProcessStep {
+ step: number;
+ title: string;
+ description: string;
+ timeline: string;
+ details: string[];
+ icon: string;
+}
+
+const processSteps: ProcessStep[] = [
+ {
+ step: 1,
+ title: "Browse & Request",
+ description: "Explore our menu collections and choose your preferred experience type. Submit a request with your event details.",
+ timeline: "5 minutes",
+ details: [
+ "Browse available menu templates",
+ "Select experience type (Buffet, Cooking Class, or Plated Dinner)",
+ "Choose your date, time, and party size",
+ "Provide event location and special requirements"
+ ],
+ icon: "🍽️"
+ },
+ {
+ step: 2,
+ title: "Chef Review & Approval",
+ description: "Chef Luis reviews your request and confirms availability. You'll receive a detailed proposal with menu customizations.",
+ timeline: "24-48 hours",
+ details: [
+ "Chef reviews your event requirements",
+ "Availability and logistics confirmation",
+ "Menu customization based on preferences",
+ "Detailed proposal with final pricing"
+ ],
+ icon: "👨🍳"
+ },
+ {
+ step: 3,
+ title: "Book & Purchase",
+ description: "Once approved, your event becomes available for purchase. Buy tickets for your guests with secure payment processing.",
+ timeline: "Immediate",
+ details: [
+ "Event becomes available as a product",
+ "Secure online ticket purchasing",
+ "Automatic confirmation emails",
+ "Guest management tools"
+ ],
+ icon: "🎫"
+ },
+ {
+ step: 4,
+ title: "Experience & Enjoy",
+ description: "Chef Luis arrives at your location with all ingredients and equipment. Relax and enjoy your personalized culinary experience.",
+ timeline: "Event day",
+ details: [
+ "Chef arrives with all necessary equipment",
+ "Fresh, premium ingredients sourced locally",
+ "Professional preparation and service",
+ "Cleanup included in service"
+ ],
+ icon: "🎉"
+ }
+];
+
+interface StepCardProps {
+ step: ProcessStep;
+ isLast?: boolean;
+ className?: string;
+}
+
+const StepCard: FC = ({ step, isLast = false, className }) => {
+ return (
+
+ {/* Connection line to next step */}
+ {!isLast && (
+
+ )}
+
+
+
+ {/* Step number and icon */}
+
+
+ {step.step}
+
+
{step.icon}
+
+
+ {/* Step title and timeline */}
+
+
+ {step.title}
+
+
+ {step.timeline}
+
+
+
+ {/* Description */}
+
+ {step.description}
+
+
+ {/* Details list */}
+
+
Key Points:
+
+ {step.details.map((detail, index) => (
+
+
+ {detail}
+
+ ))}
+
+
+
+
+
+ );
+};
+
+interface FAQItem {
+ question: string;
+ answer: string;
+}
+
+const faqItems: FAQItem[] = [
+ {
+ question: "How far in advance should I book?",
+ answer: "We recommend booking 1-2 weeks in advance, especially for weekend events. However, we can often accommodate shorter notice requests."
+ },
+ {
+ question: "What's included in the service?",
+ answer: "All ingredients, equipment, preparation, service, and cleanup are included. You just provide the location and we handle the rest."
+ },
+ {
+ question: "Can menus be customized?",
+ answer: "Absolutely! Chef Luis works with you to customize any menu based on dietary restrictions, preferences, and seasonal availability."
+ },
+ {
+ question: "What is your service area?",
+ answer: "We primarily serve the greater metropolitan area within a 30-mile radius. For events outside this area, additional travel fees may apply."
+ },
+ {
+ question: "Do you accommodate dietary restrictions?",
+ answer: "Yes! We can accommodate most dietary restrictions including vegetarian, vegan, gluten-free, and specific allergies. Please mention these when submitting your request."
+ },
+ {
+ question: "What happens if I need to cancel?",
+ answer: "Cancellation policies vary by event type and timing. Once your event is confirmed, you'll receive detailed terms and conditions including our cancellation policy."
+ },
+ {
+ question: "How does pricing work?",
+ answer: "Our pricing is transparent and per-person based: Buffet Style ($99.99), Cooking Class ($119.99), and Plated Dinner ($149.99). No hidden fees or service charges."
+ },
+ {
+ question: "Can I add more guests after booking?",
+ answer: "Additional guests can often be accommodated subject to availability and venue capacity. Contact us as soon as possible to discuss modifications."
+ },
+ {
+ question: "What equipment do you bring?",
+ answer: "We bring all necessary cooking equipment, serving dishes, and utensils. We only need access to your kitchen facilities (stove, oven, sink) and adequate space to prepare and serve."
+ }
+];
+
+export default function HowItWorksPage() {
+ return (
+ <>
+ {/* Page Header */}
+
+
+
+ How It Works
+
+
+ From browsing menus to enjoying your culinary experience, we've made the process simple and transparent.
+ Here's how your culinary journey unfolds:
+
+
+
+ {/* Process Steps */}
+
+ {processSteps.map((step, index) => (
+
+ ))}
+
+
+ {/* Call to Action */}
+
+
+
+ Ready to Start Your Culinary Journey?
+
+
+ Begin by exploring our menu collections or jump straight to requesting
+ your custom culinary experience. No commitments until the chef approves your event.
+
+
+
+
+
+
+ {/* FAQ Section */}
+
+
+
+ Frequently Asked Questions
+
+
+ Find answers to common questions about our culinary experiences and booking process.
+
+
+
+
+ {faqItems.map((faq, index) => (
+
+
+ {faq.question}
+
+
+ {faq.answer}
+
+
+ ))}
+
+
+ {/* Contact CTA */}
+
+
+
+ Still Have Questions?
+
+
+ We're here to help! Reach out to us directly for any specific questions about your event.
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/apps/storefront/app/routes/menus.$menuId.tsx b/apps/storefront/app/routes/menus.$menuId.tsx
new file mode 100644
index 000000000..bb9219503
--- /dev/null
+++ b/apps/storefront/app/routes/menus.$menuId.tsx
@@ -0,0 +1,116 @@
+import { Breadcrumbs } from '@app/components/common/breadcrumbs';
+import { Container } from '@app/components/common/container';
+import { MenuTemplate } from '@app/templates/MenuTemplate';
+import { fetchMenuById } from '@libs/util/server/data/menus.server';
+import { getMergedPageMeta } from '@libs/util/page';
+import { type LoaderFunctionArgs, type MetaFunction, redirect } from 'react-router';
+import { useLoaderData } from 'react-router';
+import HomeIcon from '@heroicons/react/24/solid/HomeIcon';
+
+export const loader = async (args: LoaderFunctionArgs) => {
+ try {
+ const menuId = args.params.menuId;
+
+ if (!menuId) {
+ throw redirect('/menus');
+ }
+
+ const { menu } = await fetchMenuById(menuId);
+
+ if (!menu) {
+ throw redirect('/404');
+ }
+
+ return { menu, success: true };
+ } catch (error) {
+ console.error('Failed to load menu:', error);
+ throw redirect('/404');
+ }
+};
+
+export type MenuPageLoaderData = typeof loader;
+
+export const meta: MetaFunction = ({ data, location }) => {
+ const menu = data?.menu;
+
+ if (!menu) {
+ return [
+ { title: 'Menu Not Found | Chef Luis Velez' },
+ { name: 'robots', content: 'noindex' },
+ ];
+ }
+
+ const courseCount = menu.courses?.length || 0;
+ const dishCount = menu.courses?.reduce((acc, course) => acc + (course.dishes?.length || 0), 0) || 0;
+
+ const title = `${menu.name} - Menu Template | Chef Luis Velez`;
+ const description = `${menu.name} featuring ${courseCount} courses and ${dishCount} dishes. Perfect for cooking classes, plated dinners, or buffet-style events. Request this menu for your culinary experience.`;
+
+ return [
+ { title },
+ { name: 'description', content: description },
+ { property: 'og:title', content: title },
+ { property: 'og:description', content: description },
+ { property: 'og:type', content: 'article' },
+ { property: 'og:url', content: `https://yourstore.com${location.pathname}` },
+ { name: 'keywords', content: `${menu.name}, chef menu, culinary experience, private dining, cooking class, chef Luis Velez` },
+ // Structured data for Recipe/Menu
+ {
+ tagName: 'script',
+ type: 'application/ld+json',
+ children: JSON.stringify({
+ '@context': 'https://schema.org',
+ '@type': 'Recipe',
+ name: menu.name,
+ description: `Professional chef menu template with ${courseCount} courses`,
+ author: {
+ '@type': 'Person',
+ name: 'Chef Luis Velez',
+ },
+ recipeCategory: 'Chef Menu Template',
+ recipeCuisine: 'Contemporary',
+ offers: {
+ '@type': 'Offer',
+ priceCurrency: 'USD',
+ price: '99.99',
+ description: 'Starting price per person for culinary experiences'
+ }
+ }),
+ },
+ ];
+};
+
+export default function MenuDetailRoute() {
+ const { menu } = useLoaderData();
+
+ if (!menu) return null;
+
+ const breadcrumbs = [
+ {
+ label: (
+
+
+ Home
+
+ ),
+ url: `/`,
+ },
+ {
+ label: 'Menu Templates',
+ url: '/menus',
+ },
+ {
+ label: menu.name,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/storefront/app/routes/menus._index.tsx b/apps/storefront/app/routes/menus._index.tsx
new file mode 100644
index 000000000..ff5929b0d
--- /dev/null
+++ b/apps/storefront/app/routes/menus._index.tsx
@@ -0,0 +1,133 @@
+import { Breadcrumbs } from '@app/components/common/breadcrumbs';
+import { Container } from '@app/components/common/container';
+import { MenuCarousel } from '@app/components/menu/MenuCarousel';
+import HomeIcon from '@heroicons/react/24/solid/HomeIcon';
+import { fetchMenus } from '@libs/util/server/data/menus.server';
+import { getMergedPageMeta } from '@libs/util/page';
+import type { LoaderFunctionArgs, MetaFunction } from 'react-router';
+import { useLoaderData } from 'react-router';
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ try {
+ const url = new URL(request.url);
+ const limit = parseInt(url.searchParams.get('limit') || '12');
+ const offset = parseInt(url.searchParams.get('offset') || '0');
+ // Search is not needed for this page; only fetch paginated menus
+ const { menus, count } = await fetchMenus({ limit, offset });
+
+ return {
+ menus,
+ count,
+ limit,
+ offset,
+ success: true
+ };
+ } catch (error) {
+ console.error('Failed to load menus:', error);
+ return {
+ menus: [],
+ count: 0,
+ limit: 12,
+ offset: 0,
+ success: false
+ };
+ }
+};
+
+export const meta: MetaFunction = ({ data }) => {
+ const count = data?.count || 0;
+
+ const title = `Menus (${count}) | Chef Luis Velez`;
+
+ const description = `Browse ${count} expertly crafted menus by Chef Luis Velez. From intimate dinners to group celebrations, find the perfect menu for your culinary experience.`;
+
+ return [
+ { title },
+ { name: 'description', content: description },
+ { property: 'og:title', content: title },
+ { property: 'og:description', content: description },
+ { property: 'og:type', content: 'website' },
+ { name: 'keywords', content: 'chef menus, private dining menus, tasting menus, cooking class menus, Chef Luis Velez' },
+ ...(count === 0 ? [{ name: 'robots', content: 'noindex' }] : []),
+ ];
+};
+
+export type MenusIndexRouteLoader = typeof loader;
+
+export default function MenusIndexRoute() {
+ const data = useLoaderData();
+
+ if (!data) return null;
+
+ const { menus, count, limit, offset } = data;
+
+ const breadcrumbs = [
+ {
+ label: (
+
+
+ Home
+
+ ),
+ url: `/`,
+ },
+ {
+ label: 'Menus',
+ },
+ ];
+
+ return (
+
+
+
+
+
+ {/* Page Header */}
+
+
+ Menus
+
+
+ Explore our carefully crafted menus, each designed to create memorable culinary experiences.
+ Every menu can be customized to your preferences and dietary requirements.
+
+
+
+ {/* Search removed by design as there are only a handful of menus */}
+
+ {/* Menus - carousel across breakpoints to showcase motion polish */}
+
+ {/* Heading */}
+ {count > 0 && (
+
+ {count} Menu{count !== 1 ? 's' : ''}
+
+ )}
+
+ {/* Horizontal snap carousel on all sizes */}
+
+
+
+ {/* Empty State */}
+ {count === 0 && (
+
+
+
🍽️
+
+ No menus available
+
+
+ We're currently preparing our menus. Check back soon for exciting culinary options!
+
+
+ Request Custom Event
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/storefront/app/routes/products.$productHandle.tsx b/apps/storefront/app/routes/products.$productHandle.tsx
index 9bc7e0220..94a73aa67 100644
--- a/apps/storefront/app/routes/products.$productHandle.tsx
+++ b/apps/storefront/app/routes/products.$productHandle.tsx
@@ -1,9 +1,11 @@
import { ProductReviewSection } from '@app/components/reviews/ProductReviewSection';
import ProductList from '@app/components/sections/ProductList';
import { ProductTemplate } from '@app/templates/ProductTemplate';
-import { getMergedProductMeta } from '@libs/util/products';
+import { EventProductDetails } from '@app/components/product/EventProductDetails';
+import { getMergedProductMeta, isEventProduct } from '@libs/util/products';
import { fetchProductReviewStats, fetchProductReviews } from '@libs/util/server/data/product-reviews.server';
import { fetchProducts } from '@libs/util/server/products.server';
+import { fetchChefEventForProduct, fetchMenuForProduct } from '@libs/util/server/data/event-products.server';
import { withPaginationParams } from '@libs/util/withPaginationParams';
import { type LoaderFunctionArgs, type MetaFunction, redirect } from 'react-router';
import { useLoaderData } from 'react-router';
@@ -16,7 +18,7 @@ export const loader = async (args: LoaderFunctionArgs) => {
const { products } = await fetchProducts(args.request, {
handle: args.params.productHandle,
- fields: '*categories',
+ fields: '*categories,variants.*,variants.sku,variants.options,variants.inventory_quantity,variants.manage_inventory',
});
if (!products.length) throw redirect('/404');
@@ -41,7 +43,20 @@ export const loader = async (args: LoaderFunctionArgs) => {
}),
]);
- return { product, productReviews, productReviewStats };
+ // Check if this is an event product and fetch additional data
+ let chefEvent = null;
+ let menu = null;
+
+ const isEvent = isEventProduct(product);
+
+ if (isEvent) {
+ [chefEvent, menu] = await Promise.all([
+ fetchChefEventForProduct(product),
+ fetchMenuForProduct(product),
+ ]);
+ }
+
+ return { product, productReviews, productReviewStats, chefEvent, menu };
};
export type ProductPageLoaderData = typeof loader;
@@ -49,8 +64,38 @@ export type ProductPageLoaderData = typeof loader;
export const meta: MetaFunction = getMergedProductMeta;
export default function ProductDetailRoute() {
- const { product, productReviews, productReviewStats } = useLoaderData();
+ const { product, productReviews, productReviewStats, chefEvent, menu } = useLoaderData();
+
+ console.log('ProductDetailRoute Debug:', {
+ productId: product.id,
+ productTitle: product.title,
+ variants: product.variants?.map(v => ({
+ id: v.id,
+ sku: v.sku,
+ options: v.options,
+ inventory_quantity: v.inventory_quantity
+ })),
+ isEvent: isEventProduct(product)
+ });
+
+ // Check if this is an event product
+ if (isEventProduct(product)) {
+ console.log('Rendering EventProductDetails');
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+ console.log('Rendering regular ProductTemplate');
+ // Regular product template
return (
<>
{
+ const selectedDate = new Date(date);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ // Must be at least 7 days from now
+ const minDate = new Date();
+ minDate.setDate(minDate.getDate() + 7);
+ minDate.setHours(0, 0, 0, 0);
+
+ return selectedDate >= minDate;
+ }, "Events must be scheduled at least 7 days in advance").refine((date) => {
+ const selectedDate = new Date(date);
+ const maxDate = new Date();
+ maxDate.setMonth(maxDate.getMonth() + 6);
+
+ return selectedDate <= maxDate;
+ }, "Events cannot be scheduled more than 6 months in advance"),
+
+ requestedTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Please enter a valid time (HH:MM format)").refine((time) => {
+ const [hours, minutes] = time.split(':').map(Number);
+ // Business hours: 10:00 AM to 8:30 PM
+ const startTime = 10 * 60; // 10:00 AM in minutes
+ const endTime = 20 * 60 + 30; // 8:30 PM in minutes
+ const timeInMinutes = hours * 60 + minutes;
+
+ return timeInMinutes >= startTime && timeInMinutes <= endTime;
+ }, "Please select a time between 10:00 AM and 8:30 PM"),
+
+ // Step 4: Party Size
+ partySize: z.number().min(2, "Minimum 2 guests required").max(50, "Maximum 50 guests allowed"),
+
+ // Step 5: Location (now only customer location)
+ locationAddress: z.string().min(10, "Please provide a complete address").max(500, "Address is too long"),
+
+ // Step 6: Contact Details
+ firstName: z.string().min(1, "First name is required").max(50, "First name is too long"),
+ lastName: z.string().min(1, "Last name is required").max(50, "Last name is too long"),
+ email: z.string().email("Please enter a valid email address").max(255, "Email address is too long"),
+ phone: z.string().optional().refine((phone) => {
+ if (!phone) return true; // Optional field
+ // Remove all non-digits to check length
+ const digitsOnly = phone.replace(/\D/g, '');
+ return digitsOnly.length === 10;
+ }, "Please enter a valid 10-digit phone number"),
+
+ // Step 7: Special Requests
+ specialRequirements: z.string().optional().refine((req) => {
+ if (!req) return true;
+ return req.length <= 1000;
+ }, "Special requirements must be less than 1000 characters"),
+ notes: z.string().optional().refine((notes) => {
+ if (!notes) return true;
+ return notes.length <= 1000;
+ }, "Notes must be less than 1000 characters"),
+
+ // Hidden fields
+ currentStep: z.number().optional(),
+});
+
+export type EventRequestFormData = z.infer;
+
+export const loader = async (args: LoaderFunctionArgs) => {
+ try {
+ // Fetch menus for menu selector step
+ const menusData = await fetchMenus({ limit: 20 });
+ return {
+ menus: menusData.menus || [],
+ success: true
+ };
+ } catch (error) {
+ console.error('Failed to load menus for request page:', error);
+ return {
+ menus: [],
+ success: false
+ };
+ }
+};
+
+export const action = async (actionArgs: ActionFunctionArgs) => {
+ console.log('🔄 REQUEST ACTION: Starting form submission');
+
+ try {
+ const { errors, data } = await getValidatedFormData(
+ actionArgs.request,
+ zodResolver(eventRequestSchema),
+ );
+
+ console.log('📝 REQUEST ACTION: Form data received:', {
+ hasData: !!data,
+ hasErrors: !!errors,
+ errorKeys: errors ? Object.keys(errors) : [],
+ });
+
+ if (errors) {
+ console.log('❌ REQUEST ACTION: Validation errors found:', errors);
+ return { errors, status: 400 };
+ }
+
+ console.log('✅ REQUEST ACTION: Form validation passed, creating request with data:', {
+ eventType: data.eventType,
+ partySize: data.partySize,
+ requestedDate: data.requestedDate,
+ requestedTime: data.requestedTime,
+ email: data.email,
+ firstName: data.firstName,
+ lastName: data.lastName,
+ });
+
+ console.log('✅ REQUEST ACTION: Raw requestedDate received:', data.requestedDate);
+ console.log('✅ REQUEST ACTION: Date validation check:', {
+ isString: typeof data.requestedDate === 'string',
+ length: data.requestedDate?.length,
+ isValidDate: data.requestedDate ? !isNaN(Date.parse(data.requestedDate)) : false,
+ parsedDate: data.requestedDate ? new Date(data.requestedDate).toISOString() : null,
+ });
+
+ // Create the chef event request
+ const response = await createChefEventRequest({
+ requestedDate: data.requestedDate,
+ requestedTime: data.requestedTime,
+ partySize: data.partySize,
+ eventType: data.eventType,
+ templateProductId: data.menuId,
+ locationType: 'customer_location', // Default to customer location since we removed the selection
+ locationAddress: data.locationAddress,
+ firstName: data.firstName,
+ lastName: data.lastName,
+ email: data.email,
+ phone: data.phone,
+ notes: data.notes,
+ specialRequirements: data.specialRequirements,
+ });
+
+ console.log('🎉 REQUEST ACTION: Chef event created successfully:', {
+ eventId: response.chefEvent.id,
+ status: response.chefEvent.status,
+ });
+
+ // Return success data instead of redirect for better client handling
+ const successUrl = `/request/success?eventId=${response.chefEvent.id}`;
+ console.log('🔀 REQUEST ACTION: Returning success data for client redirect:', successUrl);
+
+ // Try using redirect directly instead of client-side redirect
+ console.log('🔀 REQUEST ACTION: Attempting server-side redirect to:', successUrl);
+ return redirect(successUrl);
+
+ } catch (error) {
+ console.error('💥 REQUEST ACTION: Failed to create chef event request:', error);
+
+ // Log more details about the error
+ if (error instanceof Error) {
+ console.error('💥 REQUEST ACTION: Error details:', {
+ name: error.name,
+ message: error.message,
+ stack: error.stack,
+ });
+ }
+
+ return {
+ errors: {
+ root: {
+ message: 'Failed to submit request. Please try again.'
+ }
+ },
+ status: 500
+ };
+ }
+};
+
+export const meta: MetaFunction = ({ data }) => {
+ return [
+ { title: 'Request Your Culinary Experience - Chef Luis Velez' },
+ {
+ name: 'description',
+ content: 'Book a personalized culinary experience with Chef Luis. Choose from cooking classes, plated dinners, or buffet-style events.'
+ },
+ { property: 'og:title', content: 'Request Your Culinary Experience - Chef Luis Velez' },
+ {
+ property: 'og:description',
+ content: 'Submit a request for your personalized culinary experience. Chef Luis will review and create a custom proposal for your event.'
+ },
+ { property: 'og:type', content: 'website' },
+ { name: 'keywords', content: 'request event, book chef, cooking class, private dining, culinary experience' },
+ ];
+};
+
+export default function RequestPage() {
+ const { menus } = useLoaderData();
+ const [searchParams] = useSearchParams();
+
+ // Get initial values from URL params (e.g., pre-selected menu or event type)
+ const initialValues = {
+ menuId: searchParams.get('menu') || undefined,
+ eventType: searchParams.get('type') as EventRequestFormData['eventType'] || undefined,
+ };
+
+ return (
+
+
+
+ Request Your Culinary Experience
+
+
+ Tell us about your event and Chef Luis will create a personalized proposal for your culinary experience.
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/storefront/app/routes/request.success.tsx b/apps/storefront/app/routes/request.success.tsx
new file mode 100644
index 000000000..575d1dd02
--- /dev/null
+++ b/apps/storefront/app/routes/request.success.tsx
@@ -0,0 +1,237 @@
+import { Container } from '@app/components/common/container/Container';
+import { Button } from '@app/components/common/buttons/Button';
+import { CheckCircleIcon, ClockIcon, EnvelopeIcon, PhoneIcon } from '@heroicons/react/24/outline';
+import type { LoaderFunctionArgs, MetaFunction, ActionFunctionArgs } from 'react-router';
+import { useLoaderData, useSearchParams, Link, redirect } from 'react-router';
+
+export const meta: MetaFunction = () => {
+ return [
+ { title: 'Request Submitted Successfully - Chef Luis Velez' },
+ {
+ name: 'description',
+ content: 'Your culinary experience request has been submitted successfully. Chef Luis will review your request and respond within 24 hours.'
+ },
+ { property: 'og:title', content: 'Request Submitted Successfully - Chef Luis Velez' },
+ {
+ property: 'og:description',
+ content: 'Your culinary experience request has been submitted successfully. Chef Luis will review your request and respond within 24 hours.'
+ },
+ { property: 'og:type', content: 'website' },
+ { name: 'robots', content: 'noindex' }, // Don't index success pages
+ ];
+};
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const url = new URL(request.url);
+ const searchParams = new URLSearchParams(url.search);
+ const eventId = searchParams.get('eventId') || '';
+
+ console.log('🎉 SUCCESS PAGE: Loader called with eventId:', eventId);
+
+ if (!eventId) {
+ console.log('🎉 SUCCESS PAGE: No eventId found, redirecting to home');
+ throw redirect('/');
+ }
+
+ return {
+ eventId: searchParams.get('eventId') || 'unknown',
+ supportEmail: 'chef@chefluisvelez.com',
+ supportPhone: '(555) 123-4567',
+ responseTime: '24 hours',
+ };
+};
+
+// Handle POST requests to success page (redirect to GET)
+export const action = async ({ request }: ActionFunctionArgs) => {
+ console.log('🔧 SUCCESS PAGE: Received POST request, redirecting to GET');
+
+ const url = new URL(request.url);
+ const eventId = url.searchParams.get('eventId') || '';
+ const redirectUrl = `/request/success?eventId=${eventId}`;
+
+ console.log('🔧 SUCCESS PAGE: Redirecting to:', redirectUrl);
+ return redirect(redirectUrl);
+};
+
+export default function RequestSuccessPage() {
+ const { eventId, supportEmail, supportPhone, responseTime } = useLoaderData();
+
+ console.log('🎉 SUCCESS PAGE: Component rendered');
+ console.log('🎉 SUCCESS PAGE: Event ID from loader:', eventId);
+
+ return (
+
+
+ {/* Success Icon */}
+
+
+
+
+ {/* Success Message */}
+
+ Request Submitted Successfully!
+
+
+ Thank you for your interest in a personalized culinary experience with Chef Luis.
+ Your request has been received and will be reviewed shortly.
+
+
+ {/* Request Reference */}
+ {eventId && (
+
+
+ Request Reference
+
+
+ Keep this reference number for your records:
+
+
+ {eventId.slice(0, 8).toUpperCase()}
+
+
+ )}
+
+ {/* What Happens Next */}
+
+
+ What Happens Next?
+
+
+
+
+
+ 1
+
+
+
+ Review & Assessment
+
+
+ Chef Luis will review your request details, including menu preferences,
+ party size, and special requirements to ensure she can create the perfect experience.
+
+
+
+
+
+
+ 2
+
+
+
+ Custom Proposal
+
+
+ You'll receive a detailed proposal including the final menu, timeline,
+ pricing breakdown, and any special accommodations for your event.
+
+
+
+
+
+
+ 3
+
+
+
+ Confirmation & Booking
+
+
+ Once approved, your event will be added to our calendar and you'll receive
+ a booking link to purchase tickets for your guests.
+
+
+
+
+
+
+ 4
+
+
+
+ Event Preparation
+
+
+ Chef Luis will coordinate final details, source ingredients,
+ and prepare everything needed for your exceptional culinary experience.
+
+
+
+
+
+
+ {/* Timeline & Contact */}
+
+
+
+
+
+ Response Timeline
+
+
+
+ Within {responseTime}
+
+
+ Chef Luis personally reviews each request and will respond via email
+ with either approval and next steps, or questions for clarification.
+
+
+
+
+
+
+
+ Questions?
+
+
+
+ We're here to help!
+
+
+
Email: {supportEmail}
+
Phone: {supportPhone}
+
+
+
+
+ {/* Action Buttons */}
+
+
+ Return to Homepage
+
+
+ Browse More Menus
+
+
+
+ {/* Additional Information */}
+
+
+ Important Notes
+
+
+
+ • No payment required: Payment is only collected after your event is confirmed and tickets are made available.
+
+
+ • Flexible planning: Chef Luis can accommodate most dietary restrictions and special requests.
+
+
+ • Group bookings: Once approved, you'll receive a unique booking link to share with your guests.
+
+
+ • Cancellation policy: Full details will be provided in your event proposal.
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/storefront/app/styles/global.css b/apps/storefront/app/styles/global.css
index 77a788a99..fb0bfe85d 100644
--- a/apps/storefront/app/styles/global.css
+++ b/apps/storefront/app/styles/global.css
@@ -144,6 +144,15 @@
.position-right-bottom {
@apply left-full right-auto bottom-0 top-auto ml-2 origin-bottom-right;
}
+
+ /* Hide scrollbars (while keeping scroll functionality) */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ .no-scrollbar {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
}
.mkt-section {
diff --git a/apps/storefront/app/templates/MenuTemplate.tsx b/apps/storefront/app/templates/MenuTemplate.tsx
new file mode 100644
index 000000000..87048e918
--- /dev/null
+++ b/apps/storefront/app/templates/MenuTemplate.tsx
@@ -0,0 +1,277 @@
+import { ActionList } from '@app/components/common/actions-list/ActionList';
+import { Image } from '@app/components/common/images/Image';
+import { Container } from '@app/components/common/container/Container';
+import type { StoreMenuDTO } from '@libs/util/server/data/menus.server';
+import clsx from 'clsx';
+import type { FC } from 'react';
+
+export interface MenuTemplateProps {
+ menu: StoreMenuDTO;
+ className?: string;
+}
+
+interface ExperienceType {
+ id: string;
+ name: string;
+ price: string;
+ description: string;
+ duration: string;
+}
+
+const experienceTypes: ExperienceType[] = [
+ {
+ id: 'buffet_style',
+ name: 'Buffet Style',
+ price: '$99.99',
+ description: 'Perfect for larger groups with dishes served buffet-style',
+ duration: '2.5 hours'
+ },
+ {
+ id: 'cooking_class',
+ name: 'Cooking Class',
+ price: '$119.99',
+ description: 'Interactive experience learning to prepare these dishes',
+ duration: '3 hours'
+ },
+ {
+ id: 'plated_dinner',
+ name: 'Plated Dinner',
+ price: '$149.99',
+ description: 'Elegant multi-course dining experience',
+ duration: '4 hours'
+ }
+];
+
+interface CourseProps {
+ course: StoreMenuDTO['courses'][0];
+ courseNumber: number;
+}
+
+const CourseSection: FC = ({ course, courseNumber }) => {
+ return (
+
+
+
+ {courseNumber}
+
+
{course.name}
+
+
+
+ {course.dishes.map((dish) => (
+
+ ))}
+
+
+ );
+};
+
+interface DishProps {
+ dish: StoreMenuDTO['courses'][0]['dishes'][0];
+}
+
+const DishCard: FC = ({ dish }) => {
+ return (
+
+
{dish.name}
+
+ {dish.description && (
+
{dish.description}
+ )}
+
+ {dish.ingredients.length > 0 && (
+
+
Ingredients:
+
+ {dish.ingredients.map((ingredient) => (
+
+ {ingredient.name}
+ {ingredient.optional && ' (optional)'}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+const PricingSection: FC<{ menuName: string }> = ({ menuName }) => {
+ return (
+
+
+ Choose Your Experience with {menuName}
+
+
+
+ {experienceTypes.map((experience, index) => (
+
+ {index === 1 && (
+
+
+ Most Popular
+
+
+ )}
+
+
+
+ {experience.name}
+
+
+ {experience.price}
+
+
per person
+
+ {experience.description}
+
+
+ Duration: {experience.duration}
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+export const MenuTemplate: FC = ({ menu, className }) => {
+ const courseCount = menu.courses?.length || 0;
+ const totalDishes = menu.courses?.reduce((acc, course) => acc + (course.dishes?.length || 0), 0) || 0;
+
+ return (
+
+ {/* Menu Header */}
+
+
+
+
+
+
+
+ {menu.name}
+
+
+
+
+
+
+ {courseCount} Course{courseCount !== 1 ? 's' : ''}
+
+
+
+
+
+ 3-4 Hours Experience
+
+
+
+
+
+ {totalDishes} Signature Dishes
+
+
+
+
+
+ {/* Pricing Section */}
+
+
+ {/* Menu Courses */}
+
+
+
+ Menu Courses
+
+
+ Each course is carefully designed to create a progressive culinary journey.
+ All ingredients are sourced fresh and can be adapted to dietary requirements.
+
+
+
+
+ {menu.courses.map((course, index) => (
+
+ ))}
+
+
+
+ {/* Chef Notes Section */}
+
+
+
+ Chef's Notes
+
+
+ This {menu.name} menu represents a harmonious blend of flavors and techniques that I've perfected
+ over years of culinary experience. Each dish can be customized to accommodate dietary restrictions
+ and personal preferences while maintaining the integrity of the overall experience.
+
+
+
+ Dietary Accommodations: Vegetarian, Vegan, Gluten-Free, and other dietary requirements can be accommodated with advance notice.
+
+
+
+
+
+ {/* Final CTA */}
+
+
+ Ready to Experience {menu.name}?
+
+
+ Transform your next special occasion with this expertly crafted menu.
+ Each experience is personalized to your preferences and delivered with professional excellence.
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/apps/storefront/libs/config/site/navigation-items.ts b/apps/storefront/libs/config/site/navigation-items.ts
index c70a81b09..dad9c99bd 100644
--- a/apps/storefront/libs/config/site/navigation-items.ts
+++ b/apps/storefront/libs/config/site/navigation-items.ts
@@ -3,25 +3,33 @@ import { NavigationCollection, NavigationItemLocation } from '@libs/types';
export const headerNavigationItems: NavigationCollection = [
{
id: 1,
- label: 'View our Blends',
- url: '/categories/blends',
+ label: 'Our Menus',
+ url: '/menus',
sort_order: 0,
location: NavigationItemLocation.header,
new_tab: false,
},
{
- id: 3,
- label: 'Our Story',
- url: '/about-us',
+ id: 2,
+ label: 'How It Works',
+ url: '/how-it-works',
sort_order: 1,
location: NavigationItemLocation.header,
new_tab: false,
},
{
- id: 2,
- label: 'Shop All',
- url: '/products',
- sort_order: 1,
+ id: 3,
+ label: 'Request Event',
+ url: '/request',
+ sort_order: 2,
+ location: NavigationItemLocation.header,
+ new_tab: false,
+ },
+ {
+ id: 4,
+ label: 'About Chef',
+ url: '/about',
+ sort_order: 3,
location: NavigationItemLocation.header,
new_tab: false,
},
@@ -30,34 +38,50 @@ export const headerNavigationItems: NavigationCollection = [
export const footerNavigationItems: NavigationCollection = [
{
id: 1,
- label: 'Shop All',
- url: '/products',
+ label: 'Our Menus',
+ url: '/menus',
location: NavigationItemLocation.footer,
sort_order: 1,
new_tab: false,
},
{
id: 2,
- label: 'Light Roasts',
- url: '/collections/light-roasts',
+ label: 'Cooking Classes',
+ url: '/experiences#cooking_class',
location: NavigationItemLocation.footer,
- sort_order: 1,
+ sort_order: 2,
new_tab: false,
},
{
id: 3,
- label: 'Medium Roasts',
- url: '/collections/medium-roasts',
+ label: 'Plated Dinners',
+ url: '/experiences#plated_dinner',
location: NavigationItemLocation.footer,
- sort_order: 1,
+ sort_order: 3,
new_tab: false,
},
{
id: 4,
- label: 'Dark Roasts',
- url: '/collections/dark-roasts',
+ label: 'Buffet Style',
+ url: '/experiences#buffet_style',
location: NavigationItemLocation.footer,
- sort_order: 1,
+ sort_order: 4,
+ new_tab: false,
+ },
+ {
+ id: 5,
+ label: 'How It Works',
+ url: '/how-it-works',
+ location: NavigationItemLocation.footer,
+ sort_order: 5,
+ new_tab: false,
+ },
+ {
+ id: 6,
+ label: 'About Chef Velez',
+ url: '/about',
+ location: NavigationItemLocation.footer,
+ sort_order: 6,
new_tab: false,
},
];
diff --git a/apps/storefront/libs/config/site/site-settings.ts b/apps/storefront/libs/config/site/site-settings.ts
index 782511e45..63472257c 100644
--- a/apps/storefront/libs/config/site/site-settings.ts
+++ b/apps/storefront/libs/config/site/site-settings.ts
@@ -3,9 +3,9 @@ import { config } from '@libs/util/server/config.server';
export const siteSettings: SiteSettings = {
storefront_url: config.STOREFRONT_URL,
- description: '',
+ description: 'Chef Velez offers premium private chef experiences including cooking classes, plated dinners, and buffet-style events. Restaurant-quality cuisine crafted in your home.',
favicon: '/favicon.svg',
- social_facebook: 'https://www.facebook.com/',
- social_instagram: 'https://www.instagram.com/',
- social_twitter: 'https://www.twitter.com/',
+ social_facebook: '',
+ social_instagram: '',
+ social_twitter: '',
};
diff --git a/apps/storefront/libs/constants/pricing.ts b/apps/storefront/libs/constants/pricing.ts
new file mode 100644
index 000000000..58e333107
--- /dev/null
+++ b/apps/storefront/libs/constants/pricing.ts
@@ -0,0 +1,33 @@
+export const PRICING_STRUCTURE = {
+ buffet_style: 99.99,
+ cooking_class: 119.99,
+ plated_dinner: 149.99,
+} as const;
+
+export type EventType = keyof typeof PRICING_STRUCTURE;
+
+export const getEventTypeDisplayName = (eventType: EventType): string => {
+ switch (eventType) {
+ case 'cooking_class':
+ return 'Cooking Class';
+ case 'plated_dinner':
+ return 'Plated Dinner';
+ case 'buffet_style':
+ return 'Buffet Style';
+ default:
+ return 'Unknown Experience';
+ }
+};
+
+export const getEventTypeEstimatedDuration = (eventType: EventType): number => {
+ switch (eventType) {
+ case 'cooking_class':
+ return 3; // 3 hours
+ case 'plated_dinner':
+ return 4; // 4 hours
+ case 'buffet_style':
+ return 2.5; // 2.5 hours
+ default:
+ return 3;
+ }
+};
\ No newline at end of file
diff --git a/apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts b/apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts
index 0ee10a198..75ae97a46 100644
--- a/apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts
+++ b/apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts
@@ -1,3 +1,11 @@
export const amountToStripeExpressCheckoutAmount = (amount: number) => {
- return (amount ?? 0) * 100;
+ // Convert dollars to cents and ensure we have a clean integer
+ // Stripe requires amounts in the smallest currency unit (cents for USD)
+ const amountInCents = Math.round((amount ?? 0) * 100);
+ console.log('💰 amountToStripeExpressCheckoutAmount Debug:', {
+ originalAmount: amount,
+ amountInCents,
+ isInteger: Number.isInteger(amountInCents)
+ });
+ return amountInCents;
};
diff --git a/apps/storefront/libs/util/prices.ts b/apps/storefront/libs/util/prices.ts
index c76e5b463..af6881994 100644
--- a/apps/storefront/libs/util/prices.ts
+++ b/apps/storefront/libs/util/prices.ts
@@ -44,14 +44,34 @@ export function getCheapestProductVariant(product: StoreProduct) {
}
export function formatLineItemPrice(lineItem: StoreCartLineItem, regionCurrency: string) {
- return formatPrice(lineItem.unit_price, {
+ console.log('💰 formatLineItemPrice Debug:', {
+ lineItemId: lineItem.id,
+ productTitle: lineItem.product_title,
+ unitPrice: lineItem.unit_price,
+ quantity: lineItem.quantity,
+ regionCurrency: regionCurrency,
+ totalBeforeFormat: lineItem.unit_price && lineItem.quantity ? lineItem.unit_price * lineItem.quantity : null
+ });
+
+ // Medusa stores prices in dollars
+ const priceInDollars = lineItem.unit_price || 0;
+
+ return formatPrice(priceInDollars, {
currency: regionCurrency,
quantity: lineItem.quantity,
});
}
export function formatCartSubtotal(cart: StoreCart) {
- return formatPrice(cart.item_subtotal || 0, {
+ console.log('💰 formatCartSubtotal Debug:', {
+ itemSubtotal: cart.item_subtotal,
+ currencyCode: cart.region?.currency_code
+ });
+
+ // Medusa stores prices in dollars
+ const subtotalInDollars = cart.item_subtotal || 0;
+
+ return formatPrice(subtotalInDollars, {
currency: cart.region?.currency_code,
});
}
diff --git a/apps/storefront/libs/util/products.ts b/apps/storefront/libs/util/products.ts
index 2d4485ec0..b713eb58c 100644
--- a/apps/storefront/libs/util/products.ts
+++ b/apps/storefront/libs/util/products.ts
@@ -270,6 +270,44 @@ export function getVariantFromSelectedOptions(
});
}
+/**
+ * Detects if a product is an event product based on SKU pattern
+ * Event products have SKU pattern: EVENT-{eventId}-{date}-{type}
+ */
+export const isEventProduct = (product: StoreProduct): boolean => {
+ // Check if any variant has an EVENT- SKU pattern
+ return product.variants?.some(variant =>
+ variant.sku?.startsWith('EVENT-')
+ ) ?? false;
+}
+
+/**
+ * Extracts event information from product SKU
+ * @param sku - The SKU in format EVENT-{eventId}-{date}-{type}
+ * @returns Parsed event information or null if not an event SKU
+ */
+export const parseEventSku = (sku: string): { eventId: string; date: string; type: string } | null => {
+ if (!sku.startsWith('EVENT-')) return null
+
+ const parts = sku.split('-')
+ if (parts.length < 4) return null
+
+ const eventId = parts[1]
+ const date = parts[2]
+ const type = parts.slice(3).join('-') // Handle event types with hyphens
+
+ return { eventId, date, type }
+}
+
+/**
+ * Gets the event variant from a product (the variant with EVENT- SKU)
+ */
+export const getEventVariant = (product: StoreProduct): StoreProductVariant | undefined => {
+ return product.variants?.find(variant =>
+ variant.sku?.startsWith('EVENT-')
+ )
+}
+
export const getProductMeta: MetaFunction = ({ data, matches }) => {
const rootMatch = matches[0] as UIMatch;
const region = rootMatch.data?.region;
diff --git a/apps/storefront/libs/util/server/client.server.ts b/apps/storefront/libs/util/server/client.server.ts
index 0298f4812..0f88ee5dc 100644
--- a/apps/storefront/libs/util/server/client.server.ts
+++ b/apps/storefront/libs/util/server/client.server.ts
@@ -19,6 +19,14 @@ export const baseMedusaConfig = {
publishableKey: config.MEDUSA_PUBLISHABLE_KEY,
};
+// Log configuration for debugging
+console.log('⚙️ CLIENT CONFIG: Medusa configuration loaded:', {
+ baseUrl: baseMedusaConfig.baseUrl,
+ debug: baseMedusaConfig.debug,
+ hasPublishableKey: !!baseMedusaConfig.publishableKey,
+ publishableKeyPrefix: baseMedusaConfig.publishableKey?.slice(0, 10) + '...',
+});
+
export const sdk = new MedusaPluginsSDK({
...baseMedusaConfig,
});
diff --git a/apps/storefront/libs/util/server/data/cart.server.ts b/apps/storefront/libs/util/server/data/cart.server.ts
index 62e5851f8..0d4ab259b 100644
--- a/apps/storefront/libs/util/server/data/cart.server.ts
+++ b/apps/storefront/libs/util/server/data/cart.server.ts
@@ -74,7 +74,8 @@ export const addToCart = withAuthHeaders(
const cartId = await getCartId(request.headers);
if (cartId) {
- return await sdk.store.cart.createLineItem(
+ console.log('ADDING TO CART======>', cartId, variantId, quantity);
+ const resp = await sdk.store.cart.createLineItem(
cartId,
{
variant_id: variantId,
@@ -83,6 +84,8 @@ export const addToCart = withAuthHeaders(
{},
authHeaders,
);
+ console.log('Response', resp);
+ return resp;
}
const region = await getSelectedRegion(request.headers);
diff --git a/apps/storefront/libs/util/server/data/chef-events.server.ts b/apps/storefront/libs/util/server/data/chef-events.server.ts
new file mode 100644
index 000000000..273b29386
--- /dev/null
+++ b/apps/storefront/libs/util/server/data/chef-events.server.ts
@@ -0,0 +1,201 @@
+import { baseMedusaConfig } from '../client.server';
+
+export interface StoreChefEventDTO {
+ id: string;
+ status: 'pending' | 'confirmed' | 'cancelled' | 'completed';
+ requestedDate: string;
+ requestedTime: string;
+ partySize: number;
+ eventType: 'cooking_class' | 'plated_dinner' | 'buffet_style';
+ templateProductId?: string;
+ locationType: 'customer_location' | 'chef_location';
+ locationAddress: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone?: string;
+ notes?: string;
+ totalPrice: number;
+ specialRequirements?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface StoreCreateChefEventDTO {
+ requestedDate: string;
+ requestedTime: string;
+ partySize: number;
+ eventType: 'cooking_class' | 'plated_dinner' | 'buffet_style';
+ templateProductId?: string;
+ locationType: 'customer_location' | 'chef_location';
+ locationAddress: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ phone?: string;
+ notes?: string;
+ specialRequirements?: string;
+}
+
+export interface StoreChefEventResponse {
+ chefEvent: StoreChefEventDTO;
+ message: string;
+}
+
+export interface ChefEventError {
+ message: string;
+ errors?: Array<{
+ code: string;
+ path: string[];
+ message: string;
+ }>;
+}
+
+// Import pricing structure from shared constants
+import { PRICING_STRUCTURE } from '@libs/constants/pricing';
+export { PRICING_STRUCTURE };
+
+// Calculate total price for an event
+export const calculateEventPrice = (
+ eventType: keyof typeof PRICING_STRUCTURE,
+ partySize: number
+): number => {
+ return PRICING_STRUCTURE[eventType] * partySize;
+};
+
+// Create a chef event request
+export const createChefEventRequest = async (
+ data: StoreCreateChefEventDTO
+): Promise => {
+ console.log('🌐 API CLIENT: Starting chef event request to backend');
+ console.log('🌐 API CLIENT: Request data:', {
+ eventType: data.eventType,
+ partySize: data.partySize,
+ requestedDate: data.requestedDate,
+ requestedTime: data.requestedTime,
+ email: data.email,
+ baseUrl: baseMedusaConfig.baseUrl,
+ hasApiKey: !!baseMedusaConfig.publishableKey,
+ });
+
+ console.log('🌐 API CLIENT: Detailed date validation:', {
+ requestedDate: data.requestedDate,
+ dateType: typeof data.requestedDate,
+ dateLength: data.requestedDate?.length,
+ isValidISOString: data.requestedDate ? !isNaN(Date.parse(data.requestedDate)) : false,
+ sampleParseResult: data.requestedDate ? new Date(data.requestedDate) : null,
+ });
+
+ try {
+ const requestUrl = `${baseMedusaConfig.baseUrl}/store/chef-events`;
+ console.log('🌐 API CLIENT: Making request to:', requestUrl);
+
+ const response = await fetch(requestUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': baseMedusaConfig.publishableKey || '',
+ },
+ body: JSON.stringify(data),
+ });
+
+ console.log('🌐 API CLIENT: Response status:', response.status);
+ console.log('🌐 API CLIENT: Response headers:', Object.fromEntries(response.headers.entries()));
+
+ const responseData = await response.json();
+ console.log('🌐 API CLIENT: Response data:', responseData);
+
+ if (!response.ok) {
+ console.log('❌ API CLIENT: Request failed with status:', response.status);
+
+ // Handle validation errors
+ if (response.status === 400 && responseData.errors) {
+ console.log('❌ API CLIENT: Validation errors:', responseData.errors);
+ const error: ChefEventError = {
+ message: responseData.message || 'Validation error',
+ errors: responseData.errors,
+ };
+ throw error;
+ }
+
+ // Handle other errors
+ console.log('❌ API CLIENT: Other error:', responseData.message);
+ throw new Error(responseData.message || `HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ console.log('✅ API CLIENT: Chef event request successful');
+ return responseData;
+ } catch (error) {
+ console.error('💥 API CLIENT: Error in createChefEventRequest:', error);
+
+ // Re-throw ChefEventError as-is
+ if (error && typeof error === 'object' && 'errors' in error) {
+ console.log('💥 API CLIENT: Re-throwing validation error');
+ throw error;
+ }
+
+ // Wrap other errors
+ const wrappedError = new Error(
+ error instanceof Error
+ ? `Failed to create chef event request: ${error.message}`
+ : 'Failed to create chef event request: Unknown error'
+ );
+ console.error('💥 API CLIENT: Wrapped error:', wrappedError.message);
+ throw wrappedError;
+ }
+};
+
+// Validate event request data before submission
+export const validateEventRequest = (data: StoreCreateChefEventDTO): string[] => {
+ const errors: string[] = [];
+
+ if (!data.firstName?.trim()) {
+ errors.push('First name is required');
+ }
+
+ if (!data.lastName?.trim()) {
+ errors.push('Last name is required');
+ }
+
+ if (!data.email?.trim()) {
+ errors.push('Email is required');
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
+ errors.push('Invalid email format');
+ }
+
+ if (!data.requestedDate) {
+ errors.push('Event date is required');
+ } else {
+ const eventDate = new Date(data.requestedDate);
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ if (eventDate < today) {
+ errors.push('Event date cannot be in the past');
+ }
+ }
+
+ if (!data.requestedTime?.match(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/)) {
+ errors.push('Invalid time format (use HH:MM)');
+ }
+
+ if (!data.partySize || data.partySize < 2) {
+ errors.push('Minimum party size is 2');
+ } else if (data.partySize > 50) {
+ errors.push('Maximum party size is 50');
+ }
+
+ if (!data.eventType) {
+ errors.push('Event type is required');
+ }
+
+ if (!data.locationType) {
+ errors.push('Location type is required');
+ }
+
+ if (!data.locationAddress?.trim() || data.locationAddress.length < 10) {
+ errors.push('Address must be at least 10 characters');
+ }
+
+ return errors;
+};
\ No newline at end of file
diff --git a/apps/storefront/libs/util/server/data/event-products.server.ts b/apps/storefront/libs/util/server/data/event-products.server.ts
new file mode 100644
index 000000000..dfcacc4e1
--- /dev/null
+++ b/apps/storefront/libs/util/server/data/event-products.server.ts
@@ -0,0 +1,123 @@
+import { parseEventSku } from '@libs/util/products';
+import { StoreProduct } from '@medusajs/types';
+
+/**
+ * Gets the Medusa backend URL with fallback
+ */
+const getMedusaBackendUrl = () => {
+ // Try environment variable first
+ if (process.env.MEDUSA_BACKEND_URL) {
+ return process.env.MEDUSA_BACKEND_URL;
+ }
+
+ // Fallback to localhost for development
+ if (process.env.NODE_ENV === 'development') {
+ return 'http://localhost:9000';
+ }
+
+ // For production, this should be set
+ console.warn('MEDUSA_BACKEND_URL not set, using fallback');
+ return 'http://localhost:9000';
+};
+
+/**
+ * Gets the publishable API key for backend calls
+ */
+const getPublishableApiKey = () => {
+ // Try environment variable first
+ if (process.env.MEDUSA_PUBLISHABLE_API_KEY) {
+ return process.env.MEDUSA_PUBLISHABLE_API_KEY;
+ }
+
+ // Fallback to the key from the frontend
+ return 'pk_21e94c137732dd69790c80e8743c416795277bdb855f2db7595210ead34aa540';
+};
+
+/**
+ * Fetches chef event data for an event product
+ * @param product - The event product
+ * @returns Chef event data or null if not found
+ */
+export const fetchChefEventForProduct = async (product: StoreProduct) => {
+ // Debug logging
+ console.log('fetchChefEventForProduct called with product:', {
+ id: product.id,
+ title: product.title,
+ variants: product.variants?.map(v => ({
+ id: v.id,
+ sku: v.sku,
+ inventory_quantity: v.inventory_quantity
+ }))
+ });
+
+ // Extract event ID from product SKU
+ const eventVariant = product.variants?.find(variant =>
+ variant.sku?.startsWith('EVENT-')
+ );
+
+ console.log('Found event variant:', eventVariant);
+
+ if (!eventVariant?.sku) {
+ console.log('No event variant found');
+ return null;
+ }
+
+ const eventInfo = parseEventSku(eventVariant.sku);
+ console.log('Parsed event info:', eventInfo);
+
+ if (!eventInfo) {
+ console.log('Failed to parse event SKU');
+ return null;
+ }
+
+ try {
+ const backendUrl = getMedusaBackendUrl();
+ const publishableKey = getPublishableApiKey();
+ const url = `${backendUrl}/store/chef-events/${eventInfo.eventId}`;
+
+ console.log('Fetching chef event from URL:', url);
+
+ // Fetch chef event data from our backend with proper headers
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': publishableKey,
+ 'accept': 'application/json',
+ },
+ });
+
+ console.log('Chef event response status:', response.status);
+
+ if (!response.ok) {
+ console.warn('Failed to fetch chef event data:', response.status, response.statusText);
+
+ // Try to get more error details
+ try {
+ const errorText = await response.text();
+ console.warn('Error response body:', errorText);
+ } catch (e) {
+ console.warn('Could not read error response body');
+ }
+
+ return null;
+ }
+
+ const data = await response.json();
+ console.log('Chef event data received:', data);
+ return data.chefEvent;
+ } catch (error) {
+ console.error('Error fetching chef event data:', error);
+ return null;
+ }
+};
+
+/**
+ * Fetches menu data for an event product
+ * @param product - The event product
+ * @returns Menu data or null if not found
+ */
+export const fetchMenuForProduct = async (product: StoreProduct) => {
+ // Extract menu ID from product description or metadata
+ // For now, we'll return null as menu linking is not yet implemented
+ return null;
+};
\ No newline at end of file
diff --git a/apps/storefront/libs/util/server/data/menus.server.ts b/apps/storefront/libs/util/server/data/menus.server.ts
new file mode 100644
index 000000000..e7dbb5b40
--- /dev/null
+++ b/apps/storefront/libs/util/server/data/menus.server.ts
@@ -0,0 +1,131 @@
+import { cachified } from '@epic-web/cachified';
+import { sdkCache, baseMedusaConfig } from '../client.server';
+
+export interface StoreIngredientDTO {
+ id: string;
+ name: string;
+ optional?: boolean;
+}
+
+export interface StoreDishDTO {
+ id: string;
+ name: string;
+ description?: string;
+ ingredients: StoreIngredientDTO[];
+}
+
+export interface StoreCourseDTO {
+ id: string;
+ name: string;
+ dishes: StoreDishDTO[];
+}
+
+export interface StoreMenuDTO {
+ id: string;
+ name: string;
+ courses: StoreCourseDTO[];
+ images?: { id: string; url: string; rank: number }[];
+ thumbnail?: string | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface StoreMenusResponse {
+ menus: StoreMenuDTO[];
+ count: number;
+ offset: number;
+ limit: number;
+}
+
+export interface StoreMenuResponse {
+ menu: StoreMenuDTO;
+}
+
+// Fetch all available menus with optional search and pagination
+export const fetchMenus = async ({
+ limit = 20,
+ offset = 0,
+ q,
+ bypassCache = process.env.NODE_ENV !== 'production',
+}: {
+ limit?: number;
+ offset?: number;
+ q?: string;
+ bypassCache?: boolean;
+} = {}): Promise => {
+ const params = new URLSearchParams();
+ params.append('limit', limit.toString());
+ params.append('offset', offset.toString());
+ if (q) params.append('q', q);
+ if (bypassCache) params.append('_ts', Date.now().toString());
+
+ const fetcher = async () => {
+ const response = await fetch(`${baseMedusaConfig.baseUrl}/store/menus?${params}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': baseMedusaConfig.publishableKey || '',
+ ...(bypassCache ? { 'Cache-Control': 'no-cache' } : {}),
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch menus: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ if (bypassCache) {
+ return fetcher();
+ }
+
+ const cacheKey = `menus-${JSON.stringify({ limit, offset, q })}`;
+ return cachified({
+ key: cacheKey,
+ cache: sdkCache,
+ ttl: 1000 * 60 * 30,
+ getFreshValue: fetcher,
+ });
+};
+
+// Fetch a specific menu by ID with full details
+export const fetchMenuById = async (id: string, bypassCache = process.env.NODE_ENV !== 'production'): Promise => {
+ const fetcher = async () => {
+ const response = await fetch(`${baseMedusaConfig.baseUrl}/store/menus/${id}${bypassCache ? `?_ts=${Date.now()}` : ''}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'x-publishable-api-key': baseMedusaConfig.publishableKey || '',
+ ...(bypassCache ? { 'Cache-Control': 'no-cache' } : {}),
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ throw new Error(`Menu not found: ${id}`);
+ }
+ throw new Error(`Failed to fetch menu: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ if (bypassCache) {
+ return fetcher();
+ }
+
+ const cacheKey = `menu-${id}`;
+ return cachified({
+ key: cacheKey,
+ cache: sdkCache,
+ ttl: 1000 * 60 * 30,
+ getFreshValue: fetcher,
+ });
+};
+
+// Get featured menus (first 6 menus for homepage)
+export const getFeaturedMenus = async (): Promise => {
+ const response = await fetchMenus({ limit: 6, offset: 0 });
+ return response.menus;
+};
\ No newline at end of file
diff --git a/apps/storefront/libs/util/server/root.server.ts b/apps/storefront/libs/util/server/root.server.ts
index 1e231006f..11f126605 100644
--- a/apps/storefront/libs/util/server/root.server.ts
+++ b/apps/storefront/libs/util/server/root.server.ts
@@ -60,7 +60,7 @@ export const getRootLoader = async ({ request }: LoaderFunctionArgs) => {
region,
siteDetails: {
store: {
- name: 'BARRIO',
+ name: 'Chef Velez',
},
settings: siteSettings,
headerNavigationItems,
diff --git a/apps/storefront/package.json b/apps/storefront/package.json
index e03ae1d31..732381182 100644
--- a/apps/storefront/package.json
+++ b/apps/storefront/package.json
@@ -112,8 +112,8 @@
"lodash.pick": "^4.4.0",
"lru-cache": "^7.14.1",
"qs": "^6.11.2",
- "react": "19.0.0",
- "react-dom": "19.0.0",
+ "react": "19.1.0",
+ "react-dom": "19.1.0",
"react-drag-drop-files": "2.3.10",
"react-photo-album": "^2.3.0",
"react-player": "^2.12.0",
diff --git a/apps/storefront/public/assets/images/chef-hero.png b/apps/storefront/public/assets/images/chef-hero.png
new file mode 100644
index 000000000..a577f5a7d
Binary files /dev/null and b/apps/storefront/public/assets/images/chef-hero.png differ
diff --git a/apps/storefront/public/assets/images/chef-luis-velez.png b/apps/storefront/public/assets/images/chef-luis-velez.png
new file mode 100644
index 000000000..d8110c535
Binary files /dev/null and b/apps/storefront/public/assets/images/chef-luis-velez.png differ
diff --git a/apps/storefront/public/assets/images/chef_beef_menu.JPG b/apps/storefront/public/assets/images/chef_beef_menu.JPG
new file mode 100644
index 000000000..0bc399ed9
Binary files /dev/null and b/apps/storefront/public/assets/images/chef_beef_menu.JPG differ
diff --git a/apps/storefront/public/assets/images/chef_book_experience.PNG b/apps/storefront/public/assets/images/chef_book_experience.PNG
new file mode 100644
index 000000000..d27027774
Binary files /dev/null and b/apps/storefront/public/assets/images/chef_book_experience.PNG differ
diff --git a/apps/storefront/public/assets/images/chef_experience.PNG b/apps/storefront/public/assets/images/chef_experience.PNG
new file mode 100644
index 000000000..073606b3a
Binary files /dev/null and b/apps/storefront/public/assets/images/chef_experience.PNG differ
diff --git a/apps/storefront/public/assets/images/chef_scallops_home.PNG b/apps/storefront/public/assets/images/chef_scallops_home.PNG
new file mode 100644
index 000000000..b43cc7438
Binary files /dev/null and b/apps/storefront/public/assets/images/chef_scallops_home.PNG differ
diff --git a/apps/storefront/public/assets/images/chef_watermelon_home.jpg b/apps/storefront/public/assets/images/chef_watermelon_home.jpg
new file mode 100644
index 000000000..8d47eaeea
Binary files /dev/null and b/apps/storefront/public/assets/images/chef_watermelon_home.jpg differ
diff --git a/apps/storefront/types/chef-events.ts b/apps/storefront/types/chef-events.ts
new file mode 100644
index 000000000..9a1657e49
--- /dev/null
+++ b/apps/storefront/types/chef-events.ts
@@ -0,0 +1,162 @@
+// Import chef event types from the data layer for consistency
+import type {
+ StoreChefEventDTO,
+ StoreCreateChefEventDTO,
+ StoreChefEventResponse,
+ ChefEventError,
+ PRICING_STRUCTURE,
+} from '@libs/util/server/data/chef-events.server';
+
+// Re-export for external use
+export type {
+ StoreChefEventDTO,
+ StoreCreateChefEventDTO,
+ StoreChefEventResponse,
+ ChefEventError,
+};
+
+// Export pricing structure
+export { PRICING_STRUCTURE } from '@libs/util/server/data/chef-events.server';
+
+// Event type information for UI display
+export interface EventTypeInfo {
+ id: keyof typeof PRICING_STRUCTURE;
+ name: string;
+ description: string;
+ price: number;
+ duration: string;
+ features: string[];
+ idealFor: string[];
+}
+
+// Form step types for multi-step event request form
+export type EventRequestStep =
+ | 'menu-selection'
+ | 'event-type'
+ | 'date-time'
+ | 'party-size'
+ | 'location'
+ | 'contact-details'
+ | 'special-requests'
+ | 'review';
+
+export interface EventRequestFormData extends StoreCreateChefEventDTO {
+ // Additional form-specific fields that don't go to the API
+ agreedToTerms?: boolean;
+ marketingConsent?: boolean;
+ estimatedTotal?: number;
+}
+
+export interface EventRequestFormState {
+ currentStep: EventRequestStep;
+ data: Partial;
+ errors: Record;
+ isSubmitting: boolean;
+ isValid: boolean;
+}
+
+// Step validation schemas
+export interface StepValidation {
+ step: EventRequestStep;
+ validate: (data: Partial) => string[];
+ isRequired: boolean;
+}
+
+// Event type selection props
+export interface EventTypeCardProps {
+ eventType: EventTypeInfo;
+ selected?: boolean;
+ onSelect?: (eventType: keyof typeof PRICING_STRUCTURE) => void;
+ partySize?: number;
+ showPricing?: boolean;
+}
+
+// Date and time selection props
+export interface DateTimeFormProps {
+ selectedDate?: string;
+ selectedTime?: string;
+ onDateChange?: (date: string) => void;
+ onTimeChange?: (time: string) => void;
+ minDate?: string;
+ errors?: string[];
+}
+
+// Party size selector props
+export interface PartySizeSelectorProps {
+ value?: number;
+ onChange?: (size: number) => void;
+ min?: number;
+ max?: number;
+ eventType?: keyof typeof PRICING_STRUCTURE;
+ showPricing?: boolean;
+}
+
+// Location form props
+export interface LocationFormProps {
+ locationType?: 'customer_location' | 'chef_location';
+ locationAddress?: string;
+ onLocationTypeChange?: (type: 'customer_location' | 'chef_location') => void;
+ onAddressChange?: (address: string) => void;
+ errors?: string[];
+}
+
+// Contact details form props
+export interface ContactDetailsFormProps {
+ firstName?: string;
+ lastName?: string;
+ email?: string;
+ phone?: string;
+ onFieldChange?: (field: string, value: string) => void;
+ errors?: Record;
+}
+
+// Special requests form props
+export interface SpecialRequestsFormProps {
+ notes?: string;
+ specialRequirements?: string;
+ onNotesChange?: (notes: string) => void;
+ onSpecialRequirementsChange?: (requirements: string) => void;
+}
+
+// Review and summary props
+export interface EventRequestSummaryProps {
+ data: EventRequestFormData;
+ eventTypeInfo?: EventTypeInfo;
+ estimatedTotal?: number;
+ onEdit?: (step: EventRequestStep) => void;
+ onSubmit?: () => void;
+ isSubmitting?: boolean;
+}
+
+// Success page props
+export interface EventRequestSuccessProps {
+ chefEvent: StoreChefEventDTO;
+ message: string;
+}
+
+// Pricing calculation utilities
+export interface PricingBreakdown {
+ eventType: keyof typeof PRICING_STRUCTURE;
+ pricePerPerson: number;
+ partySize: number;
+ subtotal: number;
+ tax?: number;
+ total: number;
+ currency: string;
+}
+
+// Event request status for tracking
+export type EventRequestStatus = 'draft' | 'submitting' | 'submitted' | 'error';
+
+// Form navigation utilities
+export interface FormNavigation {
+ currentStep: EventRequestStep;
+ steps: EventRequestStep[];
+ canGoNext: boolean;
+ canGoPrevious: boolean;
+ isFirstStep: boolean;
+ isLastStep: boolean;
+ goToStep: (step: EventRequestStep) => void;
+ goToNext: () => void;
+ goToPrevious: () => void;
+}
\ No newline at end of file
diff --git a/apps/storefront/types/menus.ts b/apps/storefront/types/menus.ts
new file mode 100644
index 000000000..e22cf987c
--- /dev/null
+++ b/apps/storefront/types/menus.ts
@@ -0,0 +1,84 @@
+// Import menu types from the data layer for consistency
+import type {
+ StoreIngredientDTO,
+ StoreDishDTO,
+ StoreCourseDTO,
+ StoreMenuDTO,
+ StoreMenusResponse,
+ StoreMenuResponse,
+} from '@libs/util/server/data/menus.server';
+
+// Re-export for external use
+export type {
+ StoreIngredientDTO,
+ StoreDishDTO,
+ StoreCourseDTO,
+ StoreMenuDTO,
+ StoreMenusResponse,
+ StoreMenuResponse,
+};
+
+// Additional UI-specific types for menu display
+export interface MenuCardProps {
+ menu: StoreMenuDTO;
+ className?: string;
+ showCourseCount?: boolean;
+ showDescription?: boolean;
+ onMenuSelect?: (menuId: string) => void;
+}
+
+export interface CourseDisplayProps {
+ course: StoreCourseDTO;
+ expanded?: boolean;
+ onToggleExpanded?: () => void;
+}
+
+export interface DishCardProps {
+ dish: StoreDishDTO;
+ showIngredients?: boolean;
+ className?: string;
+}
+
+export interface IngredientTagProps {
+ ingredient: StoreIngredientDTO;
+ showOptional?: boolean;
+ size?: 'sm' | 'md' | 'lg';
+}
+
+// Menu filtering and search types
+export interface MenuFilters {
+ search?: string;
+ courseCount?: {
+ min?: number;
+ max?: number;
+ };
+ dishCount?: {
+ min?: number;
+ max?: number;
+ };
+}
+
+export interface MenuSearchParams {
+ q?: string;
+ limit?: number;
+ offset?: number;
+}
+
+// Menu statistics for UI
+export interface MenuStats {
+ totalCourses: number;
+ totalDishes: number;
+ totalIngredients: number;
+ optionalIngredients: number;
+ requiredIngredients: number;
+}
+
+// Helper function type for calculating menu stats
+export type CalculateMenuStats = (menu: StoreMenuDTO) => MenuStats;
+
+// Menu breadcrumb type for navigation
+export interface MenuBreadcrumb {
+ label: string;
+ href?: string;
+ current?: boolean;
+}
\ No newline at end of file
diff --git a/apps/storefront/vite.config.ts b/apps/storefront/vite.config.ts
index 6b04a68f9..22ac73527 100644
--- a/apps/storefront/vite.config.ts
+++ b/apps/storefront/vite.config.ts
@@ -12,7 +12,12 @@ export default defineConfig({
},
},
ssr: {
- noExternal: ['@medusajs/js-sdk', '@lambdacurry/medusa-plugins-sdk'],
+ noExternal: [
+ '@medusajs/js-sdk',
+ '@lambdacurry/medusa-plugins-sdk',
+ '@medusajs/ui',
+ '@medusajs/icons',
+ ],
},
plugins: [reactRouter(), tsconfigPaths({ root: './' }), vanillaExtractPlugin()],
build: {},
diff --git a/docs/chef-event-email-management-implementation-plan.md b/docs/chef-event-email-management-implementation-plan.md
new file mode 100644
index 000000000..587811827
--- /dev/null
+++ b/docs/chef-event-email-management-implementation-plan.md
@@ -0,0 +1,1132 @@
+# Chef Event Email Management Implementation Plan
+
+## Overview
+
+This implementation plan adds advanced email management features to the chef event system, allowing chefs to control email notifications and resend event details to customers or additional recipients.
+
+## Business Requirements
+
+### Current State Analysis
+
+**✅ Already Implemented:**
+- Chef event lifecycle management (pending → confirmed/cancelled)
+- Basic email notifications on event acceptance/rejection
+- Admin interface for event management
+- Email templates for customer notifications
+- Resend email service integration
+
+**🎯 New Requirements:**
+1. **Email Opt-in Control**: Checkbox for chef to control whether acceptance emails are sent
+2. **Event Detail Resend**: Tools to resend event details to host or custom email addresses
+3. **Email Management Interface**: Enhanced admin interface for email operations
+4. **Email History Tracking**: Track sent emails and their status
+
+## Implementation Phases
+
+---
+
+## Phase 1: Database Schema Updates ✅ PLANNED
+
+### 1.1 Chef Event Model Extensions
+
+**File: `apps/medusa/src/modules/chef-event/models/chef-event.ts`**
+
+Add new fields to track email preferences and history:
+
+```typescript
+export const ChefEvent = model.define("chef_event", {
+ // ... existing fields ...
+
+ // Email management fields
+ sendAcceptanceEmail: model.boolean().default(true), // Chef preference for sending acceptance emails
+ emailHistory: model.json().nullable(), // Track sent emails with timestamps and recipients
+ lastEmailSentAt: model.dateTime().nullable(), // Last email activity timestamp
+ customEmailRecipients: model.json().nullable(), // Additional email recipients for resends
+})
+```
+
+### 1.2 Database Migration
+
+**File: `apps/medusa/src/modules/chef-event/migrations/add-email-management-fields.ts`**
+
+```typescript
+import { Migration } from "@mikro-orm/migrations"
+
+export class AddEmailManagementFields20241201000000 extends Migration {
+ async up(): Promise {
+ this.addSql(`
+ ALTER TABLE "chef_event"
+ ADD COLUMN "send_acceptance_email" boolean DEFAULT true,
+ ADD COLUMN "email_history" jsonb,
+ ADD COLUMN "last_email_sent_at" timestamptz,
+ ADD COLUMN "custom_email_recipients" jsonb;
+ `)
+ }
+
+ async down(): Promise {
+ this.addSql(`
+ ALTER TABLE "chef_event"
+ DROP COLUMN "send_acceptance_email",
+ DROP COLUMN "email_history",
+ DROP COLUMN "last_email_sent_at",
+ DROP COLUMN "custom_email_recipients";
+ `)
+ }
+}
+```
+
+---
+
+## Phase 2: Enhanced Admin Interface ✅ PLANNED
+
+### 2.1 Updated Accept Event Modal
+
+**File: `apps/medusa/src/admin/routes/chef-events/[id]/page.tsx`**
+
+Enhance the acceptance modal to include email control:
+
+```typescript
+// Add state for email preferences
+const [sendAcceptanceEmail, setSendAcceptanceEmail] = useState(true)
+
+// Updated Accept Event Modal
+{showAcceptModal && (
+
+
+
+ Accept Event
+
+
+
+
This will accept the event and create a product for ticket sales.
+
+ {/* Email Notification Control */}
+
+
+
+ Send acceptance email to customer
+
+
+
+
+ Chef Notes (Optional)
+
+
+ {/* Action Buttons */}
+
+ setShowAcceptModal(false)}>
+ Cancel
+
+
+ {acceptChefEvent.isPending ? "Accepting..." : "Accept Event"}
+
+
+
+
+
+
+)}
+```
+
+### 2.2 Email Management Section
+
+Add a new section to the event detail page for email operations:
+
+```typescript
+// Add after MenuDetails component
+{isConfirmed && (
+ {
+ // Refresh event data to show updated email history
+ refetch()
+ }}
+ />
+)}
+```
+
+### 2.3 Email Management Component
+
+**File: `apps/medusa/src/admin/routes/chef-events/components/EmailManagementSection.tsx`**
+
+```typescript
+import { useState } from "react"
+import { Card, Button, Label, Input, Textarea, Badge, FocusModal } from "@medusajs/ui"
+import { useAdminResendEventEmailMutation } from "../../../hooks/chef-events"
+
+interface EmailManagementSectionProps {
+ chefEvent: any
+ onEmailSent: (emailData: any) => void
+}
+
+export const EmailManagementSection = ({ chefEvent, onEmailSent }: EmailManagementSectionProps) => {
+ const [showResendModal, setShowResendModal] = useState(false)
+ const [customEmails, setCustomEmails] = useState("")
+ const [emailNotes, setEmailNotes] = useState("")
+ const [emailType, setEmailType] = useState<"host" | "custom">("host")
+
+ const resendEmail = useAdminResendEventEmailMutation()
+
+ const handleResendEmail = async () => {
+ try {
+ const recipients = emailType === "host"
+ ? [chefEvent.email]
+ : customEmails.split(",").map(email => email.trim()).filter(Boolean)
+
+ await resendEmail.mutateAsync({
+ chefEventId: chefEvent.id,
+ recipients,
+ notes: emailNotes,
+ emailType: "event_details_resend"
+ })
+
+ toast.success("Email Sent", {
+ description: `Event details sent to ${recipients.length} recipient(s)`,
+ duration: 3000,
+ })
+
+ setShowResendModal(false)
+ setCustomEmails("")
+ setEmailNotes("")
+ onEmailSent({ recipients, sentAt: new Date() })
+
+ } catch (error) {
+ toast.error("Email Failed", {
+ description: "Failed to send email. Please try again.",
+ duration: 5000,
+ })
+ }
+ }
+
+ return (
+
+
+
+
Email Management
+ setShowResendModal(true)}
+ >
+ Resend Event Details
+
+
+
+ {/* Email History */}
+ {chefEvent.emailHistory && chefEvent.emailHistory.length > 0 && (
+
+
Recent Email Activity
+
+ {chefEvent.emailHistory.slice(-3).map((email: any, index: number) => (
+
+
+ {email.type}
+
+ to {email.recipients.join(", ")}
+
+
+
+ {new Date(email.sentAt).toLocaleDateString()}
+
+
+ ))}
+
+
+ )}
+
+ {/* Resend Modal */}
+ {showResendModal && (
+
+
+
+ Resend Event Details
+
+
+
+
Send event details and confirmation to recipients.
+
+ {/* Recipient Selection */}
+
+
+ {/* Custom Email Input */}
+ {emailType === "custom" && (
+
+
Email Addresses
+
setCustomEmails(e.target.value)}
+ />
+
+ Separate multiple emails with commas
+
+
+ )}
+
+ {/* Additional Notes */}
+
+ Additional Notes (Optional)
+
+
+ {/* Action Buttons */}
+
+ setShowResendModal(false)}>
+ Cancel
+
+
+ {resendEmail.isPending ? "Sending..." : "Send Email"}
+
+
+
+
+
+
+ )}
+
+
+ )
+}
+```
+
+---
+
+## Phase 3: Backend API Enhancements ✅ PLANNED
+
+### 3.1 Updated Accept Event API
+
+**File: `apps/medusa/src/api/admin/chef-events/[id]/accept/route.ts`**
+
+Update to handle email preferences:
+
+```typescript
+const acceptChefEventSchema = z.object({
+ chefNotes: z.string().optional(),
+ acceptedBy: z.string().optional(),
+ sendAcceptanceEmail: z.boolean().default(true) // New field
+})
+
+export async function POST(req: MedusaRequest, res: MedusaResponse) {
+ const { id } = req.params
+ const validatedBody = acceptChefEventSchema.parse(req.body)
+
+ try {
+ const { result } = await acceptChefEventWorkflow(req.scope).run({
+ input: {
+ chefEventId: id,
+ chefNotes: validatedBody.chefNotes,
+ acceptedBy: validatedBody.acceptedBy || 'chef',
+ sendAcceptanceEmail: validatedBody.sendAcceptanceEmail
+ }
+ })
+
+ res.status(200).json({
+ success: true,
+ data: result
+ })
+ } catch (error) {
+ // ... error handling
+ }
+}
+```
+
+### 3.2 New Resend Email API
+
+**File: `apps/medusa/src/api/admin/chef-events/[id]/resend-email/route.ts`**
+
+```typescript
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { z } from "zod"
+import { resendEventEmailWorkflow } from "../../../../../workflows/resend-event-email"
+
+const resendEmailSchema = z.object({
+ recipients: z.array(z.string().email()),
+ notes: z.string().optional(),
+ emailType: z.enum(["event_details_resend", "custom_message"]).default("event_details_resend")
+})
+
+export async function POST(req: MedusaRequest, res: MedusaResponse) {
+ const { id } = req.params
+ const validatedBody = resendEmailSchema.parse(req.body)
+
+ try {
+ const { result } = await resendEventEmailWorkflow(req.scope).run({
+ input: {
+ chefEventId: id,
+ recipients: validatedBody.recipients,
+ notes: validatedBody.notes,
+ emailType: validatedBody.emailType
+ }
+ })
+
+ res.status(200).json({
+ success: true,
+ data: result
+ })
+ } catch (error) {
+ console.error("Error resending event email:", error)
+ res.status(500).json({
+ success: false,
+ message: "Failed to resend event email",
+ error: error instanceof Error ? error.message : "Unknown error"
+ })
+ }
+}
+```
+
+---
+
+## Phase 4: Workflow Updates ✅ PLANNED
+
+### 4.1 Enhanced Accept Chef Event Workflow
+
+**File: `apps/medusa/src/workflows/accept-chef-event.ts`**
+
+Update to handle email preferences:
+
+```typescript
+type AcceptChefEventWorkflowInput = {
+ chefEventId: string
+ chefNotes?: string
+ acceptedBy?: string
+ sendAcceptanceEmail?: boolean // New field
+}
+
+const updateChefEventStep = createStep(
+ "update-chef-event-step",
+ async (input: AcceptChefEventWorkflowInput, { container }) => {
+ // ... existing logic ...
+
+ // Update chef event with email preference
+ const updatedChefEvent = await chefEventModuleService.updateChefEvents({
+ id: input.chefEventId,
+ status: 'confirmed',
+ acceptedAt: new Date(),
+ acceptedBy: input.acceptedBy || 'chef',
+ chefNotes: input.chefNotes,
+ sendAcceptanceEmail: input.sendAcceptanceEmail ?? true
+ })
+
+ return new StepResponse(updatedChefEvent)
+ }
+)
+
+const conditionalEmitEventStep = createStep(
+ "conditional-emit-event-step",
+ async (input: { chefEvent: any; sendEmail: boolean }, { container }) => {
+ // Only emit event if email should be sent
+ if (input.sendEmail) {
+ const eventBusModuleService = container.resolve(Modules.EVENT_BUS)
+ await eventBusModuleService.emit("chef-event.accepted", {
+ chefEventId: input.chefEvent.id
+ })
+ }
+
+ return new StepResponse({ emailSent: input.sendEmail })
+ }
+)
+
+export const acceptChefEventWorkflow = createWorkflow(
+ "accept-chef-event-workflow",
+ function (input: AcceptChefEventWorkflowInput) {
+ const updatedChefEvent = updateChefEventStep(input)
+ const productCreated = createEventProductStep(updatedChefEvent)
+ const emailResult = conditionalEmitEventStep({
+ chefEvent: updatedChefEvent,
+ sendEmail: input.sendAcceptanceEmail ?? true
+ })
+
+ return new WorkflowResponse({
+ chefEvent: updatedChefEvent,
+ product: productCreated,
+ emailSent: emailResult
+ })
+ }
+)
+```
+
+### 4.2 New Resend Event Email Workflow
+
+**File: `apps/medusa/src/workflows/resend-event-email.ts`**
+
+```typescript
+import {
+ createStep,
+ createWorkflow,
+ StepResponse,
+ WorkflowResponse
+} from "@medusajs/workflows-sdk"
+import { CHEF_EVENT_MODULE } from "../modules/chef-event"
+import { Modules } from "@medusajs/framework/utils"
+
+type ResendEventEmailWorkflowInput = {
+ chefEventId: string
+ recipients: string[]
+ notes?: string
+ emailType: "event_details_resend" | "custom_message"
+}
+
+const updateEmailHistoryStep = createStep(
+ "update-email-history-step",
+ async (input: ResendEventEmailWorkflowInput, { container }) => {
+ const chefEventModuleService = container.resolve(CHEF_EVENT_MODULE)
+
+ // Get current chef event
+ const chefEvent = await chefEventModuleService.retrieveChefEvent(input.chefEventId)
+
+ // Update email history
+ const currentHistory = chefEvent.emailHistory || []
+ const newEmailEntry = {
+ type: input.emailType,
+ recipients: input.recipients,
+ notes: input.notes,
+ sentAt: new Date().toISOString(),
+ sentBy: "chef_admin" // Could be dynamic based on user
+ }
+
+ const updatedChefEvent = await chefEventModuleService.updateChefEvents({
+ id: input.chefEventId,
+ emailHistory: [...currentHistory, newEmailEntry],
+ lastEmailSentAt: new Date()
+ })
+
+ return new StepResponse(updatedChefEvent)
+ }
+)
+
+const emitResendEmailEventStep = createStep(
+ "emit-resend-email-event-step",
+ async (input: ResendEventEmailWorkflowInput, { container }) => {
+ const eventBusModuleService = container.resolve(Modules.EVENT_BUS)
+
+ await eventBusModuleService.emit("chef-event.email-resend", {
+ chefEventId: input.chefEventId,
+ recipients: input.recipients,
+ notes: input.notes,
+ emailType: input.emailType
+ })
+
+ return new StepResponse({ emailEventEmitted: true })
+ }
+)
+
+export const resendEventEmailWorkflow = createWorkflow(
+ "resend-event-email-workflow",
+ function (input: ResendEventEmailWorkflowInput) {
+ const updatedChefEvent = updateEmailHistoryStep(input)
+ const emailEvent = emitResendEmailEventStep(input)
+
+ return new WorkflowResponse({
+ chefEvent: updatedChefEvent,
+ emailSent: emailEvent
+ })
+ }
+)
+```
+
+---
+
+## Phase 5: Email Templates & Subscribers ✅ PLANNED
+
+### 5.1 New Event Details Resend Template
+
+**File: `apps/medusa/src/modules/resend/emails/event-details-resend.tsx`**
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Button,
+} from "@react-email/components"
+
+type EventDetailsResendEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ event: {
+ status: string
+ total_price: string
+ price_per_person: string
+ }
+ product: {
+ id: string
+ handle: string
+ title: string
+ purchase_url: string
+ }
+ chef: {
+ name: string
+ email: string
+ phone: string
+ }
+ requestReference: string
+ customNotes?: string
+ emailType: "event_details_resend"
+}
+
+function EventDetailsResendEmailComponent({
+ customer,
+ booking,
+ event,
+ product,
+ chef,
+ requestReference,
+ customNotes
+}: EventDetailsResendEmailProps) {
+
+ return (
+
+
+
+ Your chef event details and confirmation
+
+ {/* Header */}
+
+
+
+
+
+ 📧 Event Details Reminder
+
+
+ Your confirmed chef event information
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ Hi {customer.first_name}!
+
+
+ Here are your confirmed event details. We're looking forward to creating an amazing culinary experience for you!
+
+
+ {/* Custom Notes from Chef */}
+ {customNotes && (
+
+
+ Message from Chef Elena
+
+
+ "{customNotes}"
+
+
+ )}
+
+ {/* Event Details - Same as acceptance email */}
+
+
+ Your Event Details
+
+
+ {/* Event details rows - same structure as acceptance email */}
+ {/* ... */}
+
+
+ {/* Payment Link (if not fully paid) */}
+
+
+ Event Access
+
+
+ Access your event details and purchase additional tickets if needed.
+
+
+
+
+
+ View Event Details
+
+
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+
+
+ {/* Footer - Same as other templates */}
+ {/* ... */}
+
+
+
+ )
+}
+
+export const eventDetailsResendEmail = (props: EventDetailsResendEmailProps) => (
+
+)
+```
+
+### 5.2 New Email Resend Subscriber
+
+**File: `apps/medusa/src/subscribers/chef-event-email-resend.ts`**
+
+```typescript
+import type { SubscriberArgs, SubscriberConfig } from "@medusajs/framework"
+import { CHEF_EVENT_MODULE } from "../modules/chef-event"
+import { Modules } from "@medusajs/framework/utils"
+import { DateTime } from "luxon"
+
+type EventData = {
+ chefEventId: string
+ recipients: string[]
+ notes?: string
+ emailType: "event_details_resend" | "custom_message"
+}
+
+export default async function chefEventEmailResendHandler({
+ event: { data },
+ container,
+}: SubscriberArgs) {
+ console.log("🔄 CHEF EVENT EMAIL RESEND SUBSCRIBER: Processing resend request:", data)
+
+ try {
+ const chefEventModuleService = container.resolve(CHEF_EVENT_MODULE)
+ const notificationService = container.resolve(Modules.NOTIFICATION)
+
+ // Get chef event details
+ const chefEvent = await chefEventModuleService.retrieveChefEvent(data.chefEventId)
+
+ if (!chefEvent) {
+ throw new Error(`Chef event not found: ${data.chefEventId}`)
+ }
+
+ // Get product details if event is confirmed
+ let product = null
+ if (chefEvent.productId) {
+ const productModuleService = container.resolve(Modules.PRODUCT)
+ product = await productModuleService.retrieveProduct(chefEvent.productId)
+ }
+
+ // Format data for email template
+ const formattedDate = DateTime.fromJSDate(chefEvent.requestedDate).toFormat('LLL d, yyyy')
+ const formattedTime = chefEvent.requestedTime
+
+ const eventTypeMap: Record = {
+ cooking_class: "Cooking Class",
+ plated_dinner: "Plated Dinner",
+ buffet_style: "Buffet Style"
+ }
+
+ const locationTypeMap: Record = {
+ customer_location: "at Customer's Location",
+ chef_location: "at Chef's Location"
+ }
+
+ // Calculate pricing
+ const PRICING_STRUCTURE = {
+ buffet_style: 99.99,
+ cooking_class: 119.99,
+ plated_dinner: 149.99
+ }
+
+ const pricePerPerson = PRICING_STRUCTURE[chefEvent.eventType as keyof typeof PRICING_STRUCTURE]
+ const totalPrice = pricePerPerson * chefEvent.partySize
+
+ // Common email data
+ const emailData = {
+ customer: {
+ first_name: chefEvent.firstName,
+ last_name: chefEvent.lastName,
+ email: chefEvent.email,
+ phone: chefEvent.phone || "Not provided"
+ },
+ booking: {
+ date: formattedDate,
+ time: formattedTime,
+ event_type: eventTypeMap[chefEvent.eventType] || chefEvent.eventType,
+ location_type: locationTypeMap[chefEvent.locationType] || chefEvent.locationType,
+ location_address: chefEvent.locationAddress || "Not provided",
+ party_size: chefEvent.partySize,
+ notes: chefEvent.notes || "No special notes provided"
+ },
+ event: {
+ status: chefEvent.status,
+ total_price: totalPrice.toFixed(2),
+ price_per_person: pricePerPerson.toFixed(2)
+ },
+ product: product ? {
+ id: product.id,
+ handle: product.handle,
+ title: product.title,
+ purchase_url: `${process.env.STOREFRONT_URL}/products/${product.handle}`
+ } : null,
+ chef: {
+ name: "Chef Elena Rodriguez",
+ email: "hello@chefelenar.com",
+ phone: "(555) 123-4567"
+ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ customNotes: data.notes,
+ emailType: data.emailType
+ }
+
+ // Send emails to all recipients
+ for (const recipient of data.recipients) {
+ await notificationService.createNotifications({
+ to: recipient,
+ channel: "email",
+ template: "event-details-resend",
+ data: emailData
+ })
+
+ console.log(`✅ CHEF EVENT EMAIL RESEND SUBSCRIBER: Email sent to ${recipient}`)
+ }
+
+ } catch (error) {
+ console.error("❌ CHEF EVENT EMAIL RESEND SUBSCRIBER: Failed to process resend:", error)
+ throw error
+ }
+}
+
+export const config: SubscriberConfig = {
+ event: "chef-event.email-resend",
+}
+```
+
+---
+
+## Phase 6: SDK & Hooks Updates ✅ PLANNED
+
+### 6.1 Updated Admin SDK
+
+**File: `apps/medusa/src/sdk/admin/admin-chef-events.ts`**
+
+Add new methods for email management:
+
+```typescript
+export interface AdminAcceptChefEventDTO {
+ chefNotes?: string
+ acceptedBy?: string
+ sendAcceptanceEmail?: boolean // New field
+}
+
+export interface AdminResendEventEmailDTO {
+ recipients: string[]
+ notes?: string
+ emailType?: "event_details_resend" | "custom_message"
+}
+
+export class AdminChefEventsResource {
+ // ... existing methods ...
+
+ /**
+ * Accept a chef event with email preferences
+ */
+ async accept(id: string, data: AdminAcceptChefEventDTO = {}) {
+ const response = await this.client.fetch<{ success: boolean; data: any }>(`/admin/chef-events/${id}/accept`, {
+ method: 'POST',
+ body: {
+ ...data,
+ sendAcceptanceEmail: data.sendAcceptanceEmail ?? true
+ },
+ })
+ return response
+ }
+
+ /**
+ * Resend event details to specified recipients
+ */
+ async resendEmail(id: string, data: AdminResendEventEmailDTO) {
+ const response = await this.client.fetch<{ success: boolean; data: any }>(`/admin/chef-events/${id}/resend-email`, {
+ method: 'POST',
+ body: data,
+ })
+ return response
+ }
+}
+```
+
+### 6.2 Updated Admin Hooks
+
+**File: `apps/medusa/src/admin/hooks/chef-events.ts`**
+
+Add new React Query hooks:
+
+```typescript
+/**
+ * Hook for resending event emails
+ */
+export const useAdminResendEventEmailMutation = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({ chefEventId, ...data }: { chefEventId: string } & AdminResendEventEmailDTO) => {
+ const sdk = getAdminSDK()
+ return await sdk.admin.chefEvents.resendEmail(chefEventId, data)
+ },
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["admin", "chef-events"] })
+ queryClient.invalidateQueries({ queryKey: ["admin", "chef-events", variables.chefEventId] })
+ },
+ })
+}
+
+/**
+ * Updated accept mutation to handle email preferences
+ */
+export const useAdminAcceptChefEventMutation = () => {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({ id, data }: { id: string; data: AdminAcceptChefEventDTO }) => {
+ const sdk = getAdminSDK()
+ return await sdk.admin.chefEvents.accept(id, data)
+ },
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["admin", "chef-events"] })
+ queryClient.invalidateQueries({ queryKey: ["admin", "chef-events", variables.id] })
+ },
+ })
+}
+```
+
+---
+
+## Phase 7: Testing & Validation ✅ PLANNED
+
+### 7.1 Unit Tests
+
+**File: `apps/medusa/src/workflows/__tests__/accept-chef-event.test.ts`**
+
+```typescript
+describe("Accept Chef Event Workflow", () => {
+ it("should accept event without sending email when disabled", async () => {
+ const input = {
+ chefEventId: "event_123",
+ chefNotes: "Test notes",
+ sendAcceptanceEmail: false
+ }
+
+ const result = await acceptChefEventWorkflow(container).run({ input })
+
+ expect(result.chefEvent.status).toBe("confirmed")
+ expect(result.emailSent).toBe(false)
+ })
+
+ it("should accept event and send email when enabled", async () => {
+ const input = {
+ chefEventId: "event_123",
+ chefNotes: "Test notes",
+ sendAcceptanceEmail: true
+ }
+
+ const result = await acceptChefEventWorkflow(container).run({ input })
+
+ expect(result.chefEvent.status).toBe("confirmed")
+ expect(result.emailSent).toBe(true)
+ })
+})
+```
+
+### 7.2 Integration Tests
+
+**File: `apps/medusa/src/api/__tests__/chef-events-email.test.ts`**
+
+```typescript
+describe("Chef Events Email API", () => {
+ it("should accept event with email preference", async () => {
+ const response = await api.post(`/admin/chef-events/${eventId}/accept`, {
+ chefNotes: "Looking forward to this event!",
+ sendAcceptanceEmail: false
+ })
+
+ expect(response.status).toBe(200)
+ expect(response.data.success).toBe(true)
+ })
+
+ it("should resend event details to custom recipients", async () => {
+ const response = await api.post(`/admin/chef-events/${eventId}/resend-email`, {
+ recipients: ["test1@example.com", "test2@example.com"],
+ notes: "Additional information for the event",
+ emailType: "event_details_resend"
+ })
+
+ expect(response.status).toBe(200)
+ expect(response.data.success).toBe(true)
+ })
+})
+```
+
+### 7.3 Component Tests
+
+**File: `apps/medusa/src/admin/routes/chef-events/components/__tests__/EmailManagementSection.test.tsx`**
+
+```typescript
+describe("EmailManagementSection", () => {
+ it("should render email history", () => {
+ const chefEvent = {
+ emailHistory: [
+ {
+ type: "event_details_resend",
+ recipients: ["test@example.com"],
+ sentAt: "2024-01-01T00:00:00Z"
+ }
+ ]
+ }
+
+ render( )
+
+ expect(screen.getByText("Recent Email Activity")).toBeInTheDocument()
+ expect(screen.getByText("event_details_resend")).toBeInTheDocument()
+ })
+
+ it("should open resend modal when button clicked", () => {
+ render( )
+
+ fireEvent.click(screen.getByText("Resend Event Details"))
+
+ expect(screen.getByText("Send to")).toBeInTheDocument()
+ })
+})
+```
+
+---
+
+## Implementation Timeline
+
+### Week 1: Database & Backend
+- [ ] Database migration for new fields
+- [ ] Update chef event model
+- [ ] Enhanced accept event API
+- [ ] New resend email API
+- [ ] Updated workflows
+
+### Week 2: Admin Interface
+- [ ] Enhanced accept event modal
+- [ ] Email management section component
+- [ ] Updated admin hooks
+- [ ] SDK updates
+
+### Week 3: Email System
+- [ ] New email templates
+- [ ] Email resend subscriber
+- [ ] Email history tracking
+- [ ] Template testing
+
+### Week 4: Testing & Polish
+- [ ] Unit tests
+- [ ] Integration tests
+- [ ] Component tests
+- [ ] End-to-end testing
+- [ ] Documentation updates
+
+## Success Criteria
+
+### Functional Requirements
+- ✅ Chef can opt-in/out of sending acceptance emails
+- ✅ Chef can resend event details to host email
+- ✅ Chef can send event details to custom email addresses
+- ✅ Email history is tracked and displayed
+- ✅ All existing functionality remains intact
+
+### Technical Requirements
+- ✅ Database schema properly migrated
+- ✅ API endpoints follow existing patterns
+- ✅ Email templates are responsive and professional
+- ✅ Admin interface is intuitive and consistent
+- ✅ Comprehensive test coverage
+
+### User Experience
+- ✅ Clear and intuitive email controls
+- ✅ Helpful feedback on email operations
+- ✅ Professional email templates
+- ✅ Consistent with existing admin interface
+
+## Future Enhancements
+
+### Phase 8+: Advanced Features
+- [ ] Email scheduling (send reminders at specific times)
+- [ ] Email templates customization
+- [ ] Bulk email operations
+- [ ] Email analytics and tracking
+- [ ] SMS notifications integration
+- [ ] Customer email preferences
+- [ ] Email automation rules
+
+This implementation plan provides a comprehensive approach to adding advanced email management features to the chef event system while maintaining consistency with the existing codebase architecture and patterns.
\ No newline at end of file
diff --git a/docs/chef-events-implementation-plan.md b/docs/chef-events-implementation-plan.md
new file mode 100644
index 000000000..63f6a9891
--- /dev/null
+++ b/docs/chef-events-implementation-plan.md
@@ -0,0 +1,652 @@
+# Chef Event Management Implementation Plan
+
+## Overview
+This implementation plan creates a comprehensive chef event management system that mirrors the existing menu management structure to maintain consistency across the admin interface.
+
+## Current State Analysis
+
+### ✅ Already Implemented
+- Chef event model with comprehensive fields
+- Basic chef event service
+- SDK with DTOs and API methods
+- Admin hooks with React Query
+- Links between products and chef events
+
+### ❌ Missing for Event Management
+- Workflows (create, update, delete)
+- API routes
+- Admin UI components (form, list, page)
+- Validation schemas
+
+## Implementation Requirements
+
+### User Experience Decisions
+- **Form Structure**: Tab-based approach similar to menus for consistency
+- **Field Management**: All fields editable through admin initially
+- **Validation Rules**:
+ - Events must be scheduled in the future
+ - Party size max 50
+ - Required field validation
+- **Status Management**: Simple updates with transition restrictions
+- **Product Integration**: Link events to specific menus
+- **Admin Routes**: Follow `/app/chef-events` pattern with detail pages
+
+## 1. Workflows (`src/workflows/`)
+
+Create three workflow files following the menu pattern:
+
+### `create-chef-event.ts`
+```typescript
+import {
+ createStep,
+ createWorkflow,
+ StepResponse,
+ WorkflowResponse
+} from "@medusajs/workflows-sdk"
+import { CHEF_EVENT_MODULE } from "../modules/chef-event"
+
+type CreateChefEventWorkflowInput = {
+ status: 'pending' | 'confirmed' | 'cancelled' | 'completed'
+ requestedDate: string
+ requestedTime: string
+ partySize: number
+ eventType: 'cooking_class' | 'plated_dinner' | 'buffet_style'
+ templateProductId?: string
+ locationType: 'customer_location' | 'chef_location'
+ locationAddress: string
+ firstName: string
+ lastName: string
+ email: string
+ phone?: string
+ notes?: string
+ totalPrice?: number
+ depositPaid?: boolean
+ specialRequirements?: string
+ estimatedDuration?: number
+}
+
+const createChefEventStep = createStep(
+ "create-chef-event-step",
+ async (input: CreateChefEventWorkflowInput, { container }: { container: any }) => {
+ const chefEventModuleService = container.resolve(CHEF_EVENT_MODULE)
+
+ const chefEvent = await chefEventModuleService.createChefEvents({
+ ...input,
+ requestedDate: new Date(input.requestedDate),
+ totalPrice: input.totalPrice || 0,
+ depositPaid: input.depositPaid || false
+ })
+
+ return new StepResponse(chefEvent)
+ }
+)
+
+export const createChefEventWorkflow = createWorkflow(
+ "create-chef-event-workflow",
+ function (input: CreateChefEventWorkflowInput) {
+ const chefEvent = createChefEventStep(input)
+
+ return new WorkflowResponse({
+ chefEvent
+ })
+ }
+)
+```
+
+### `update-chef-event.ts`
+```typescript
+import {
+ createStep,
+ createWorkflow,
+ StepResponse,
+ WorkflowResponse
+} from "@medusajs/workflows-sdk"
+import { CHEF_EVENT_MODULE } from "../modules/chef-event"
+
+type UpdateChefEventWorkflowInput = {
+ id: string
+ status?: 'pending' | 'confirmed' | 'cancelled' | 'completed'
+ requestedDate?: string
+ requestedTime?: string
+ partySize?: number
+ eventType?: 'cooking_class' | 'plated_dinner' | 'buffet_style'
+ templateProductId?: string
+ locationType?: 'customer_location' | 'chef_location'
+ locationAddress?: string
+ firstName?: string
+ lastName?: string
+ email?: string
+ phone?: string
+ notes?: string
+ totalPrice?: number
+ depositPaid?: boolean
+ specialRequirements?: string
+ estimatedDuration?: number
+}
+
+const updateChefEventStep = createStep(
+ "update-chef-event-step",
+ async (input: UpdateChefEventWorkflowInput, { container }: { container: any }) => {
+ const chefEventModuleService = container.resolve(CHEF_EVENT_MODULE)
+
+ const updateData = { ...input }
+ if (input.requestedDate) {
+ updateData.requestedDate = new Date(input.requestedDate)
+ }
+
+ const chefEvent = await chefEventModuleService.updateChefEvents(updateData)
+
+ return new StepResponse(chefEvent)
+ }
+)
+
+export const updateChefEventWorkflow = createWorkflow(
+ "update-chef-event-workflow",
+ function (input: UpdateChefEventWorkflowInput) {
+ const chefEvent = updateChefEventStep(input)
+
+ return new WorkflowResponse({
+ chefEvent
+ })
+ }
+)
+```
+
+### `delete-chef-event.ts`
+```typescript
+import {
+ createStep,
+ createWorkflow,
+ StepResponse,
+ WorkflowResponse
+} from "@medusajs/workflows-sdk"
+import { CHEF_EVENT_MODULE } from "../modules/chef-event"
+
+type DeleteChefEventWorkflowInput = {
+ id: string
+}
+
+const deleteChefEventStep = createStep(
+ "delete-chef-event-step",
+ async (input: DeleteChefEventWorkflowInput, { container }: { container: any }) => {
+ const chefEventModuleService = container.resolve(CHEF_EVENT_MODULE)
+
+ const chefEvent = await chefEventModuleService.retrieveChefEvent(input.id)
+
+ if (!chefEvent) {
+ throw new Error(`Chef event with id ${input.id} not found`)
+ }
+
+ await chefEventModuleService.deleteChefEvents(input.id)
+
+ return new StepResponse({
+ id: input.id,
+ deleted: true
+ })
+ }
+)
+
+export const deleteChefEventWorkflow = createWorkflow(
+ "delete-chef-event-workflow",
+ function (input: DeleteChefEventWorkflowInput) {
+ const result = deleteChefEventStep(input)
+
+ return new WorkflowResponse({
+ result
+ })
+ }
+)
+```
+
+## 2. API Routes (`src/api/admin/chef-events/`)
+
+### `route.ts` - List and Create Endpoints
+```typescript
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { z } from "zod"
+import { createChefEventWorkflow } from "../../../workflows/create-chef-event"
+import { CHEF_EVENT_MODULE } from "../../../modules/chef-event"
+
+const createChefEventSchema = z.object({
+ status: z.enum(['pending', 'confirmed', 'cancelled', 'completed']).default('pending'),
+ requestedDate: z.string(),
+ requestedTime: z.string(),
+ partySize: z.number().min(1).max(50),
+ eventType: z.enum(['cooking_class', 'plated_dinner', 'buffet_style']),
+ templateProductId: z.string().optional(),
+ locationType: z.enum(['customer_location', 'chef_location']),
+ locationAddress: z.string().min(1),
+ firstName: z.string().min(1),
+ lastName: z.string().min(1),
+ email: z.string().email(),
+ phone: z.string().optional(),
+ notes: z.string().optional(),
+ totalPrice: z.number().optional(),
+ depositPaid: z.boolean().optional(),
+ specialRequirements: z.string().optional(),
+ estimatedDuration: z.number().optional()
+})
+
+export async function GET(req: MedusaRequest, res: MedusaResponse) {
+ const chefEventModuleService = req.scope.resolve(CHEF_EVENT_MODULE)
+
+ const { limit = 20, offset = 0, q, status, eventType, locationType } = req.query
+
+ const filters: any = {}
+ if (q) filters.q = q
+ if (status) filters.status = status
+ if (eventType) filters.eventType = eventType
+ if (locationType) filters.locationType = locationType
+
+ const [chefEvents, count] = await chefEventModuleService.listAndCountChefEvents(filters, {
+ take: limit,
+ skip: offset,
+ order: { requestedDate: 'ASC' }
+ })
+
+ res.json({
+ chefEvents,
+ count,
+ limit,
+ offset
+ })
+}
+
+export async function POST(req: MedusaRequest, res: MedusaResponse) {
+ const validatedBody = createChefEventSchema.parse(req.body)
+
+ const { result } = await createChefEventWorkflow(req.scope).run({
+ input: validatedBody
+ })
+
+ res.status(201).json({ chefEvent: result.chefEvent })
+}
+```
+
+### `[id]/route.ts` - Retrieve, Update, Delete Endpoints
+```typescript
+import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
+import { z } from "zod"
+import { updateChefEventWorkflow } from "../../../../workflows/update-chef-event"
+import { deleteChefEventWorkflow } from "../../../../workflows/delete-chef-event"
+import { CHEF_EVENT_MODULE } from "../../../../modules/chef-event"
+
+const updateChefEventSchema = z.object({
+ status: z.enum(['pending', 'confirmed', 'cancelled', 'completed']).optional(),
+ requestedDate: z.string().optional(),
+ requestedTime: z.string().optional(),
+ partySize: z.number().min(1).max(50).optional(),
+ eventType: z.enum(['cooking_class', 'plated_dinner', 'buffet_style']).optional(),
+ templateProductId: z.string().optional(),
+ locationType: z.enum(['customer_location', 'chef_location']).optional(),
+ locationAddress: z.string().min(1).optional(),
+ firstName: z.string().min(1).optional(),
+ lastName: z.string().min(1).optional(),
+ email: z.string().email().optional(),
+ phone: z.string().optional(),
+ notes: z.string().optional(),
+ totalPrice: z.number().optional(),
+ depositPaid: z.boolean().optional(),
+ specialRequirements: z.string().optional(),
+ estimatedDuration: z.number().optional()
+})
+
+export async function GET(req: MedusaRequest, res: MedusaResponse) {
+ const chefEventModuleService = req.scope.resolve(CHEF_EVENT_MODULE)
+ const { id } = req.params
+
+ const chefEvent = await chefEventModuleService.retrieveChefEvent(id)
+
+ if (!chefEvent) {
+ return res.status(404).json({ message: "Chef event not found" })
+ }
+
+ res.json({ chefEvent })
+}
+
+export async function POST(req: MedusaRequest, res: MedusaResponse) {
+ const { id } = req.params
+ const validatedBody = updateChefEventSchema.parse(req.body)
+
+ const { result } = await updateChefEventWorkflow(req.scope).run({
+ input: {
+ id,
+ ...validatedBody
+ }
+ })
+
+ res.json({ chefEvent: result.chefEvent })
+}
+
+export async function DELETE(req: MedusaRequest, res: MedusaResponse) {
+ const { id } = req.params
+
+ const { result } = await deleteChefEventWorkflow(req.scope).run({
+ input: { id }
+ })
+
+ res.json({ deleted: result.deleted })
+}
+```
+
+## 3. Validation Schemas (`src/admin/routes/chef-events/schemas.ts`)
+
+```typescript
+import { z } from "zod"
+
+export const chefEventSchema = z.object({
+ status: z.enum(['pending', 'confirmed', 'cancelled', 'completed']).default('pending'),
+ requestedDate: z.string().refine(
+ (date) => {
+ const eventDate = new Date(date)
+ const now = new Date()
+ now.setHours(0, 0, 0, 0) // Reset to start of day for comparison
+ return eventDate >= now
+ },
+ "Event date must be today or in the future"
+ ),
+ requestedTime: z.string().regex(
+ /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/,
+ "Time must be in HH:MM format"
+ ),
+ partySize: z.number().min(1, "Party size must be at least 1").max(50, "Party size cannot exceed 50"),
+ eventType: z.enum(['cooking_class', 'plated_dinner', 'buffet_style']),
+ templateProductId: z.string().optional(),
+ locationType: z.enum(['customer_location', 'chef_location']),
+ locationAddress: z.string().min(1, "Location address is required"),
+ firstName: z.string().min(1, "First name is required"),
+ lastName: z.string().min(1, "Last name is required"),
+ email: z.string().email("Invalid email address"),
+ phone: z.string().optional(),
+ notes: z.string().optional(),
+ totalPrice: z.number().min(0, "Total price cannot be negative").optional(),
+ depositPaid: z.boolean().optional(),
+ specialRequirements: z.string().optional(),
+ estimatedDuration: z.number().min(30, "Duration must be at least 30 minutes").optional()
+})
+
+export const chefEventUpdateSchema = chefEventSchema.partial()
+
+// Status transition validation
+export const getValidStatusTransitions = (currentStatus: string) => {
+ const transitions = {
+ pending: ['confirmed', 'cancelled'],
+ confirmed: ['completed', 'cancelled'],
+ cancelled: [], // Final state
+ completed: [] // Final state
+ }
+ return transitions[currentStatus] || []
+}
+
+export const validateStatusTransition = (from: string, to: string) => {
+ const validTransitions = getValidStatusTransitions(from)
+ return validTransitions.includes(to)
+}
+```
+
+## 4. Admin UI Components
+
+### Main Page (`src/admin/routes/chef-events/page.tsx`)
+```typescript
+import { defineRouteConfig } from "@medusajs/admin-sdk"
+import { Container, Heading, FocusModal, toast } from "@medusajs/ui"
+import { ChefEventList } from "./components/chef-event-list"
+import { ChefEventForm } from "./components/chef-event-form"
+import { useAdminCreateChefEventMutation } from "../../hooks/chef-events"
+import { useState } from "react"
+import type { AdminCreateChefEventDTO } from "../../../sdk/admin/admin-chef-events"
+
+const ChefEventsPage = () => {
+ const [showCreateModal, setShowCreateModal] = useState(false)
+ const createChefEvent = useAdminCreateChefEventMutation()
+
+ const handleCreateChefEvent = async (data: AdminCreateChefEventDTO) => {
+ try {
+ await createChefEvent.mutateAsync(data)
+ setShowCreateModal(false)
+ toast.success("Chef Event Created", {
+ description: "The chef event has been created successfully.",
+ duration: 3000,
+ })
+ } catch (error) {
+ console.error("Error creating chef event:", error)
+ toast.error("Creation Failed", {
+ description: "There was an error creating the chef event. Please try again.",
+ duration: 5000,
+ })
+ }
+ }
+
+ return (
+
+
+ Chef Events
+
+
+ setShowCreateModal(true)} />
+
+ {showCreateModal && (
+
+
+
+ Create Chef Event
+
+
+ setShowCreateModal(false)}
+ />
+
+
+
+ )}
+
+ )
+}
+
+export const config = defineRouteConfig({
+ label: "Chef Events",
+})
+
+export default ChefEventsPage
+```
+
+### Detail Page (`src/admin/routes/chef-events/[id]/page.tsx`)
+```typescript
+import { defineRouteConfig } from "@medusajs/admin-sdk"
+import { Container, Heading, toast } from "@medusajs/ui"
+import { useParams } from "react-router-dom"
+import { ChefEventForm } from "../components/chef-event-form"
+import { useAdminRetrieveChefEvent, useAdminUpdateChefEventMutation } from "../../../hooks/chef-events"
+import type { AdminUpdateChefEventDTO } from "../../../../sdk/admin/admin-chef-events"
+
+const ChefEventDetailPage = () => {
+ const { id } = useParams<{ id: string }>()
+ const { data: chefEvent, isLoading } = useAdminRetrieveChefEvent(id!)
+ const updateChefEvent = useAdminUpdateChefEventMutation(id!)
+
+ const handleUpdateChefEvent = async (data: AdminUpdateChefEventDTO) => {
+ try {
+ await updateChefEvent.mutateAsync(data)
+ toast.success("Chef Event Updated", {
+ description: "The chef event has been updated successfully.",
+ duration: 3000,
+ })
+ } catch (error) {
+ console.error("Error updating chef event:", error)
+ toast.error("Update Failed", {
+ description: "There was an error updating the chef event. Please try again.",
+ duration: 5000,
+ })
+ }
+ }
+
+ if (isLoading) {
+ return Loading...
+ }
+
+ if (!chefEvent) {
+ return Chef event not found
+ }
+
+ return (
+
+
+
+ Edit Chef Event - {chefEvent.firstName} {chefEvent.lastName}
+
+
+
+ window.history.back()}
+ />
+
+ )
+}
+
+export const config = defineRouteConfig({
+ label: "Chef Event Details",
+})
+
+export default ChefEventDetailPage
+```
+
+## 5. Form Component Structure
+
+### Form Component (`src/admin/routes/chef-events/components/chef-event-form.tsx`)
+
+**Tab Structure:**
+1. **General Info**: Basic event details (date, time, party size, event type)
+2. **Contact**: Customer contact information
+3. **Location**: Location type and address
+4. **Details**: Additional details (notes, special requirements, pricing)
+
+**Key Features:**
+- Status transition validation
+- Date/time validation
+- Party size limits
+- Menu integration dropdown
+- Form state management with react-hook-form
+- Error handling and display
+
+## 6. List Component Features
+
+### List Component (`src/admin/routes/chef-events/components/chef-event-list.tsx`)
+
+**Primary Columns:**
+- Customer Name (firstName + lastName)
+- Event Date & Time
+- Event Type
+- Party Size
+- Status (with status badge)
+- Location Type
+- Created At
+
+**Filtering Options:**
+- Status dropdown
+- Event type dropdown
+- Date range picker
+- Location type dropdown
+- Search by customer name/email
+
+**Actions:**
+- View/Edit event
+- Delete event
+- Quick status change
+
+## 7. File Structure
+
+```
+src/
+├── workflows/
+│ ├── create-chef-event.ts
+│ ├── update-chef-event.ts
+│ └── delete-chef-event.ts
+├── api/admin/chef-events/
+│ ├── route.ts
+│ └── [id]/route.ts
+├── admin/routes/chef-events/
+│ ├── page.tsx
+│ ├── [id]/page.tsx
+│ ├── components/
+│ │ ├── chef-event-form.tsx
+│ │ └── chef-event-list.tsx
+│ └── schemas.ts
+```
+
+## 8. Implementation Order
+
+1. **Workflows** - Create the business logic layer
+2. **API Routes** - Implement the REST endpoints
+3. **Validation Schemas** - Define form validation
+4. **List Component** - Create the events listing page
+5. **Form Component** - Build the create/edit form
+6. **Main Pages** - Wire everything together
+7. **Menu Integration** - Add menu linking functionality
+
+## 9. Menu Integration Points
+
+- Add menu selection dropdown in the form using existing `useAdminListMenus` hook
+- Use existing `AdminMenusResponse` type from menu SDK
+- Display linked menu information in the list view
+- Add menu details in the event detail view
+- Link to menu detail page from event form
+
+## 10. Status Management
+
+**Status Transitions:**
+- `pending` → `confirmed` | `cancelled`
+- `confirmed` → `completed` | `cancelled`
+- `cancelled` → (final state)
+- `completed` → (final state)
+
+**Implementation in Form:**
+```typescript
+const validateStatusTransition = (from: string, to: string) => {
+ const validTransitions = getValidStatusTransitions(from)
+ return validTransitions.includes(to)
+}
+```
+
+## 11. Additional Features
+
+### Status Badges
+- Color-coded status indicators
+- Tooltip with status descriptions
+- Visual status progression
+
+### Data Validation
+- Future date validation
+- Email format validation
+- Phone number formatting
+- Time format validation
+
+### User Experience
+- Loading states
+- Error handling
+- Success notifications
+- Confirmation dialogs for destructive actions
+
+## 12. Testing Considerations
+
+### Unit Tests
+- Form validation
+- Status transition logic
+- Date/time validation
+- API endpoint testing
+
+### Integration Tests
+- Workflow execution
+- Database operations
+- API request/response cycles
+
+### E2E Tests
+- Complete event creation flow
+- Event editing and status changes
+- List filtering and search
+
+This implementation plan provides a comprehensive chef event management system that maintains consistency with the existing menu management while providing all necessary functionality for managing chef events through the admin interface.
\ No newline at end of file
diff --git a/docs/chef-events-storefront-implementation-plan.md b/docs/chef-events-storefront-implementation-plan.md
new file mode 100644
index 000000000..adba6d36c
--- /dev/null
+++ b/docs/chef-events-storefront-implementation-plan.md
@@ -0,0 +1,1276 @@
+# Chef Events Storefront Implementation Plan
+
+## Overview
+
+Transform the existing coffee shop storefront (`Barrio`) into a premium chef events booking platform for a single chef business. The system will leverage the existing Medusa v2 backend infrastructure and e-commerce capabilities while implementing a new customer-facing booking flow.
+
+## Business Model & Flow
+
+### Target Architecture
+```
+Menu Browse → Event Request → Chef Approval → Product Creation → Ticket Sales → Event Experience
+```
+
+### Key Business Rules
+- **Single Chef Platform**: One chef's business, not multi-chef marketplace
+- **Fixed Pricing Structure**:
+ - Buffet Style: $99.99 per person
+ - Cooking Class: $119.99 per person
+ - Plated Dinner: $149.99 per person
+- **No Deposit Required**: Full payment on ticket purchase
+- **Inventory Model**: Approved events become products with ticket-based inventory
+
+## Current State Analysis
+
+### ✅ Backend Strengths (Complete)
+- Menu management system with hierarchical structure (Menu → Course → Dish → Ingredient)
+- Chef event lifecycle management with status tracking
+- Admin interface for chef operations
+- Complete workflow system for business operations
+- Type-safe SDK with comprehensive APIs (`AdminMenusResource`, `AdminChefEventsResource`)
+- Validation and error handling
+- Complete email notification system for event lifecycle
+- Product creation workflow for accepted events
+- Store APIs for customer-facing menu and event data
+
+### ✅ Frontend Achievements (Core Complete)
+- Complete homepage transformation from coffee shop to chef events platform
+- Menu discovery system with search and pagination
+- 8-step event request flow with form validation
+- Enhanced product display for event products
+- Complete cart and checkout integration for event products with payment processing
+- Event-specific styling and messaging throughout the platform
+- Professional branding and user experience
+- Fixed Stripe payment integration with precision handling
+
+### 🔄 Remaining Tasks
+- Share and group purchase functionality
+- Static information pages (How It Works, About Chef, Experience Types)
+- Final testing, polish, and launch preparation
+
+## Implementation Phases
+
+---
+
+## Phase 1: Backend Store API Foundation ✅ COMPLETED
+**Timeline: 3-5 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 1 Summary
+**Completed Successfully!** We have implemented the complete backend foundation for customer-facing chef events and menu APIs:
+
+#### ✅ What We Built:
+- **Store Menu APIs**: `GET /store/menus` and `GET /store/menus/:id` with full CRUD functionality
+- **Store Chef Events API**: `POST /store/chef-events` with automatic pricing calculation
+- **Complete SDK Integration**: Type-safe client libraries for both admin and store operations
+- **Validation & Error Handling**: Comprehensive Zod schemas and proper error responses
+- **Pricing Logic**: Automatic calculation based on business rules (Buffet: $99.99, Cooking Class: $119.99, Plated Dinner: $149.99)
+
+#### ✅ Technical Achievements:
+- **Cache Headers**: 30-minute TTL for optimal performance
+- **Type Safety**: Full TypeScript interfaces and DTOs
+- **Business Logic**: Automatic status setting to 'pending' for customer requests
+- **Default Values**: Smart defaults for estimated duration based on event type
+- **Database Integration**: Proper model updates with nullable fields where appropriate
+
+#### ✅ Tested & Verified:
+- Menu listing with 2 test menus ✅
+- Menu detail retrieval ✅
+- Chef event creation with full pricing calculation ✅
+- All endpoints working with publishable API key authentication ✅
+
+#### 🔧 Key Fixes Applied:
+- **Model Field Issue**: Fixed `estimatedDuration` field to be nullable in chef event model
+- **Default Duration Logic**: Added smart defaults (3h for cooking class, 4h for plated dinner, 2.5h for buffet)
+- **Server Restart Required**: Store API routes require server restart to be recognized (development note)
+
+**Ready for Phase 2: Storefront SDK Integration** 🚀
+
+### ✅ Checkpoint 1.1: Create Store API Endpoints ✅ COMPLETED
+
+#### 1.1.1 Store Menu APIs ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/api/store/menus/route.ts
+// Reference: apps/medusa/src/api/admin/menus/route.ts
+```
+**Implementation Tasks:**
+- [x] Create `GET /store/menus` endpoint (list available menu templates)
+ - Public endpoint with publishable API key requirement (Medusa v2 standard)
+ - Return menu with courses, dishes, ingredients
+ - Add caching with 30min TTL
+- [x] Create `GET /store/menus/:id` endpoint (detailed menu)
+ - Full menu details with chef notes
+ - Include estimated serving times
+ - Optimize for SEO (structured data ready)
+
+**Validation Schema:**
+```typescript
+const listStoreMenusSchema = z.object({
+ limit: z.string().transform(val => parseInt(val)).optional().default("20"),
+ offset: z.string().transform(val => parseInt(val)).optional().default("0"),
+ q: z.string().optional()
+})
+```
+
+#### 1.1.2 Store Chef Event API ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/api/store/chef-events/route.ts
+// Reference: apps/medusa/src/api/admin/chef-events/route.ts
+```
+**Implementation Tasks:**
+- [x] Create `POST /store/chef-events` endpoint (customer event requests)
+ - Only allow creation with status 'pending'
+ - Validate all required fields
+ - Auto-calculate pricing based on event type and party size
+- [x] Add input validation schema with Zod
+- [x] Implement proper error handling
+
+**Validation Schema:**
+```typescript
+const createStoreChefEventSchema = z.object({
+ requestedDate: z.string().datetime(),
+ requestedTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/),
+ partySize: z.number().min(2).max(50),
+ eventType: z.enum(['cooking_class', 'plated_dinner', 'buffet_style']),
+ templateProductId: z.string().optional(),
+ locationType: z.enum(['customer_location', 'chef_location']),
+ locationAddress: z.string().min(10),
+ firstName: z.string().min(1),
+ lastName: z.string().min(1),
+ email: z.string().email(),
+ phone: z.string().optional(),
+ notes: z.string().optional(),
+ specialRequirements: z.string().optional()
+})
+```
+
+### ✅ Checkpoint 1.2: Create Event-to-Product Workflow
+
+#### 1.2.1 Product Creation Workflow
+```typescript
+// File: apps/medusa/src/workflows/create-event-product.ts
+// Reference: apps/medusa/src/workflows/create-menu.ts
+```
+**Implementation Tasks:**
+- [ ] Create workflow that triggers on chef event status change to "confirmed"
+- [ ] Auto-generate product with event details in title
+- [ ] Create 3 variants for each event type with fixed pricing
+- [ ] Set inventory quantity equal to party size
+- [ ] Generate unique SKU pattern: `EVENT-{eventId}-{date}-{type}`
+- [ ] Link product back to chef event and original menu
+
+**Workflow Structure:**
+```typescript
+type CreateEventProductWorkflowInput = {
+ chefEventId: string
+ eventDetails: ChefEventDetails
+ menuId?: string
+}
+
+const createEventProductStep = createStep("create-event-product-step", async (input, { container }) => {
+ // Create product with variants
+ // Set inventory management
+ // Link to chef event
+})
+```
+
+#### 1.2.2 Update Chef Event Workflow Enhancement
+```typescript
+// File: apps/medusa/src/workflows/update-chef-event.ts
+```
+**Implementation Tasks:**
+- [ ] Modify existing workflow to trigger product creation
+- [ ] Add conditional step for status change to "confirmed"
+- [ ] Handle error cases (rollback on product creation failure)
+- [ ] Add notification triggers for customer
+
+### ✅ Checkpoint 1.3: Create Store SDK Extensions ✅ COMPLETED
+
+#### 1.3.1 Store SDK Resources ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/sdk/store/store-menus.ts
+// Reference: apps/medusa/src/sdk/admin/admin-menus.ts
+```
+**Implementation Tasks:**
+- [x] Create `StoreMenusResource` class
+- [x] Implement `list()` and `retrieve()` methods
+- [x] Add proper TypeScript interfaces for store responses
+- [x] Add response caching logic
+
+```typescript
+// File: apps/medusa/src/sdk/store/store-chef-events.ts
+```
+**Implementation Tasks:**
+- [x] Create `StoreChefEventsResource` class
+- [x] Implement `create()` method for customer requests
+- [x] Add validation and error handling
+- [x] Create response types
+
+#### 1.3.2 Extend Main SDK ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/sdk/index.ts
+```
+**Implementation Tasks:**
+- [x] Add store resources to main SDK export
+- [x] Create store SDK class extending base Client
+- [x] Ensure proper authentication handling
+
+---
+
+## Phase 2: Storefront SDK Integration ✅ COMPLETED
+**Timeline: 2-3 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 2 Summary
+**Completed Successfully!** We have implemented complete storefront integration for our chef events and menu APIs:
+
+#### ✅ What We Built:
+- **Data Fetching Utilities**: Server-side functions for menus and chef events with caching
+- **Complete Type Safety**: TypeScript interfaces for all data structures and UI components
+- **Validation System**: Client-side validation for chef event requests
+- **Error Handling**: Comprehensive error handling with proper user feedback
+- **Pricing Integration**: Automatic price calculation with business rule consistency
+
+#### ✅ Technical Achievements:
+- **Caching Strategy**: 30-minute TTL using `@epic-web/cachified` for optimal performance
+- **Direct API Integration**: Direct fetch calls to our new store APIs with proper authentication
+- **Form Ready Types**: Complete type definitions for multi-step event request forms
+- **UI Component Props**: Ready-to-use interfaces for all upcoming UI components
+
+#### ✅ Files Created:
+- `apps/storefront/libs/util/server/data/menus.server.ts`: Menu data fetching with caching
+- `apps/storefront/libs/util/server/data/chef-events.server.ts`: Chef event creation and validation
+- `apps/storefront/types/menus.ts`: Complete menu type definitions and UI props
+- `apps/storefront/types/chef-events.ts`: Complete chef event types and form interfaces
+
+#### ✅ Tested & Verified:
+- TypeScript compilation successful ✅
+- Data fetching utilities integrated ✅
+- Type definitions working correctly ✅
+- Build process completed without errors ✅
+
+**Ready for Phase 3: Homepage & Branding Transformation** 🚀
+
+### ✅ Checkpoint 2.1: Update Storefront Client ✅ COMPLETED
+
+#### 2.1.1 Extend MedusaPluginsSDK ✅ COMPLETED
+```typescript
+// File: apps/storefront/libs/util/server/client.server.ts
+// Reference: Current SDK usage patterns
+```
+**Implementation Tasks:**
+- [x] Add chef events and menus to storefront SDK (via direct API calls)
+- [x] Configure base URL and authentication
+- [x] Test SDK connectivity with backend
+- [x] Add error handling and retry logic
+
+#### 2.1.2 Create Data Fetching Utilities ✅ COMPLETED
+```typescript
+// File: apps/storefront/libs/util/server/data/menus.server.ts
+// Reference: apps/storefront/libs/util/server/data/products.server.ts
+```
+**Implementation Tasks:**
+- [x] Create `fetchMenus()` function with caching
+- [x] Create `fetchMenuById()` function
+- [x] Add region-based filtering if needed
+- [x] Implement error handling
+
+```typescript
+// File: apps/storefront/libs/util/server/data/chef-events.server.ts
+```
+**Implementation Tasks:**
+- [x] Create `createChefEventRequest()` function
+- [x] Add proper validation and error formatting
+- [x] Implement success/failure handling
+
+### ✅ Checkpoint 2.2: Create Response Types ✅ COMPLETED
+```typescript
+// File: apps/storefront/types/menus.ts
+```
+**Implementation Tasks:**
+- [x] Define TypeScript interfaces for menu data
+- [x] Create DTOs for course, dish, ingredient
+- [x] Add pricing calculation types
+
+```typescript
+// File: apps/storefront/types/chef-events.ts
+```
+**Implementation Tasks:**
+- [x] Define chef event request types
+- [x] Create form validation schemas
+- [x] Add status and pricing types
+
+---
+
+## Phase 3: Homepage & Branding Transformation ✅ COMPLETED
+**Timeline: 4-5 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 3 Summary
+**Completed Successfully!** We have completely transformed the coffee shop storefront into a premium chef events booking platform:
+
+#### ✅ What We Built:
+- **ChefHero Component**: Professional hero section with chef branding, clear value proposition, and call-to-action buttons
+- **FeaturedMenus Component**: Dynamic menu showcase pulling from our backend data with elegant card layouts
+- **ExperienceTypes Component**: Three pricing tiers ($99.99, $119.99, $149.99) with detailed descriptions and features
+- **HowItWorks Component**: 4-step booking process explanation with FAQ section and visual flow
+- **Complete Homepage Transformation**: Replaced all coffee branding with chef-focused content and testimonials
+
+#### ✅ Technical Achievements:
+- **Navigation Updates**: Replaced coffee categories with chef experience navigation (Our Menus, How It Works, Request Event, About Chef)
+- **Site Configuration**: Updated SEO metadata, social media links, and site description for chef business
+- **Component Integration**: All new components integrated with existing design system and data fetching utilities
+- **Type Safety**: Full TypeScript integration with proper prop interfaces and error handling
+- **Responsive Design**: Mobile-first approach with Tailwind CSS styling
+
+#### ✅ Content & Branding:
+- Professional chef persona (Chef Elena Rodriguez) with 15+ years experience
+- Clear value propositions for each experience type
+- Customer testimonials and social proof
+- Business process transparency and FAQ section
+- Call-to-action optimization throughout the user journey
+
+#### ✅ Build Verification:
+- Successful TypeScript compilation ✅
+- All components render without errors ✅
+- Navigation system updated ✅
+- SEO optimization implemented ✅
+
+**Ready for Phase 4: Menu Discovery System** 🚀
+
+---
+
+## Phase 4: Menu Discovery System ✅ COMPLETED
+**Timeline: 5-7 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 4 Summary
+**Completed Successfully!** We have built a complete menu discovery system that allows customers to browse and view detailed menu templates:
+
+#### ✅ What We Built:
+- **Menu Listing Page (`/menus`)**: Complete menu catalog with search functionality, pagination, and responsive grid layout
+- **Menu Detail Page (`/menus/$menuId`)**: Comprehensive menu display with courses, dishes, ingredients, and pricing options
+- **MenuTemplate Component**: Rich template showcasing menu structure with pricing for all three experience types
+- **Complete Component Library**: MenuGrid, MenuListItem, MenuListHeader, MenuGridSkeleton for reusable menu display
+- **MenuListWithPagination**: Full pagination support for large menu catalogs
+
+#### ✅ Technical Achievements:
+- **Dynamic Routing**: Proper parameter handling for menu IDs with 404 redirects for invalid menus
+- **Data Integration**: Full integration with our Phase 2 data fetching utilities (`fetchMenus`, `fetchMenuById`)
+- **SEO Optimization**: Comprehensive meta tags, structured data (Recipe schema), and OpenGraph tags
+- **Search Functionality**: Real-time menu search with query persistence and result highlighting
+- **Error Handling**: Graceful error states, loading skeletons, and empty state management
+- **Performance**: Proper caching (30-min TTL), image optimization, and lazy loading
+
+#### ✅ UI/UX Features:
+- **Responsive Design**: Mobile-first approach with Tailwind CSS grid layouts
+- **Interactive Elements**: Hover effects, transitions, and visual feedback for all interactions
+- **Pricing Integration**: Clear display of all three experience pricing tiers ($99.99, $119.99, $149.99)
+- **Navigation**: Breadcrumbs, clear CTAs, and intuitive user flow from browsing to requesting
+- **Course Structure**: Visual course breakdown with numbered sections and ingredient tags
+- **Chef Branding**: Consistent chef persona and professional presentation throughout
+
+#### ✅ Content & Features:
+- **Complete Menu Display**: Hierarchical presentation (Menu → Course → Dish → Ingredient)
+- **Experience Type Integration**: All three pricing tiers displayed with "Request This Experience" CTAs
+- **Chef's Notes Section**: Professional commentary and dietary accommodation information
+- **Ingredient Categorization**: Visual distinction between required and optional ingredients
+- **Call-to-Action Optimization**: Strategic placement of request buttons with pre-filled menu parameters
+
+#### ✅ Build Verification:
+- Menu listing route compilation ✅ (`menus._index-goaX4TCN.js`)
+- Menu detail route compilation ✅ (`menus._menuId-tvVZUH_4.js`)
+- All components type-safe ✅
+- SEO structured data implemented ✅
+- Navigation integration working ✅
+
+**Ready for Phase 5: Event Request Flow** 🚀
+
+---
+
+## Phase 5: Event Request Flow ✅ COMPLETED
+**Timeline: 6-8 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 5 Summary
+**Completed Successfully!** We have implemented a complete 8-step event request flow with professional success handling:
+
+#### ✅ What We Built:
+- **Complete 8-Step Form**: Menu selection, event type, date/time, party size, location, contact details, special requests, and summary
+- **Multi-Step Navigation**: Progress indicator, step validation, and form data persistence across steps
+- **Professional Success Page**: Confirmation with event ID, next steps explanation, and contact information
+- **React Router v7 Integration**: Proper route structure with `request._index.tsx` and `request.success.tsx`
+- **Server-Side Redirect**: Clean redirect flow after successful form submission
+
+#### ✅ Technical Achievements:
+- **Form State Management**: `remix-hook-form` with Zod validation for all 8 steps
+- **Real-time Validation**: Client-side validation with proper error messages
+- **Data Integration**: Full integration with Phase 2 backend APIs
+- **Type Safety**: Complete TypeScript interfaces for all form components
+- **Responsive Design**: Mobile-first approach with Tailwind CSS styling
+- **Error Handling**: Comprehensive error states and user feedback
+
+#### ✅ User Experience:
+- **Intuitive Flow**: Clear progression through 8 logical steps
+- **Visual Feedback**: Progress indicators, validation messages, and loading states
+- **Professional Presentation**: Consistent branding and professional confirmation page
+- **Accessibility**: Proper form labels, ARIA attributes, and keyboard navigation
+
+#### ✅ Build Verification:
+- Form submission working correctly ✅
+- Success page displaying properly ✅
+- Event creation in database ✅
+- Route structure following React Router v7 conventions ✅
+
+**Ready for Phase 5.5: Email Notification System** 🚀
+
+### ✅ Checkpoint 5.1: Event Request Form ✅ COMPLETED
+
+#### 5.1.1 Main Request Route ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/routes/request._index.tsx
+```
+**Implementation Tasks:**
+- [x] Create multi-step form wizard
+- [x] Implement form state management with remix-hook-form
+- [x] Add proper validation with Zod
+- [x] Handle form submission and errors
+- [x] Add loading states and success handling
+
+**Form Validation Schema:**
+```typescript
+const eventRequestSchema = z.object({
+ menuId: z.string().optional(),
+ eventType: z.enum(['cooking_class', 'plated_dinner', 'buffet_style']),
+ requestedDate: z.string().date(),
+ requestedTime: z.string().regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/),
+ partySize: z.number().min(2).max(50),
+ locationType: z.enum(['customer_location', 'chef_location']),
+ locationAddress: z.string().min(10),
+ firstName: z.string().min(1),
+ lastName: z.string().min(1),
+ email: z.string().email(),
+ phone: z.string().optional(),
+ specialRequirements: z.string().optional(),
+ notes: z.string().optional()
+})
+```
+
+#### 5.1.2 Form Action Handler ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/routes/request._index.tsx (action function)
+```
+**Implementation Tasks:**
+- [x] Validate form data with Zod
+- [x] Call backend chef events API
+- [x] Handle success/error responses
+- [x] Return proper response format
+- [x] Add error logging
+
+### ✅ Checkpoint 5.2: Form Components ✅ COMPLETED
+
+#### 5.2.1 Multi-Step Form Container ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/components/event-request/EventRequestForm.tsx
+```
+**Implementation Tasks:**
+- [x] Step navigation component
+- [x] Progress indicator
+- [x] Step validation and transitions
+- [x] Form data persistence across steps
+- [x] Mobile-responsive design
+
+#### 5.2.2 Individual Form Steps ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/components/event-request/MenuSelector.tsx
+```
+**Implementation Tasks:**
+- [x] Menu grid with selection interface
+- [x] Search and filter capabilities
+- [x] Skip if coming from menu detail page
+- [x] Visual selection indicators
+
+```typescript
+// File: apps/storefront/app/components/event-request/EventTypeSelector.tsx
+```
+**Implementation Tasks:**
+- [x] Three event type cards with pricing
+- [x] Clear descriptions and inclusions
+- [x] Radio button selection interface
+- [x] Price calculation display
+
+```typescript
+// File: apps/storefront/app/components/event-request/DateTimeForm.tsx
+```
+**Implementation Tasks:**
+- [x] Date picker component
+- [x] Time selection interface
+- [x] Availability checking (future enhancement)
+- [x] Validation for minimum notice period
+
+```typescript
+// File: apps/storefront/app/components/event-request/PartySizeSelector.tsx
+```
+**Implementation Tasks:**
+- [x] Number input with +/- buttons
+- [x] Party size validation (2-50)
+- [x] Price calculation updates
+- [x] Visual feedback for totals
+
+```typescript
+// File: apps/storefront/app/components/event-request/LocationForm.tsx
+```
+**Implementation Tasks:**
+- [x] Location type radio buttons
+- [x] Address input fields
+- [x] Address validation
+- [x] Map integration (future enhancement)
+
+```typescript
+// File: apps/storefront/app/components/event-request/ContactDetails.tsx
+```
+**Implementation Tasks:**
+- [x] Name, email, phone inputs
+- [x] Email validation
+- [x] Phone formatting
+- [x] Required field indicators
+
+```typescript
+// File: apps/storefront/app/components/event-request/SpecialRequests.tsx
+```
+**Implementation Tasks:**
+- [x] Dietary restrictions checkboxes
+- [x] Special requirements textarea
+- [x] Additional notes field
+- [x] Character limits and validation
+
+```typescript
+// File: apps/storefront/app/components/event-request/RequestSummary.tsx
+```
+**Implementation Tasks:**
+- [x] Complete request review
+- [x] Pricing breakdown
+- [x] Edit links to previous steps
+- [x] Terms and conditions
+- [x] Final submit button
+
+### ✅ Checkpoint 5.3: Confirmation Flow ✅ COMPLETED
+
+#### 5.3.1 Success Page ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/routes/request.success.tsx
+```
+**Implementation Tasks:**
+- [x] Thank you message
+- [x] Request reference number
+- [x] What happens next explanation
+- [x] Chef response timeline
+- [x] Contact information
+
+#### 5.3.2 Email Notifications (Next Phase)
+**Implementation Tasks:**
+- [ ] Customer confirmation email
+- [ ] Chef notification email
+- [ ] Email templates
+- [ ] Integration with email service
+
+---
+
+## Phase 5.5: Email Notification System ✅ COMPLETED
+**Timeline: 2-3 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 5.5 Summary
+**Completed Successfully!** We have implemented a complete email notification system for the event request lifecycle:
+
+#### ✅ What We Built:
+- **Event Emission System**: Using `emitEventStep` from Medusa's core flows for clean event architecture
+- **Dual Email Notifications**: Customer confirmation and chef notification emails
+- **Database-First Approach**: Subscriber fetches fresh data from database using event ID
+- **Minimal Event Data**: Only emitting `{ chefEventId: "..." }` to avoid Redis serialization issues
+- **Complete Error Handling**: Robust error handling and logging throughout
+
+#### ✅ Technical Achievements:
+- **Clean Event Architecture**: Using built-in `emitEventStep` helper step
+- **Redis Compatibility**: No complex object serialization issues
+- **Type Safety**: Proper TypeScript interfaces for event data
+- **Email Templates**: Professional email templates for both customer and chef notifications
+- **Event-Driven Design**: Proper separation between event emission and business logic
+
+#### ✅ Business Flow Implemented:
+1. **Customer submits event request** → Form submission with all details
+2. **Workflow creates chef event** → Database entry with complete information
+3. **Workflow emits event** → `emitEventStep` with minimal data
+4. **Subscriber processes event** → Fetches fresh data from database
+5. **Emails sent** → Customer confirmation + Chef notification
+
+#### ✅ Files Created/Updated:
+- `apps/medusa/src/workflows/create-chef-event.ts`: Added `emitEventStep` for event emission
+- `apps/medusa/src/subscribers/chef-event-requested.ts`: Complete email notification system
+- Email templates and notification service integration
+
+#### ✅ Tested & Verified:
+- Event emission working correctly ✅
+- Email notifications sent to both customer and chef ✅
+- Database integration working properly ✅
+- Error handling and logging implemented ✅
+
+**Ready for Phase 6: Chef Acceptance Workflow & Purchase Integration** 🚀
+
+### ✅ Checkpoint 5.5.1: Email Service Integration
+
+#### 5.5.1.1 Email Service Setup
+```typescript
+// File: apps/medusa/src/services/email.service.ts
+// Reference: Medusa v2 service patterns
+```
+**Implementation Tasks:**
+- [ ] Set up email service (SendGrid, AWS SES, or similar)
+- [ ] Configure email templates
+- [ ] Add email service to Medusa container
+- [ ] Test email delivery
+
+#### 5.5.1.2 Email Templates
+```typescript
+// File: apps/medusa/src/templates/emails/
+```
+**Implementation Tasks:**
+- [ ] Customer confirmation email template
+- [ ] Chef notification email template
+- [ ] Event accepted email template (for Phase 6)
+- [ ] HTML and text versions
+- [ ] Branding and styling
+
+### ✅ Checkpoint 5.5.2: Event Request Notifications
+
+#### 5.5.2.1 Customer Confirmation Email
+**Implementation Tasks:**
+- [ ] Trigger on successful event creation
+- [ ] Include request reference number
+- [ ] Show event details (date, time, type, party size)
+- [ ] Include chef contact information
+- [ ] Set expectations for response timeline
+
+#### 5.5.2.2 Chef Notification Email
+**Implementation Tasks:**
+- [ ] Trigger on successful event creation
+- [ ] Include complete request details
+- [ ] Show customer contact information
+- [ ] Include pricing breakdown
+- [ ] Provide admin link for review
+
+### ✅ Checkpoint 5.5.3: Backend Integration
+
+#### 5.5.3.1 Workflow Enhancement
+```typescript
+// File: apps/medusa/src/workflows/create-chef-event.ts
+```
+**Implementation Tasks:**
+- [ ] Add email notification steps to existing workflow
+- [ ] Send customer confirmation email
+- [ ] Send chef notification email
+- [ ] Handle email failures gracefully
+- [ ] Add email status tracking
+
+#### 5.5.3.2 Email Status Tracking
+**Implementation Tasks:**
+- [ ] Add email_sent field to chef event model
+- [ ] Track email delivery status
+- [ ] Add retry logic for failed emails
+- [ ] Log email activities
+
+### ✅ Checkpoint 5.5.4: Testing & Validation
+
+#### 5.5.4.1 Email Testing
+**Implementation Tasks:**
+- [ ] Test email delivery in development
+- [ ] Verify email templates render correctly
+- [ ] Test with different event types and data
+- [ ] Validate email content and formatting
+
+#### 5.5.4.2 Integration Testing
+**Implementation Tasks:**
+- [ ] Test complete flow: form → database → emails
+- [ ] Verify both customer and chef emails sent
+- [ ] Test error handling for email failures
+- [ ] Validate email tracking and logging
+
+---
+
+## Phase 6: Chef Acceptance Workflow & Purchase Integration ✅ COMPLETED
+**Timeline: 4-5 days** | **Status: ✅ COMPLETED**
+
+### 🎉 Phase 6 Summary
+**Completed Successfully!** We have implemented a complete chef acceptance/rejection workflow with product creation, email notifications, and full cart integration:
+
+#### ✅ What We Built:
+- **Database Migration**: Added acceptance/rejection tracking fields to chef event model
+- **Acceptance Workflow**: Complete workflow that updates status, creates product, and sends email
+- **Rejection Workflow**: Complete workflow that updates status and sends rejection email
+- **Product Creation**: Automatic digital product creation with URL-safe handles and proper pricing
+- **Email Notifications**: Professional acceptance and rejection emails with purchase links
+- **Admin Interface**: Enhanced admin interface with accept/reject buttons and confirmation dialogs
+- **Event Product Display**: Enhanced product pages with event-specific information and styling
+- **Cart Integration**: Complete cart and checkout integration for event products with special messaging
+
+#### ✅ Technical Achievements:
+- **Workflow Integration**: Proper use of `createProductsWorkflow` for digital product creation
+- **URL-Safe Handles**: Generated clean product handles using event type, customer name, and date
+- **Email Integration**: Complete email notification system using Medusa's notification service
+- **Type Safety**: Full TypeScript integration with proper error handling
+- **Database Schema**: Added `productId`, `acceptedAt`, `acceptedBy`, `rejectionReason`, and `chefNotes` fields
+- **Cart API Fixes**: Resolved request body reading issues and added event product support
+- **Event Product Detection**: Comprehensive utilities for identifying and handling event products
+
+#### ✅ Business Flow Implemented:
+1. **Chef reviews pending event** → Admin interface shows event details
+2. **Chef accepts/rejects event** → Status change triggers workflow
+3. **Acceptance workflow** → Creates product + sends email with purchase link
+4. **Rejection workflow** → Sends rejection email with explanation
+5. **Customer receives email** → With link to purchase tickets (acceptance) or explanation (rejection)
+6. **Event product purchase** → Enhanced cart experience with event-specific messaging
+7. **Checkout completion** → Full ticket purchase flow with inventory tracking
+
+#### ✅ Files Created/Updated:
+- `apps/medusa/src/modules/chef-event/models/chef-event.ts`: Added acceptance/rejection fields
+- `apps/medusa/src/modules/chef-event/migrations/add-acceptance-fields.ts`: Database migration
+- `apps/medusa/src/workflows/accept-chef-event.ts`: Complete acceptance workflow
+- `apps/medusa/src/workflows/reject-chef-event.ts`: Complete rejection workflow
+- `apps/medusa/src/subscribers/chef-event-accepted.ts`: Acceptance email notifications
+- `apps/medusa/src/subscribers/chef-event-rejected.ts`: Rejection email notifications
+- `apps/medusa/src/api/admin/chef-events/[id]/accept/route.ts`: Acceptance API endpoint
+- `apps/medusa/src/api/admin/chef-events/[id]/reject/route.ts`: Rejection API endpoint
+- `apps/medusa/src/admin/routes/chef-events/[id]/page.tsx`: Enhanced admin interface
+- `apps/medusa/src/sdk/admin/admin-chef-events.ts`: SDK extensions for accept/reject
+- `apps/medusa/src/admin/hooks/chef-events.ts`: React Query hooks for admin actions
+- `apps/storefront/libs/util/products.ts`: Event product detection and utility functions
+- `apps/storefront/libs/util/server/data/event-products.server.ts`: Server-side data fetching
+- `apps/storefront/app/components/product/EventProductDetails.tsx`: Enhanced event product display
+- `apps/storefront/app/components/cart/CartDrawer.tsx`: Enhanced cart display for events
+- `apps/storefront/app/components/cart/CartDrawerItem.tsx`: Event-specific cart item display
+- `apps/storefront/app/routes/api.cart.line-items.create.ts`: Fixed cart API for event products
+- `apps/storefront/app/routes/products.$productHandle.tsx`: Enhanced product route for events
+
+#### ✅ Tested & Verified:
+- Database migration applied successfully ✅
+- Acceptance workflow creates products correctly ✅
+- Rejection workflow updates status properly ✅
+- Email notifications sent to customers ✅
+- Admin interface accepts/rejects events ✅
+- Product creation with URL-safe handles ✅
+- Event products can be added to cart successfully ✅
+- Cart displays event-specific information correctly ✅
+- Checkout flow works with event tickets ✅
+- Inventory tracking works properly ✅
+
+**Ready for Phase 6.6: Share & Group Purchase** 🚀
+
+### ✅ Checkpoint 6.1: Chef Acceptance Interface ✅ COMPLETED
+
+#### 6.1.1 Admin Acceptance Actions ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/admin/routes/chef-events/[id]/page.tsx
+```
+**Implementation Tasks:**
+- [x] Add "Accept Event" and "Reject Event" action buttons to chef event detail page
+- [x] Implement status transition validation (pending → confirmed/cancelled)
+- [x] Add confirmation dialogs for acceptance/rejection actions
+- [x] Show acceptance/rejection history and timestamps
+- [x] Add chef notes field for acceptance/rejection reasons
+
+#### 6.1.2 Acceptance Workflow ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/workflows/accept-chef-event.ts
+```
+**Implementation Tasks:**
+- [x] Create workflow for chef event acceptance
+- [x] Update event status to "confirmed"
+- [x] Trigger product creation workflow
+- [x] Send acceptance email to customer with product link
+- [x] Log acceptance activity with chef notes
+
+#### 6.1.3 Rejection Workflow ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/workflows/reject-chef-event.ts
+```
+**Implementation Tasks:**
+- [x] Create workflow for chef event rejection
+- [x] Update event status to "cancelled"
+- [x] Send rejection email to customer with explanation
+- [x] Add rejection reason field to chef event model
+- [x] Log rejection activity
+
+### ✅ Checkpoint 6.2: Email Notification System ✅ COMPLETED
+
+#### 6.2.1 Event Acceptance Email ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/subscribers/chef-event-accepted.ts
+```
+**Implementation Tasks:**
+- [x] Create subscriber for "chef-event.accepted" event
+- [x] Send acceptance email to customer with:
+ - Event confirmation details
+ - Link to enhanced product page for ticket purchase
+ - Event date, time, and location reminder
+ - Chef contact information
+ - Payment instructions and timeline
+- [x] Include product purchase link with pre-filled event details
+- [x] Add email template for acceptance notifications
+
+#### 6.2.2 Event Rejection Email ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/subscribers/chef-event-rejected.ts
+```
+**Implementation Tasks:**
+- [x] Create subscriber for "chef-event.rejected" event
+- [x] Send rejection email to customer with:
+ - Professional rejection message
+- [x] Chef's explanation or alternative suggestions
+- [x] Contact information for questions
+- [x] Future booking encouragement
+
+### ✅ Checkpoint 6.3: Product Creation Workflow ✅ COMPLETED
+
+#### 6.3.1 Auto-Generate Event Product ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/workflows/accept-chef-event.ts (integrated)
+```
+**Implementation Tasks:**
+- [x] Create workflow triggered by event acceptance
+- [x] Generate product with event-specific details:
+ - Title: "{Event Type} - {Customer Name} - {Date}"
+ - Description: Event details, menu information, location
+ - SKU: `EVENT-{eventId}-{date}-{type}`
+ - Variants: Single variant with event pricing
+ - Inventory: Set to party size quantity
+- [x] Link product to original chef event and menu
+- [x] Set product status to "published" for immediate purchase
+
+#### 6.3.2 Product Metadata Enhancement ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/modules/chef-event/models/chef-event.ts
+```
+**Implementation Tasks:**
+- [x] Add `productId` field to chef event model
+- [x] Add `acceptedAt` and `acceptedBy` fields
+- [x] Add `rejectionReason` field for rejected events
+- [x] Add `chefNotes` field for acceptance/rejection notes
+
+### ✅ Checkpoint 6.4: Enhanced Product Display ✅ COMPLETED
+**Timeline: 2-3 days** | **Status: ✅ COMPLETED**
+
+#### 6.4.1 Event Product Template ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/components/product/EventProductDetails.tsx
+```
+**Implementation Tasks:**
+- [x] Detect event products vs regular products
+- [x] Display event metadata (date, time, location, party size)
+- [x] Show original menu details and courses
+- [x] Display chef event status and acceptance details
+- [x] Ticket quantity selector (limited to party size)
+- [x] Share functionality for group purchases
+- [x] Custom product template for events
+
+#### 6.4.2 Product Route Enhancement ✅ COMPLETED
+```typescript
+// File: apps/storefront/app/routes/products.$productHandle.tsx
+```
+**Implementation Tasks:**
+- [x] Detect event products by product metadata
+- [x] Load related chef event and menu data
+- [x] Pass to enhanced product template
+- [x] Handle special event product logic
+- [x] Add event-specific SEO meta tags
+
+#### 6.4.3 Backend API Support ✅ COMPLETED
+```typescript
+// File: apps/medusa/src/api/store/chef-events/[id]/route.ts
+```
+**Implementation Tasks:**
+- [x] Create GET endpoint for individual chef events
+- [x] Return only confirmed events for storefront
+- [x] Include all necessary event data
+- [x] Proper error handling and validation
+
+#### 6.4.4 Utility Functions ✅ COMPLETED
+```typescript
+// File: apps/storefront/libs/util/products.ts
+// File: apps/storefront/libs/util/server/data/event-products.server.ts
+```
+**Implementation Tasks:**
+- [x] Event product detection utilities
+- [x] SKU parsing for event information
+- [x] Server-side data fetching for event products
+- [x] Integration with existing product utilities
+
+### ✅ Checkpoint 6.5: Cart & Checkout Integration ✅ COMPLETED
+**Timeline: 1-2 days** | **Status: ✅ COMPLETED**
+
+#### 🎉 Cart Integration Summary
+**Completed Successfully!** We have implemented complete cart and checkout integration for event products:
+
+#### ✅ What We Built:
+- **Event Product Detection**: Utilities to identify event products by SKU pattern (`EVENT-{eventId}-{date}-{type}`)
+- **Enhanced Cart Display**: Special styling and messaging for event products in cart
+- **Cart API Fixes**: Resolved "Body has already been read" error by reading form data only once
+- **Event-Specific Messaging**: Warning about non-refundable tickets and event-specific checkout button text
+- **Inventory Integration**: Proper handling of ticket quantities and remaining tickets display
+- **Stripe Payment Integration**: Fixed floating point precision issues for payment processing
+
+#### ✅ Technical Achievements:
+- **Cart API Enhancement**: Fixed `apps/storefront/app/routes/api.cart.line-items.create.ts` to handle event products without options
+- **Event Product Utilities**: Created comprehensive utilities in `apps/storefront/libs/util/products.ts` for event detection and data extraction
+- **Cart Component Updates**: Enhanced `CartDrawer.tsx` and `CartDrawerItem.tsx` with event-specific styling and messaging
+- **Form Data Handling**: Resolved request body reading conflicts by implementing manual form data parsing
+- **Fallback Logic**: Added support for single-variant products (event products) that don't require option selection
+- **Payment Precision Fix**: Resolved Stripe integration errors by ensuring clean integer values for payment amounts
+
+#### ✅ Business Flow Implemented:
+1. **Event Product Detection**: Automatic detection of event products by SKU pattern
+2. **Cart Addition**: Event products can be added to cart without traditional product options
+3. **Enhanced Display**: Cart shows event-specific information (tickets, event type, date)
+4. **Checkout Messaging**: Clear warnings about non-refundable tickets and event-specific button text
+5. **Inventory Management**: Proper tracking of remaining tickets vs. party size
+6. **Payment Processing**: Clean payment integration with Stripe for event ticket purchases
+
+#### ✅ Files Created/Updated:
+- `apps/storefront/libs/util/products.ts`: Event product detection and utility functions
+- `apps/storefront/libs/util/server/data/event-products.server.ts`: Server-side data fetching for event products
+- `apps/storefront/app/components/product/EventProductDetails.tsx`: Enhanced event product display component
+- `apps/storefront/app/components/cart/CartDrawer.tsx`: Enhanced cart display for events
+- `apps/storefront/app/components/cart/CartDrawerItem.tsx`: Event-specific cart item display
+- `apps/storefront/app/routes/api.cart.line-items.create.ts`: Fixed cart API for event products
+- `apps/storefront/app/routes/products.$productHandle.tsx`: Enhanced product route for event products
+- `apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts`: Fixed payment precision issues
+
+#### ✅ Key Fixes Applied:
+- **Request Body Issue**: Fixed "Body has already been read" error by reading form data only once
+- **Event Product Options**: Added fallback logic for single-variant event products
+- **Inventory Display**: Fixed inventory quantity display with proper fallbacks
+- **Form Integration**: Resolved `remix-hook-form` integration issues with proper provider setup
+- **Environment Variables**: Added helper functions for backend URL and API key management
+- **Stripe Payment Precision**: Fixed floating point precision issues by using `Math.round()` for clean integer values
+- **Product Field Queries**: Added missing fields (`sku`, `inventory_quantity`, `options`) to product queries
+
+#### ✅ Tested & Verified:
+- Event products can be added to cart successfully ✅
+- Cart displays event-specific information correctly ✅
+- Checkout flow works with event tickets ✅
+- Inventory tracking works properly ✅
+- Error handling and validation working ✅
+- Stripe payment integration working without precision errors ✅
+
+**Implementation Tasks:**
+- [x] Test event products in existing cart
+- [x] Ensure checkout flow works with tickets
+- [x] Update cart display for event products
+- [x] Handle inventory properly (tickets remaining)
+- [x] Test complete purchase flow
+- [x] Add event-specific checkout messaging
+- [x] Fix Stripe payment precision issues
+
+---
+
+## Phase 6.6: Share & Group Purchase
+**Timeline: 1-2 days** | **Status: 🔄 PENDING**
+```typescript
+// File: apps/storefront/app/components/product/EventProductShare.tsx
+```
+**Implementation Tasks:**
+- [ ] Generate shareable links for event tickets
+- [ ] Social media sharing buttons
+- [ ] Email sharing functionality
+- [ ] Group purchase messaging
+- [ ] Remaining tickets display
+- [ ] Event-specific sharing content
+
+---
+
+## Phase 7: Experience & Information Pages
+**Timeline: 3-4 days**
+
+### ✅ Checkpoint 7.1: Static Information Pages
+
+#### 7.1.1 How It Works Page
+```typescript
+// File: apps/storefront/app/routes/how-it-works.tsx
+// Reference: apps/storefront/app/routes/about-us.tsx
+```
+**Implementation Tasks:**
+- [ ] Step-by-step process explanation
+- [ ] Timeline expectations for each step
+- [ ] Pricing structure explanation
+- [ ] FAQ section
+- [ ] Equipment/space requirements
+- [ ] Cancellation policies
+
+#### 7.1.2 About Chef Page
+```typescript
+// File: apps/storefront/app/routes/about.tsx
+```
+**Implementation Tasks:**
+- [ ] Chef's background and philosophy
+- [ ] Professional credentials and experience
+- [ ] Photo gallery of past events
+- [ ] Service area information
+- [ ] Awards and recognition
+- [ ] Personal story and inspiration
+
+#### 7.1.3 Experience Types Page
+```typescript
+// File: apps/storefront/app/routes/experiences.tsx
+```
+**Implementation Tasks:**
+- [ ] Detailed breakdown of each experience type:
+ - Buffet Style ($99.99): Description, what's included, ideal for
+ - Cooking Class ($119.99): Interactive experience, learning focus
+ - Plated Dinner ($149.99): Fine dining, full service experience
+- [ ] Photo galleries for each type
+- [ ] Sample menus or past events
+- [ ] Booking CTAs
+
+### ✅ Checkpoint 7.2: Enhanced Content
+**Implementation Tasks:**
+- [ ] Customer testimonials and reviews
+- [ ] Photo galleries from past events
+- [ ] Chef's blog or cooking tips (future)
+- [ ] Seasonal menu highlights
+- [ ] Social media integration
+
+---
+
+## Phase 8: Testing, Polish & Launch
+**Timeline: 3-5 days**
+
+### ✅ Checkpoint 8.1: Comprehensive Testing
+
+#### 8.1.1 End-to-End Flow Testing
+**Implementation Tasks:**
+- [ ] Test complete customer journey
+- [ ] Menu browsing to request submission
+- [ ] Chef approval to product creation
+- [ ] Product purchase to completion
+- [ ] Error handling and edge cases
+- [ ] Mobile responsiveness testing
+
+#### 8.1.2 Performance Testing
+**Implementation Tasks:**
+- [ ] Page load speed optimization
+- [ ] Image optimization and lazy loading
+- [ ] API response time testing
+- [ ] Database query optimization
+- [ ] Caching effectiveness
+
+#### 8.1.3 SEO & Accessibility
+**Implementation Tasks:**
+- [ ] Meta tags and structured data
+- [ ] Accessibility audit (WCAG compliance)
+- [ ] Search engine optimization
+- [ ] Social media preview testing
+- [ ] Mobile SEO testing
+
+### ✅ Checkpoint 8.2: Content & Branding Finalization
+**Implementation Tasks:**
+- [ ] Replace all placeholder content
+- [ ] Professional food photography
+- [ ] Chef brand assets and styling
+- [ ] Legal pages (terms, privacy)
+- [ ] Final copy review and editing
+
+### ✅ Checkpoint 8.3: Launch Preparation
+**Implementation Tasks:**
+- [ ] Production environment setup
+- [ ] Domain configuration
+- [ ] SSL certificates
+- [ ] Analytics setup (Google Analytics, etc.)
+- [ ] Error monitoring (Sentry, etc.)
+- [ ] Backup and monitoring systems
+
+---
+
+## Technical Reference
+
+### Current Codebase Patterns
+
+#### Form Handling Pattern
+The storefront uses `remix-hook-form` with Zod validation:
+```typescript
+// Reference: apps/storefront/app/components/reviews/ProductReviewForm.tsx
+const form = useRemixForm({
+ resolver: zodResolver(schema),
+ fetcher,
+ submitConfig: {
+ method: 'post',
+ action: '/api/endpoint',
+ encType: 'multipart/form-data',
+ },
+ defaultValues: { /* ... */ }
+})
+```
+
+#### Route Structure Pattern
+- Static routes: `app/routes/about.tsx`
+- Dynamic routes: `app/routes/products.$productHandle.tsx`
+- API routes: `app/routes/api.endpoint.ts`
+- Index routes: `app/routes/products._index.tsx`
+
+#### Component Architecture
+- Section components in `app/components/sections/`
+- Common components in `app/components/common/`
+- Business logic components by domain
+- Reference Hero component structure for consistent layouts
+
+#### Data Fetching Pattern
+```typescript
+// Reference: apps/storefront/libs/util/server/data/products.server.ts
+export const fetchMenus = async (request: Request, options = {}) => {
+ return await cachified({
+ key: `menus-${JSON.stringify(options)}`,
+ cache: sdkCache,
+ ttl: MILLIS.THIRTY_MINUTES,
+ async getFreshValue() {
+ return await sdk.store.menus.list(options)
+ }
+ })
+}
+```
+
+#### Workflow Pattern (Backend)
+```typescript
+// Reference: apps/medusa/src/workflows/create-menu.ts
+export const createEventProductWorkflow = createWorkflow(
+ "create-event-product-workflow",
+ function (input: CreateEventProductInput) {
+ const product = createEventProductStep(input)
+ const linkChefEvent = linkChefEventToProductStep(product, input)
+
+ return new WorkflowResponse({ product })
+ }
+)
+```
+
+### Key Dependencies & Libraries
+
+#### Frontend
+- **Form Management**: `remix-hook-form`, `@hookform/resolvers/zod`
+- **Validation**: `zod`
+- **Styling**: `tailwindcss`, `clsx`
+- **Routing**: React Router v7 (Remix)
+- **HTTP**: `@epic-web/cachified` for caching
+- **Components**: `@lambdacurry/forms/remix-hook-form`
+
+#### Backend
+- **Framework**: Medusa v2
+- **Workflows**: `@medusajs/workflows-sdk`
+- **Database**: PostgreSQL with migrations
+- **Validation**: `zod`
+- **SDK**: Custom extensions for type safety
+
+### Performance Considerations
+- Use `cachified` for API responses with appropriate TTL
+- Implement image optimization for food photography
+- Add lazy loading for menu galleries
+- Use proper React Router prefetching
+- Optimize bundle size with dynamic imports
+
+### SEO Strategy
+- Structured data for recipes/menus (Schema.org)
+- Meta tags for each menu page
+- OpenGraph tags for social sharing
+- Sitemap generation for menu pages
+- Local business schema markup
+
+### Error Handling Strategy
+- Zod validation on both frontend and backend
+- Comprehensive error boundaries
+- User-friendly error messages
+- Logging and monitoring integration
+- Graceful degradation for failures
+
+---
+
+## Success Metrics
+
+### Technical Metrics
+- [ ] Page load speed < 3 seconds
+- [ ] Mobile responsiveness score > 95%
+- [ ] Accessibility score > 90%
+- [ ] SEO score > 85%
+
+### Business Metrics
+- [ ] Complete customer journey (browse to purchase)
+- [ ] Event request submission success rate
+- [ ] Chef approval to product creation automation
+- [ ] Payment completion rate
+
+### User Experience Metrics
+- [ ] Intuitive navigation and flow
+- [ ] Mobile-first design implementation
+- [ ] Clear pricing and expectations
+- [ ] Professional brand presentation
+
+---
+
+## Future Enhancements
+
+### Phase 9+ (Post-Launch)
+- [ ] Customer authentication and request tracking
+- [ ] Chef availability calendar integration
+- [ ] Review and rating system for completed events
+- [ ] Email automation for notifications
+- [ ] Advanced search and filtering
+- [ ] Multi-chef platform expansion
+- [ ] Integration with scheduling tools
+- [ ] Customer loyalty program
+- [ ] Advanced analytics and reporting
+
+---
+
+## Recent Progress Summary (Latest Updates)
+
+### 🎯 Cart Integration & Payment Processing ✅ COMPLETED
+**Date: Latest Session** | **Status: ✅ COMPLETED**
+
+#### ✅ Major Achievements:
+- **Fixed Cart API**: Resolved "Body has already been read" error by implementing manual form data parsing
+- **Event Product Detection**: Created comprehensive utilities to identify event products by SKU pattern
+- **Enhanced Cart Display**: Added event-specific styling and messaging for event products in cart
+- **Inventory Integration**: Proper handling of ticket quantities and remaining tickets display
+- **Form Integration**: Resolved `remix-hook-form` integration issues with proper provider setup
+- **Stripe Payment Fix**: Resolved floating point precision issues causing payment integration errors
+
+#### ✅ Technical Fixes Applied:
+- **Request Body Issue**: Fixed by reading form data only once in cart API
+- **Event Product Options**: Added fallback logic for single-variant event products
+- **Inventory Display**: Fixed inventory quantity display with proper fallbacks
+- **Environment Variables**: Added helper functions for backend URL and API key management
+- **Product Field Queries**: Added missing fields (`sku`, `inventory_quantity`, `options`) to product queries
+- **Payment Precision**: Fixed Stripe integration by using `Math.round()` to ensure clean integer values
+- **Debug Logging**: Added comprehensive logging to track payment conversion process
+
+#### ✅ Business Flow Now Working:
+1. **Event Product Display**: Enhanced product pages show event-specific information
+2. **Cart Addition**: Event products can be added to cart without traditional options
+3. **Cart Display**: Shows event-specific information (tickets, event type, date)
+4. **Checkout Messaging**: Clear warnings about non-refundable tickets
+5. **Inventory Tracking**: Proper tracking of remaining tickets vs. party size
+6. **Payment Processing**: Clean Stripe integration without floating point precision errors
+
+#### ✅ Files Updated:
+- `apps/storefront/app/routes/api.cart.line-items.create.ts`: Fixed cart API for event products
+- `apps/storefront/libs/util/products.ts`: Event product detection utilities
+- `apps/storefront/libs/util/server/data/event-products.server.ts`: Server-side data fetching
+- `apps/storefront/app/components/product/EventProductDetails.tsx`: Enhanced event product display
+- `apps/storefront/app/components/cart/CartDrawer.tsx`: Enhanced cart display for events
+- `apps/storefront/app/components/cart/CartDrawerItem.tsx`: Event-specific cart item display
+- `apps/storefront/libs/util/checkout/amountToStripeExpressCheckoutAmount.ts`: Fixed payment precision
+- `apps/storefront/app/components/checkout/CheckoutOrderSummary/CheckoutOrderSummaryTotals.tsx`: Fixed subtotal calculation
+
+#### ✅ Tested & Verified:
+- Event products can be added to cart successfully ✅
+- Cart displays event-specific information correctly ✅
+- Checkout flow works with event tickets ✅
+- Inventory tracking works properly ✅
+- Error handling and validation working ✅
+- Stripe payment integration working without precision errors ✅
+
+**Ready for Phase 6.6: Share & Group Purchase** 🚀
+
+---
+
+*This implementation plan provides a comprehensive roadmap for transforming the coffee shop storefront into a premium chef events booking platform while leveraging the existing e-commerce infrastructure and maintaining high code quality standards.*
\ No newline at end of file
diff --git a/docs/chef-management-system-documentation.md b/docs/chef-management-system-documentation.md
new file mode 100644
index 000000000..43c46cce4
--- /dev/null
+++ b/docs/chef-management-system-documentation.md
@@ -0,0 +1,404 @@
+# Chef Management System Documentation
+
+## Overview
+
+This Medusa v2 application implements a comprehensive chef/catering business management system that allows chefs to:
+- Create and manage menu templates with hierarchical structure (menus → courses → dishes → ingredients)
+- Handle customer booking requests (chef events) with full lifecycle management
+- Link menu templates to bookings for standardized service offerings
+- Manage business operations through a custom admin interface
+
+## Business Model
+
+### Core Entities
+
+**Menus** = Service offerings/templates that chefs create to showcase their culinary services
+**Chef Events** = Customer booking requests that reference menu templates and track the booking lifecycle
+
+### Workflow
+```
+Menu Creation → Customer Discovery → Event Request → Confirmation → Service Delivery
+```
+
+## System Architecture
+
+### Module Structure
+The system is built using Medusa v2's modular architecture with two core modules:
+
+```
+apps/medusa/src/modules/
+├── menu/ # Menu management module
+│ ├── models/ # Data models (Menu, Course, Dish, Ingredient)
+│ ├── service.ts # Business logic layer
+│ ├── migrations/ # Database schema changes
+│ └── index.ts # Module definition
+└── chef-event/ # Chef event management module
+ ├── models/ # Data models (ChefEvent)
+ ├── service.ts # Business logic layer
+ ├── migrations/ # Database schema changes
+ └── index.ts # Module definition
+```
+
+### Data Models
+
+#### Menu Hierarchy
+```typescript
+// apps/medusa/src/modules/menu/models/menu.ts
+Menu {
+ id: string
+ name: string
+ description?: string
+ isActive: boolean
+ estimatedCost?: number
+ estimatedServingTime?: number
+ courses: Course[] // One-to-many relationship
+}
+
+// apps/medusa/src/modules/menu/models/course.ts
+Course {
+ id: string
+ name: string
+ description?: string
+ order: number
+ menu_id: string
+ dishes: Dish[] // One-to-many relationship
+}
+
+// apps/medusa/src/modules/menu/models/dish.ts
+Dish {
+ id: string
+ name: string
+ description?: string
+ course_id: string
+ ingredients: Ingredient[] // One-to-many relationship
+}
+
+// apps/medusa/src/modules/menu/models/ingredient.ts
+Ingredient {
+ id: string
+ name: string
+ quantity?: string
+ unit?: string
+ notes?: string
+ dish_id: string
+}
+```
+
+#### Chef Event Model
+```typescript
+// apps/medusa/src/modules/chef-event/models/chef-event.ts
+ChefEvent {
+ id: string
+ status: 'pending' | 'confirmed' | 'cancelled' | 'completed'
+ requestedDate: Date
+ requestedTime: string
+ partySize: number
+ eventType: 'cooking_class' | 'plated_dinner' | 'buffet_style'
+ templateProductId?: string // Reference to Menu template
+ locationType: 'customer_location' | 'chef_location'
+ locationAddress: string
+ firstName: string
+ lastName: string
+ email: string
+ phone?: string
+ notes?: string
+ totalPrice?: number
+ depositPaid: boolean
+ specialRequirements?: string
+ estimatedDuration?: number
+}
+```
+
+### Service Layer
+
+Both modules use Medusa v2's Service Factory pattern for automatic CRUD operations:
+
+```typescript
+// apps/medusa/src/modules/menu/service.ts
+class MenuModuleService extends MedusaService({
+ Menu, Course, Dish, Ingredient
+}) {
+ // Automatically generates:
+ // - createMenus, retrieveMenu, listMenus, updateMenus, deleteMenus
+ // - createCourses, retrieveCourse, listCourses, updateCourses, deleteCourses
+ // - createDishes, retrieveDish, listDishes, updateDishes, deleteDishes
+ // - createIngredients, retrieveIngredient, listIngredients, updateIngredients, deleteIngredients
+}
+
+// apps/medusa/src/modules/chef-event/service.ts
+class ChefEventModuleService extends MedusaService({
+ ChefEvent
+}) {
+ // Automatically generates:
+ // - createChefEvents, retrieveChefEvent, listChefEvents, updateChefEvents, deleteChefEvents
+}
+```
+
+## API Layer
+
+### REST Endpoints
+
+#### Menu APIs
+```typescript
+// apps/medusa/src/api/admin/menus/route.ts
+GET /admin/menus # List all menus with pagination
+POST /admin/menus # Create new menu
+
+// apps/medusa/src/api/admin/menus/[id]/route.ts
+GET /admin/menus/:id # Retrieve specific menu
+POST /admin/menus/:id # Update menu
+DELETE /admin/menus/:id # Delete menu
+```
+
+#### Chef Event APIs
+```typescript
+// apps/medusa/src/api/admin/chef-events/route.ts
+GET /admin/chef-events # List events with filtering (status, type, location, search)
+POST /admin/chef-events # Create new chef event
+
+// apps/medusa/src/api/admin/chef-events/[id]/route.ts
+GET /admin/chef-events/:id # Retrieve specific event
+POST /admin/chef-events/:id # Update event
+DELETE /admin/chef-events/:id # Delete event
+```
+
+### Response Formats
+
+All APIs return data wrapped in response objects:
+
+```typescript
+// Single entity responses
+{ chefEvent: ChefEventDTO }
+{ menu: MenuDTO }
+
+// List responses
+{
+ chefEvents: ChefEventDTO[],
+ count: number,
+ limit: number,
+ offset: number
+}
+{
+ menus: MenuDTO[],
+ count: number,
+ limit: number,
+ offset: number
+}
+```
+
+## Workflow Layer
+
+### Chef Event Workflows
+```typescript
+// apps/medusa/src/workflows/create-chef-event.ts
+createChefEventWorkflow() # Validates and creates new chef events
+
+// apps/medusa/src/workflows/update-chef-event.ts
+updateChefEventWorkflow() # Handles updates with date conversion
+
+// apps/medusa/src/workflows/delete-chef-event.ts
+deleteChefEventWorkflow() # Soft delete with existence validation
+```
+
+### Menu Workflows
+```typescript
+// apps/medusa/src/workflows/create-menu.ts
+createMenuWorkflow() # Creates menus with validation
+
+// apps/medusa/src/workflows/delete-menu.ts
+deleteMenuWorkflow() # Handles menu deletion
+```
+
+## Admin Interface
+
+### Component Architecture
+
+#### Chef Events Management
+```typescript
+// apps/medusa/src/admin/routes/chef-events/page.tsx
+- Main listing page with filtering and search
+- Create modal integration
+- Status-based filtering (pending, confirmed, cancelled, completed)
+
+// apps/medusa/src/admin/routes/chef-events/[id]/page.tsx
+- Detail/edit page for individual events
+- Form integration with validation
+
+// apps/medusa/src/admin/routes/chef-events/components/
+├── chef-event-form.tsx # Tabbed form (General, Contact, Location, Details)
+├── chef-event-list.tsx # Table with filtering and actions
+└── menu-details.tsx # Menu template display component
+```
+
+#### Menu Management
+```typescript
+// apps/medusa/src/admin/routes/menus/page.tsx
+- Menu listing and management interface
+
+// apps/medusa/src/admin/routes/menus/[id]/page.tsx
+- Menu detail and editing interface
+
+// apps/medusa/src/admin/routes/menus/components/
+├── menu-form.tsx # Menu creation/editing forms
+└── menu-list.tsx # Menu listing component
+```
+
+### Form Validation
+
+Uses Zod schemas for runtime validation:
+
+```typescript
+// apps/medusa/src/admin/routes/chef-events/schemas.ts
+chefEventSchema # Complete validation for new events
+chefEventUpdateSchema # Partial validation for updates
+statusTransitionValidation # Business rule enforcement
+
+// apps/medusa/src/admin/routes/menus/schemas.ts
+menuSchema # Menu validation rules
+```
+
+### State Management
+
+#### React Query Integration
+```typescript
+// apps/medusa/src/admin/hooks/chef-events.ts
+useAdminListChefEvents() # List with filtering
+useAdminRetrieveChefEvent() # Single event retrieval
+useAdminCreateChefEventMutation() # Create mutations
+useAdminUpdateChefEventMutation() # Update mutations
+
+// apps/medusa/src/admin/hooks/menus.ts
+useAdminListMenus() # Menu listing
+useAdminRetrieveMenu() # Single menu retrieval
+useAdminCreateMenuMutation() # Create mutations
+useAdminUpdateMenuMutation() # Update mutations
+```
+
+#### Form State Management
+React Hook Form with Zod validation:
+- Automatic form validation
+- Error handling and display
+- Form reset on data changes
+- Status transition validation
+
+## SDK Integration
+
+### Type-Safe Client
+```typescript
+// apps/medusa/src/sdk/admin/admin-chef-events.ts
+AdminChefEventsResource {
+ list(query) # Filtered listing
+ retrieve(id) # Single entity retrieval
+ create(data) # Creation
+ update(id, data) # Updates
+ delete(id) # Deletion
+}
+
+// apps/medusa/src/sdk/admin/admin-menus.ts
+AdminMenusResource {
+ list(query) # Menu listing
+ retrieve(id) # Single menu retrieval
+ create(data) # Menu creation
+ update(id, data) # Menu updates
+ delete(id) # Menu deletion
+}
+```
+
+### Response Unwrapping
+SDK handles API response unwrapping automatically:
+```typescript
+// API returns: { chefEvent: {...} }
+// SDK returns: {...} (unwrapped chef event)
+```
+
+## Data Relationships
+
+### Menu-Chef Event Integration
+```typescript
+// Chef events can reference menu templates
+ChefEvent.templateProductId -> Menu.id
+
+// This enables:
+// 1. Using menus as starting templates for events
+// 2. Tracking which menus are most popular
+// 3. Standardizing service offerings
+// 4. Providing cost estimates based on menu complexity
+```
+
+### Product Linking
+```typescript
+// apps/medusa/src/links/product-menu.ts
+// apps/medusa/src/links/product-chefEvent.ts
+// Links establish relationships between core entities and Medusa products
+```
+
+## Key Features
+
+### Chef Event Lifecycle Management
+1. **Pending**: New customer requests
+2. **Confirmed**: Chef has accepted and confirmed details
+3. **Completed**: Service has been delivered
+4. **Cancelled**: Request was cancelled
+
+### Business Intelligence
+- Event status tracking and reporting
+- Menu template usage analytics
+- Customer information management
+- Revenue tracking through pricing fields
+
+### Customization Support
+- Menu templates provide starting points
+- Events can be customized per customer needs
+- Special requirements and notes fields
+- Flexible pricing and duration
+
+## Technical Patterns
+
+### Medusa v2 Best Practices
+- **Modules**: Self-contained domain logic
+- **Service Factory**: Automatic CRUD generation
+- **Workflows**: Complex business operations
+- **Type Safety**: Full TypeScript coverage
+- **Validation**: Zod schemas for runtime safety
+- **Error Handling**: Consistent error responses
+- **Admin SDK**: Type-safe client integration
+
+### Frontend Architecture
+- **React Query**: Server state management
+- **React Hook Form**: Form state and validation
+- **Medusa UI**: Consistent design system
+- **TypeScript**: End-to-end type safety
+
+## Development Guidelines
+
+### Adding New Features
+1. Start with data model in modules/
+2. Add workflows for business logic
+3. Create API routes following REST patterns
+4. Update SDK with new endpoints
+5. Build admin components with validation
+6. Add comprehensive error handling
+
+### Testing Strategy
+- Unit tests for services and workflows
+- Integration tests for API endpoints
+- Component tests for admin interface
+- End-to-end tests for critical user flows
+
+## Future Storefront Integration
+
+### Potential Customer-Facing Features
+- Browse available menu templates
+- Request chef events with menu selection
+- Real-time availability checking
+- Customer portal for booking management
+- Review and rating system
+- Online payment integration
+
+### Data Already Available
+- Complete menu catalog with detailed ingredients
+- Event types and pricing information
+- Chef location and service area data
+- Availability and scheduling data
+
+This system provides a solid foundation for both chef administration and future customer-facing storefront development.
\ No newline at end of file
diff --git a/docs/deployment-plan.md b/docs/deployment-plan.md
new file mode 100644
index 000000000..48d5a5754
--- /dev/null
+++ b/docs/deployment-plan.md
@@ -0,0 +1,424 @@
+# Medusa Monorepo Deployment Plan
+
+## Overview
+This document outlines the step-by-step deployment plan for the Medusa monorepo to Railway (backend) and Vercel (storefront), following the official Medusa deployment documentation.
+
+## Project Structure
+- **Backend**: Medusa v2 API with custom modules (menu, chef-event, resend)
+- **Storefront**: React Router v7 (Remix) application
+- **Database**: PostgreSQL (Railway)
+- **Cache**: Redis (Railway)
+- **File Storage**: Local (can be upgraded to S3)
+- **Email**: Resend integration
+
+## Pre-Deployment Checklist
+
+### ✅ Environment Setup
+- [ ] Node.js 20+ installed
+- [ ] Yarn package manager configured
+- [ ] Git repository ready
+- [ ] Railway CLI installed (`npm install -g @railway/cli`)
+- [ ] Vercel CLI installed (`npm install -g vercel`)
+
+### ✅ Code Preparation
+- [ ] All changes committed to main branch
+- [ ] Tests passing locally
+- [ ] Build process working (`yarn build`)
+- [ ] Environment variables documented
+
+## Phase 1: Backend Configuration Updates ✅ COMPLETED
+
+### Checkpoint 1.1: Update Medusa Configuration
+**Status**: ✅ Completed
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [x] Update `apps/medusa/medusa-config.ts` with worker mode configuration
+- [x] Add admin disable configuration
+- [x] Verify Redis configuration for production
+- [x] Test configuration locally
+
+**Files to Modify**:
+- `apps/medusa/medusa-config.ts`
+
+**Commands**:
+```bash
+cd apps/medusa
+yarn typecheck
+yarn build
+```
+
+### Checkpoint 1.2: Add Predeploy Script
+**Status**: ✅ Completed
+**Estimated Time**: 15 minutes
+
+**Tasks**:
+- [x] Add predeploy script to `apps/medusa/package.json`
+- [x] Test migration script locally
+- [x] Verify database sync works
+
+**Files to Modify**:
+- `apps/medusa/package.json`
+
+**Commands**:
+```bash
+cd apps/medusa
+yarn predeploy
+```
+
+### Checkpoint 1.3: Health Check Implementation
+**Status**: ✅ Completed
+**Estimated Time**: 20 minutes
+
+**Tasks**:
+- [x] Create health check endpoint
+- [x] Add database connectivity test
+- [x] Test health endpoint locally
+
+**Files to Create**:
+- `apps/medusa/src/api/health.ts`
+
+**Commands**:
+```bash
+cd apps/medusa
+yarn dev
+# Test: curl http://localhost:9000/health
+```
+
+## Phase 2: Railway Backend Deployment
+
+### Checkpoint 2.1: Create Railway Project
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Create new Railway project
+- [ ] Add PostgreSQL database service
+- [ ] Add Redis database service
+- [ ] Configure database connection strings
+
+**Railway Steps**:
+1. Go to Railway dashboard
+2. Click "New Project"
+3. Choose "Deploy PostgreSQL"
+4. Add Redis service
+
+### Checkpoint 2.2: Deploy Medusa Server
+**Status**: ⏳ Pending
+**Estimated Time**: 45 minutes
+
+**Tasks**:
+- [ ] Create Medusa server service
+- [ ] Configure environment variables (server mode)
+- [ ] Set start command for server mode
+- [ ] Deploy and verify
+
+**Environment Variables (Server Mode)**:
+```bash
+COOKIE_SECRET=your-super-secure-cookie-secret
+JWT_SECRET=your-super-secure-jwt-secret
+STORE_CORS=https://your-storefront-domain.vercel.app
+ADMIN_CORS=https://your-railway-server-domain.railway.app
+AUTH_CORS=https://your-storefront-domain.vercel.app,https://your-railway-server-domain.railway.app
+DISABLE_MEDUSA_ADMIN=false
+MEDUSA_WORKER_MODE=server
+PORT=9000
+HOST=0.0.0.0
+DATABASE_URL=${{Postgres.DATABASE_PUBLIC_URL}}
+REDIS_URL=${{Redis.REDIS_PUBLIC_URL}}
+STRIPE_API_KEY=sk_live_...
+RESEND_API_KEY=re_...
+RESEND_FROM_EMAIL=noreply@yourdomain.com
+STOREFRONT_URL=https://your-storefront-domain.vercel.app
+ADMIN_BACKEND_URL=https://your-railway-server-domain.railway.app
+```
+
+**Start Command (Server Mode)**:
+```bash
+cd .medusa/server && yarn install && yarn run predeploy && yarn run start
+```
+
+### Checkpoint 2.3: Deploy Medusa Worker
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Create Medusa worker service
+- [ ] Configure environment variables (worker mode)
+- [ ] Set start command for worker mode
+- [ ] Deploy and verify
+
+**Environment Variables (Worker Mode)**:
+```bash
+COOKIE_SECRET=your-super-secure-cookie-secret
+JWT_SECRET=your-super-secure-jwt-secret
+DISABLE_MEDUSA_ADMIN=true
+MEDUSA_WORKER_MODE=worker
+PORT=9000
+HOST=0.0.0.0
+DATABASE_URL=${{Postgres.DATABASE_PUBLIC_URL}}
+REDIS_URL=${{Redis.REDIS_PUBLIC_URL}}
+STRIPE_API_KEY=sk_live_...
+RESEND_API_KEY=re_...
+RESEND_FROM_EMAIL=noreply@yourdomain.com
+```
+
+**Start Command (Worker Mode)**:
+```bash
+cd .medusa/server && yarn install && yarn run start
+```
+
+### Checkpoint 2.4: Verify Backend Deployment
+**Status**: ⏳ Pending
+**Estimated Time**: 20 minutes
+
+**Tasks**:
+- [ ] Test health endpoint
+- [ ] Verify admin dashboard access
+- [ ] Test API endpoints
+- [ ] Check database connectivity
+- [ ] Verify Redis connectivity
+
+**Test Commands**:
+```bash
+# Health check
+curl https://your-railway-server-domain.railway.app/health
+
+# Admin dashboard
+open https://your-railway-server-domain.railway.app/app
+
+# Store API
+curl https://your-railway-server-domain.railway.app/store/products
+```
+
+## Phase 3: Storefront Deployment (Vercel)
+
+### Checkpoint 3.1: Configure Vercel
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Create `vercel.json` configuration
+- [ ] Set up environment variables
+- [ ] Configure build settings
+- [ ] Test build locally
+
+**Files to Create**:
+- `apps/storefront/vercel.json`
+
+**Environment Variables**:
+```bash
+MEDUSA_BACKEND_URL=https://your-railway-server-domain.railway.app
+STRIPE_PUBLISHABLE_KEY=pk_live_...
+GOOGLE_MAPS_API_KEY=your-google-maps-api-key
+NODE_ENV=production
+```
+
+### Checkpoint 3.2: Deploy Storefront
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Connect repository to Vercel
+- [ ] Configure build settings
+- [ ] Deploy to production
+- [ ] Verify deployment
+
+**Commands**:
+```bash
+cd apps/storefront
+vercel --prod
+```
+
+### Checkpoint 3.3: Verify Storefront
+**Status**: ⏳ Pending
+**Estimated Time**: 20 minutes
+
+**Tasks**:
+- [ ] Test homepage loading
+- [ ] Verify API connectivity
+- [ ] Test product pages
+- [ ] Check checkout flow
+- [ ] Verify cart functionality
+
+## Phase 4: Post-Deployment Setup
+
+### Checkpoint 4.1: Create Admin User
+**Status**: ⏳ Pending
+**Estimated Time**: 15 minutes
+
+**Tasks**:
+- [ ] Install Railway CLI
+- [ ] Link to Railway project
+- [ ] Create admin user
+- [ ] Test admin login
+
+**Commands**:
+```bash
+npm install -g @railway/cli
+railway login
+railway link
+railway run npx medusa user -e admin@yourdomain.com -p your-secure-password
+```
+
+### Checkpoint 4.2: Seed Production Data
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Run production seed script
+- [ ] Verify menu data
+- [ ] Verify chef event data
+- [ ] Test email functionality
+
+**Commands**:
+```bash
+cd apps/medusa
+railway run yarn seed:prod
+```
+
+### Checkpoint 4.3: Configure Custom Domains
+**Status**: ⏳ Pending
+**Estimated Time**: 45 minutes
+
+**Tasks**:
+- [ ] Configure custom domain for Railway backend
+- [ ] Configure custom domain for Vercel storefront
+- [ ] Update DNS settings
+- [ ] Update environment variables with new domains
+- [ ] Redeploy with new domains
+
+## Phase 5: Monitoring and Testing
+
+### Checkpoint 5.1: Set Up Monitoring
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Configure Railway monitoring
+- [ ] Set up Vercel analytics
+- [ ] Configure error tracking (Sentry)
+- [ ] Set up uptime monitoring
+
+### Checkpoint 5.2: End-to-End Testing
+**Status**: ⏳ Pending
+**Estimated Time**: 60 minutes
+
+**Tasks**:
+- [ ] Test complete user journey
+- [ ] Test admin functionality
+- [ ] Test payment processing
+- [ ] Test email notifications
+- [ ] Test file uploads
+- [ ] Performance testing
+
+### Checkpoint 5.3: Security Review
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Verify HTTPS is enabled
+- [ ] Check CORS configuration
+- [ ] Verify environment variables are secure
+- [ ] Test authentication flows
+- [ ] Review access controls
+
+## Phase 6: Documentation and Handover
+
+### Checkpoint 6.1: Update Documentation
+**Status**: ⏳ Pending
+**Estimated Time**: 45 minutes
+
+**Tasks**:
+- [ ] Document deployment URLs
+- [ ] Update environment variable documentation
+- [ ] Create troubleshooting guide
+- [ ] Document monitoring setup
+- [ ] Create rollback procedures
+
+### Checkpoint 6.2: Team Handover
+**Status**: ⏳ Pending
+**Estimated Time**: 30 minutes
+
+**Tasks**:
+- [ ] Share deployment credentials
+- [ ] Provide access to monitoring tools
+- [ ] Document common issues and solutions
+- [ ] Create maintenance schedule
+
+## Risk Mitigation
+
+### High-Risk Items
+- [ ] Database migration failures
+- [ ] Environment variable misconfiguration
+- [ ] CORS issues between services
+- [ ] Payment processing setup
+- [ ] Email service configuration
+
+### Rollback Plan
+1. **Backend Rollback**: Use Railway rollback feature
+2. **Storefront Rollback**: Use Vercel rollback feature
+3. **Database Rollback**: Restore from backup if needed
+4. **Environment Variables**: Revert to previous values
+
+## Success Criteria
+
+### Technical Success
+- [ ] All services deployed and accessible
+- [ ] Health checks passing
+- [ ] Database migrations completed
+- [ ] Admin user created and accessible
+- [ ] Storefront loading correctly
+- [ ] API endpoints responding
+
+### Business Success
+- [ ] Users can browse products
+- [ ] Cart functionality works
+- [ ] Checkout process completes
+- [ ] Admin can manage content
+- [ ] Email notifications sent
+- [ ] Payment processing works
+
+## Timeline Estimate
+
+- **Phase 1**: 1-2 hours
+- **Phase 2**: 2-3 hours
+- **Phase 3**: 1-2 hours
+- **Phase 4**: 1-2 hours
+- **Phase 5**: 2-3 hours
+- **Phase 6**: 1-2 hours
+
+**Total Estimated Time**: 8-14 hours
+
+## Notes and Decisions
+
+### Environment Variables
+- Use Railway template syntax for database URLs
+- Generate secure secrets for JWT and cookies
+- Configure CORS for production domains
+
+### Custom Modules
+- Menu module: ✅ Ready for production
+- Chef Event module: ✅ Ready for production
+- Resend module: ✅ Ready for production
+
+### Dependencies
+- All Medusa v2.7.0 packages aligned
+- React Router v7 configured
+- Tailwind CSS configured
+- TypeScript strict mode enabled
+
+## Next Steps
+
+1. **Start with Phase 1**: Update Medusa configuration
+2. **Set up Railway project**: Create database services
+3. **Deploy backend**: Server and worker modes
+4. **Deploy storefront**: Vercel configuration
+5. **Verify deployment**: End-to-end testing
+6. **Document everything**: Update team documentation
+
+---
+
+**Last Updated**: December 2024
+**Status**: Planning Phase
+**Next Review**: After Phase 1 completion
\ No newline at end of file
diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md
new file mode 100644
index 000000000..ca84f61f8
--- /dev/null
+++ b/docs/implementation-plan.md
@@ -0,0 +1,97 @@
+# Menu Admin CRUD Implementation Plan
+
+## Overview
+This document outlines the implementation plan for the Menu Admin CRUD functionality, including progress checkpoints and next steps.
+
+## Architecture
+
+### SDK Layer
+- [x] Base SDK structure following Medusa patterns
+ - Extended `Admin` class from `@medusajs/js-sdk`
+ - Proper resource organization
+ - Type-safe interfaces
+- [x] Admin Resources
+ - [x] `AdminMenusResource`
+ - [x] `AdminChefEventsResource`
+ - [x] Proper CRUD operations
+ - [x] Type definitions for DTOs
+
+### Data Layer
+- [x] React Query Integration
+ - [x] Query client setup
+ - [x] Basic hooks for menus
+ - [ ] Proper query invalidation
+ - [ ] Optimistic updates
+ - [ ] Error handling
+
+### UI Layer
+- [ ] Admin Components
+ - [ ] Data tables following product reviews pattern
+ - [ ] Drawer-based forms
+ - [ ] Proper loading states
+ - [ ] Error states
+- [ ] Form Handling
+ - [ ] Migration to `@lambdacurry/forms`
+ - [ ] Zod schema validation
+ - [ ] Proper error handling
+ - [ ] Form state management
+
+## Checkpoints
+
+### Checkpoint 1: SDK Structure (Current)
+- ✅ Extended base Medusa SDK
+- ✅ Proper resource organization
+- ✅ Type-safe interfaces
+- ✅ CRUD operations for menus and chef events
+
+### Checkpoint 2: Data Layer (Next)
+- [ ] React Query hooks for all operations
+- [ ] Proper caching strategy
+- [ ] Optimistic updates
+- [ ] Error handling
+
+### Checkpoint 3: UI Components
+- [ ] Data tables with sorting and filtering
+- [ ] Drawer-based forms
+- [ ] Loading and error states
+- [ ] Proper form validation
+
+### Checkpoint 4: Form Handling
+- [ ] Migration to `@lambdacurry/forms`
+- [ ] Zod schema validation
+- [ ] Error handling
+- [ ] Form state management
+
+## Next Steps
+
+1. **React Query Hooks Implementation**
+ - Create hooks for all CRUD operations
+ - Implement proper query invalidation
+ - Add optimistic updates
+ - Add error handling
+
+2. **Admin Components**
+ - Create data tables following product reviews pattern
+ - Implement drawer-based forms
+ - Add loading and error states
+ - Implement proper form validation
+
+3. **Form Handling**
+ - Migrate to `@lambdacurry/forms`
+ - Implement Zod schema validation
+ - Add proper error handling
+ - Implement form state management
+
+## Dependencies
+- `@medusajs/js-sdk`
+- `@medusajs/ui`
+- `@tanstack/react-query`
+- `@lambdacurry/forms`
+- `zod`
+- `react-hook-form`
+
+## References
+- Product Reviews Plugin Implementation
+- Medusa Admin UI Patterns
+- React Query Best Practices
+- Form Handling Best Practices
\ No newline at end of file
diff --git a/docs/menu-images-implementation-plan.md b/docs/menu-images-implementation-plan.md
new file mode 100644
index 000000000..7876a1bbd
--- /dev/null
+++ b/docs/menu-images-implementation-plan.md
@@ -0,0 +1,474 @@
+## Menu Images Implementation Plan (Medusa v2, ChefV)
+
+### Defaults and Scope
+
+- Multiple images per menu
+- Ordering persisted via rank; drag-and-drop in Admin UI
+- API payload uses images: string[] and optional thumbnail?: string | null
+- Update replaces entire image set (idempotent by order)
+- Admin and Store GETs return images (and thumbnail) by default
+- Store File Module file_id in metadata for clean deletions
+- Immediate upload on selection in Admin; immediate delete on remove in UI
+- Auto-thumbnail set to first image if not explicitly provided
+
+### Directory Map (key files)
+
+- Backend (module)
+ - `apps/medusa/src/modules/menu/models/menu.ts`
+ - `apps/medusa/src/modules/menu/models/menu-image.ts` (new)
+ - `apps/medusa/src/modules/menu/service.ts`
+ - `apps/medusa/src/modules/menu/migrations/MigrationYYYYMMDDHHMMSS_add_menu_images.ts` (new)
+ - `apps/medusa/src/modules/menu/migrations/MigrationYYYYMMDDHHMMSS_add_menu_thumbnail.ts` (new)
+- Backend (API)
+ - `apps/medusa/src/api/admin/menus/route.ts`
+ - `apps/medusa/src/api/admin/menus/[id]/route.ts` (add if needed)
+ - `apps/medusa/src/api/store/menus/route.ts`
+ - `apps/medusa/src/api/store/menus/[id]/route.ts` (ensure includes images)
+- SDK/Types
+ - `apps/medusa/src/sdk/admin/admin-menus.ts`
+ - `apps/medusa/src/sdk/store/store-menus.ts`
+- Admin UI
+ - `apps/medusa/src/admin/routes/menus/components/menu-media/MenuMedia.tsx` (new)
+ - Wire into `apps/medusa/src/admin/routes/menus/page.tsx` and edit pages
+ - Hooks: `apps/medusa/src/admin/hooks/menus.ts`
+
+### 1) Data Models and Migrations
+
+#### 1.1 MenuImage model
+
+Add a new model `menu_image` with snake_case columns and soft deletes.
+
+```ts
+// apps/medusa/src/modules/menu/models/menu-image.ts
+import { model } from "@medusajs/framework/utils"
+import { Menu } from "./menu"
+
+export const MenuImage = model.define("menu_image", {
+ id: model.id().primaryKey(),
+ menu_id: model.text(),
+ url: model.text(),
+ rank: model.number().default(0),
+ metadata: model.json().nullable(),
+ menu: model.belongsTo(() => Menu),
+}).indexes([
+ { on: ["menu_id"] },
+])
+```
+
+Extend `Menu` to include a one-to-many relation and optional `thumbnail`:
+
+```ts
+// apps/medusa/src/modules/menu/models/menu.ts
+import { model } from "@medusajs/framework/utils"
+import { Course } from "./course"
+import { MenuImage } from "./menu-image"
+
+export const Menu = model.define("menu", {
+ name: model.text(),
+ id: model.id().primaryKey(),
+ courses: model.hasMany(() => Course),
+ images: model.hasMany(() => MenuImage),
+ thumbnail: model.text().nullable(),
+}).cascades({
+ delete: ["courses", "images"],
+})
+```
+
+#### 1.2 Migrations (Mikro-ORM)
+
+Create `menu_image` table and add `thumbnail` to `menu`.
+
+```ts
+// apps/medusa/src/modules/menu/migrations/MigrationYYYYMMDDHHMMSS_add_menu_images.ts
+import { Migration } from '@mikro-orm/migrations'
+
+export class MigrationYYYYMMDDHHMMSS_add_menu_images extends Migration {
+ override async up(): Promise {
+ this.addSql(`create table if not exists "menu_image" (
+ "id" text not null,
+ "menu_id" text not null,
+ "url" text not null,
+ "rank" int not null default 0,
+ "metadata" jsonb null,
+ "created_at" timestamptz not null default now(),
+ "updated_at" timestamptz not null default now(),
+ "deleted_at" timestamptz null,
+ constraint "menu_image_pkey" primary key ("id")
+ );`)
+
+ this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_menu_image_menu_id" ON "menu_image" (menu_id) WHERE deleted_at IS NULL;`)
+ this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_menu_image_deleted_at" ON "menu_image" (deleted_at) WHERE deleted_at IS NULL;`)
+
+ this.addSql(`alter table if exists "menu_image" add constraint "menu_image_menu_id_foreign" foreign key ("menu_id") references "menu" ("id") on update cascade on delete cascade;`)
+ }
+
+ override async down(): Promise {
+ this.addSql(`drop table if exists "menu_image" cascade;`)
+ }
+}
+```
+
+```ts
+// apps/medusa/src/modules/menu/migrations/MigrationYYYYMMDDHHMMSS_add_menu_thumbnail.ts
+import { Migration } from '@mikro-orm/migrations'
+
+export class MigrationYYYYMMDDHHMMSS_add_menu_thumbnail extends Migration {
+ override async up(): Promise {
+ this.addSql(`alter table if exists "menu" add column if not exists "thumbnail" text null;`)
+ }
+
+ override async down(): Promise {
+ this.addSql(`alter table if exists "menu" drop column if exists "thumbnail";`)
+ }
+}
+```
+
+Notes
+- Keep snake_case, add partial indexes as above, and rely on FK cascade.
+- If enforcing a custom ID prefix is desired, we can add it later; `model.id()` is sufficient.
+
+### 2) Service Layer Integration
+
+`apps/medusa/src/modules/menu/service.ts`
+
+- Keep generated CRUD via `MedusaService` for `Menu`, `Course`, `Dish`, `Ingredient`, and register `MenuImage` as well.
+- Add custom methods for image set replacement and single-image deletion.
+
+Proposed signatures:
+
+```ts
+async replaceMenuImages(
+ menuId: string,
+ urls: string[],
+ opts?: { thumbnail?: string | null; fileMap?: Record }
+): Promise
+
+async deleteMenuImage(menuId: string, imageId: string): Promise
+```
+
+Behavior
+- replaceMenuImages
+ - Load existing `menu_image` rows for `menuId`.
+ - Compute removed vs next set (full replace is acceptable and simpler).
+ - Delete removed rows.
+ - Create new rows with `rank = index`, `url`, `menu_id`, and set `metadata.file_id = fileMap[url]` when provided.
+ - If `opts.thumbnail` provided, set it; else set to first image url or `null` if empty.
+- deleteMenuImage
+ - Validate ownership (`image.menu_id === menuId`).
+ - If `metadata.file_id` exists, resolve File Module and `deleteFiles([file_id])`.
+ - Soft delete the row.
+- Menu deletion
+ - Prior to deletion, fetch images, call `deleteFiles` for those with `file_id`, then delete menu (FK cascade removes rows).
+
+Transactions
+- Wrap replace operations in a single atomic transaction to keep images and thumbnail consistent.
+
+File Module
+- We do not upload binaries in the menu service (two-step flow). We only delete storage objects when removing images or deleting a menu.
+
+### 3) Admin API
+
+`apps/medusa/src/api/admin/menus/route.ts`
+
+Schemas (Zod):
+
+```ts
+const imageUrlsSchema = z.array(z.string().url()).optional().default([])
+const imageFilesSchema = z.array(z.object({
+ url: z.string().url(),
+ file_id: z.string().optional(),
+})).optional()
+
+const createMenuSchema = z.object({
+ name: z.string().min(1, "Menu name is required"),
+ courses: z.array(/* existing course schema */).optional().default([]),
+ images: imageUrlsSchema,
+ thumbnail: z.string().url().nullable().optional(),
+ image_files: imageFilesSchema,
+})
+```
+
+Handlers
+- GET /admin/menus
+ - Use `listAndCountMenus` with `relations: ["courses", "images"]`.
+ - Return `{ menus, count, offset, limit }` with `images` and `thumbnail` per menu.
+- POST /admin/menus
+ - Validate body via `createMenuSchema`.
+ - Run existing `createMenuWorkflow` to create base `menu`.
+ - After creation, call `replaceMenuImages(menu.id, images, { thumbnail, fileMap })`, where `fileMap` is built from `image_files` by url.
+ - Return created menu including `images` and `thumbnail`.
+
+`apps/medusa/src/api/admin/menus/[id]/route.ts` (add if needed)
+- GET: retrieve one menu including `images` and `thumbnail`.
+- POST: update menu fields; if `images` provided, call `replaceMenuImages` with ordering and `thumbnail`.
+- DELETE (optional single-image endpoint): `DELETE /admin/menus/:id/images/:imageId` invokes `deleteMenuImage`.
+
+Response shape
+- `images: Array<{ id: string; url: string; rank: number; created_at: string; updated_at: string }>`
+- `thumbnail?: string | null`
+
+### 4) Store API
+
+`apps/medusa/src/api/store/menus/route.ts`
+- Add `"images"` to relations: `relations: ["courses", "courses.dishes", "courses.dishes.ingredients", "images"]`.
+- Include `thumbnail` in the JSON response.
+- Keep existing cache headers (public, 30 minutes) as already implemented.
+
+`apps/medusa/src/api/store/menus/[id]/route.ts`
+- Ensure it returns `images` ordered by `rank` and `thumbnail`.
+
+### 5) SDK and Types
+
+`apps/medusa/src/sdk/admin/admin-menus.ts`
+
+```ts
+export interface AdminMenuImageDTO {
+ id: string
+ url: string
+ rank: number
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminMenuDTO {
+ id: string
+ name: string
+ courses: AdminCourseDTO[]
+ images: AdminMenuImageDTO[]
+ thumbnail?: string | null
+ created_at: string
+ updated_at: string
+}
+
+export interface AdminCreateMenuDTO {
+ name: string
+ courses?: Array<{
+ name: string
+ dishes: Array<{
+ name: string
+ description?: string
+ ingredients: Array<{ name: string; optional?: boolean }>
+ }>
+ }>
+ images?: string[]
+ thumbnail?: string | null
+ image_files?: { url: string; file_id?: string }[]
+}
+
+export interface AdminUpdateMenuDTO {
+ name?: string
+ courses?: Array<{
+ id?: string
+ name: string
+ dishes: Array<{
+ id?: string
+ name: string
+ description?: string
+ ingredients: Array<{ id?: string; name: string; optional?: boolean }>
+ }>
+ }>
+ images?: string[]
+ thumbnail?: string | null
+ image_files?: { url: string; file_id?: string }[]
+}
+```
+
+`apps/medusa/src/sdk/store/store-menus.ts`
+
+```ts
+export interface StoreMenuImageDTO { id: string; url: string; rank: number }
+
+export interface StoreMenuDTO {
+ id: string
+ name: string
+ courses: StoreCourseDTO[]
+ images: StoreMenuImageDTO[]
+ thumbnail?: string | null
+ created_at: string
+ updated_at: string
+}
+```
+
+No new SDK methods are necessary with the two-step upload flow. If you add a specific image-delete endpoint, consider a helper like `deleteImage(menuId, imageId)`.
+
+### 6) Workflows
+
+`apps/medusa/src/workflows/create-menu.ts`
+- Extend input type to include `images?: string[]`, `thumbnail?: string | null`, `image_files?: { url: string; file_id?: string }[]`.
+- After the base menu is created, add a step to call `replaceMenuImages(menu.id, images, { thumbnail, fileMap })`.
+- Return the final menu including `images` and `thumbnail`.
+
+If an update workflow exists, mirror the same steps there. For delete workflows, include a step to delete storage files for any images with `metadata.file_id` before removing the menu.
+
+### 7) Admin UI Implementation
+
+Component
+- `apps/medusa/src/admin/routes/menus/components/menu-media/MenuMedia.tsx` (new)
+ - Multi-file upload input + drag-and-drop area
+ - Previews for images
+ - Drag-and-drop reordering to set display order (rank)
+ - “Set as cover” action to set `thumbnail` (or derive from first item)
+ - Remove image button per item
+
+Upload behavior
+- Use Admin Uploads API via SDK:
+ - On selection, immediately upload: `sdk.admin.upload.create({ files: [file] })`
+ - Grab `{ url, id }` (id is `file_id`) and push into local state as `{ url, file_id }`
+ - Show progress and error feedback
+- On removing an uploaded-but-unsaved image, immediately call `sdk.admin.upload.delete(file_id)` and remove from state to prevent orphans.
+
+Submit behavior
+- On create/update page submit:
+ - Build ordered `images: string[]` from the current list
+ - Build `image_files: { url, file_id }[]` from local state
+ - Include `thumbnail?: string | null`
+ - Call `sdk.admin.menus.create` or `sdk.admin.menus.update`
+ - Invalidate queries using `useQueryClient` in `apps/medusa/src/admin/hooks/menus.ts`
+
+Accessibility & UX
+- Keyboard-friendly reorder controls and labels
+- Client-side file type/size validation with clear errors
+- “Cover” badge in the list for the current thumbnail
+
+### 8) Deletion Semantics
+
+Single image removal (saved menus)
+- Preferred: send an update with the filtered `images` array (full replace). Backend detects removed rows and deletes storage objects for any with `metadata.file_id`.
+- Optional: dedicated endpoint `DELETE /admin/menus/:id/images/:imageId` calling `deleteMenuImage`.
+
+Menu deletion
+- Fetch images; for each with `metadata.file_id`, call File Module `deleteFiles([file_id])`.
+- Delete the menu (FK cascade removes `menu_image` rows).
+
+Missing file_id
+- If some URLs lack `file_id` (e.g., legacy), skip storage deletion. The admin upload flow minimizes this by deleting uploads immediately for removed pre-save items.
+
+### 9) Validation and Error Handling
+
+Admin Zod validation
+- `images` and `thumbnail` must be valid absolute URLs.
+- `image_files` mapping is optional; ignore unknown URLs; log mismatches.
+
+Service errors
+- 404 for missing menu/image, 400 for bad inputs, 500 for unexpected.
+- Transactional guarantees on replace to avoid partial updates.
+
+API errors
+- Use structured JSON with `message` and optional `errors`.
+
+### 10) Performance and Indexing
+
+- Use partial indexes on `deleted_at` for all tables.
+- Index `menu_image.menu_id` and consider composite `(menu_id, rank)` for ordered retrieval.
+- Admin/store listing supports `limit/offset`; keep existing pagination structure.
+
+### 11) Testing Plan
+
+Integration (Admin)
+- Create with images: verify count, rank ordering, thumbnail default.
+- Update with replacement set: verify old images removed, new ranks applied, thumbnail updated.
+- Delete single image: row removed and `deleteFiles` invoked when `file_id` exists (mock File Module).
+- Delete menu: `deleteFiles` invoked for each image with `file_id` and rows removed by cascade.
+
+Integration (Store)
+- GET /store/menus: images ordered by `rank`, includes `thumbnail`.
+
+UI tests
+- Upload success path: previews render, URL and `file_id` captured.
+- Remove pre-save: calls upload delete, state updated.
+- Reorder reflects in submit payload ordering.
+- Set cover toggles the expected thumbnail value.
+
+### 12) Operations and Configuration
+
+- Development uses Local File Provider (uploads folder). Backend must serve uploaded files.
+- Production can switch to S3 provider via File Module configuration; no code changes required.
+- Document recommended image size and file type constraints; enforce on client.
+
+### 13) Rollout Checklist
+
+1) Add `menu-image.ts` model and extend `menu.ts` with `images` and `thumbnail`.
+2) Create and run migrations for `menu_image` and `menu.thumbnail`.
+3) Register `MenuImage` in the module service context if needed.
+4) Implement service helpers: `replaceMenuImages`, `deleteMenuImage` (+ File Module delete).
+5) Update Admin routes:
+ - Extend schemas to accept `images`, `thumbnail`, `image_files`.
+ - Include `images` in relations on GET/list.
+ - Replace images on create/update.
+6) Update Store routes to include `images` and `thumbnail`.
+7) Update SDK DTOs for Admin and Store to include `images` and `thumbnail`.
+8) Implement `MenuMedia.tsx` with upload, reorder, remove, and cover selection.
+9) Add tests (admin/store integration + UI where applicable).
+10) Verify end-to-end manually:
+ - Upload → attach → reorder → set cover → save → fetch → delete image → delete menu.
+
+### Progress Checkpoints
+
+- Models & Migrations
+ - [ ] Create `apps/medusa/src/modules/menu/models/menu-image.ts`
+ - [ ] Update `apps/medusa/src/modules/menu/models/menu.ts` with `images` and `thumbnail`
+ - [ ] Add migration `MigrationYYYYMMDDHHMMSS_add_menu_images` (created and committed)
+ - [ ] Add migration `MigrationYYYYMMDDHHMMSS_add_menu_thumbnail` (created and committed)
+ - [ ] Run migrations locally (DB updated)
+
+- Service Layer
+ - [ ] Register `MenuImage` in service factory context (if needed)
+ - [ ] Implement `replaceMenuImages(menuId, urls, { thumbnail, fileMap })`
+ - [ ] Implement `deleteMenuImage(menuId, imageId)` with File Module deletion
+ - [ ] Ensure operations are transactional
+
+- Admin API
+ - [ ] Extend Zod schema to accept `images`, `thumbnail`, `image_files`
+ - [ ] GET /admin/menus includes `images` relation and `thumbnail`
+ - [ ] POST /admin/menus attaches images and sets `thumbnail`
+ - [ ] /admin/menus/:id GET/POST implemented for retrieve/update with images
+ - [ ] (Optional) DELETE /admin/menus/:id/images/:imageId implemented
+
+- Store API
+ - [ ] GET /store/menus includes `images` and `thumbnail`
+ - [ ] GET /store/menus/:id returns `images` ordered by `rank` and `thumbnail`
+
+- SDK & Hooks
+ - [ ] Update Admin DTOs to include `images` and `thumbnail`
+ - [ ] Update Store DTOs to include `images` and `thumbnail`
+ - [ ] Adjust admin hooks payload types for `images`, `thumbnail`, `image_files`
+
+- Admin UI
+ - [ ] Create `MenuMedia.tsx` with multi-upload and previews
+ - [ ] Upload on select using `sdk.admin.upload.create`
+ - [ ] Immediate delete on remove using upload delete API
+ - [ ] Drag-and-drop reordering reflected in payload order
+ - [ ] “Set as cover” toggles `thumbnail`
+ - [ ] Wire into create/edit menu forms and submission
+
+- Deletion Semantics
+ - [ ] File Module deletes called on image removal when `file_id` exists
+ - [ ] Menu deletion cleans up files for all attached images with `file_id`
+
+- Testing
+ - [ ] Admin integration: create, update (replace), delete image, delete menu
+ - [ ] Store integration: lists/gets include images and thumbnail in order
+ - [ ] UI: upload, remove pre-save, reorder, set cover
+
+- Operations
+ - [ ] Developer docs updated (image size/type guidance)
+ - [ ] Production storage provider configured (e.g., S3) if applicable
+
+- Tracking
+ - Implementation owner(s): ______________________
+ - Start date: __________ Target completion: __________
+ - Related PRs:
+ - [ ] Admin/Module: PR #____ — models, migrations, service
+ - [ ] Admin API: PR #____ — routes and schemas
+ - [ ] Store API: PR #____ — routes
+ - [ ] SDK/Types: PR #____ — DTO updates
+ - [ ] Admin UI: PR #____ — Menu media component and wiring
+ - Deployments:
+ - [ ] Dev
+ - [ ] Staging
+ - [ ] Production
+
+---
+
+This plan follows Medusa v2 patterns (modules, DML models, service factory, Admin/Store routes, and File Module integration), uses snake_case DB naming, and aligns with the current codebase structure in `apps/medusa` and the custom Admin UI.
+
diff --git a/docs/resend-chef-event-templates.md b/docs/resend-chef-event-templates.md
new file mode 100644
index 000000000..9c170052e
--- /dev/null
+++ b/docs/resend-chef-event-templates.md
@@ -0,0 +1,944 @@
+# Chef Event Email Templates for Resend
+
+This document provides detailed email templates for the chef event system using Resend and React Email components.
+
+## Template Structure
+
+All templates follow a consistent structure:
+- Header with branding
+- Main content section
+- Action buttons/links
+- Footer with contact information
+
+## 1. Chef Event Requested Template
+
+### File: `src/modules/resend/emails/chef-event-requested.tsx`
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Link,
+ Button,
+} from "@react-email/components"
+
+type ChefEventRequestedEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ menu: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ event: {
+ status: string
+ total_price: string
+ conflict: boolean
+ }
+ requestReference: string
+ chefContact: {
+ email: string
+ phone: string
+ }
+ emailType: "customer_confirmation" | "chef_notification"
+}
+
+function ChefEventRequestedEmailComponent({
+ customer,
+ booking,
+ event,
+ requestReference,
+ chefContact,
+ emailType
+}: ChefEventRequestedEmailProps) {
+ const isCustomerEmail = emailType === "customer_confirmation"
+
+ return (
+
+
+
+
+ {isCustomerEmail
+ ? "Your chef event request has been received"
+ : "New chef event request received"
+ }
+
+
+ {/* Header */}
+
+
+
+
+
+ 🍳 Chef Elena Rodriguez
+
+
+ Private Chef & Culinary Experiences
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {isCustomerEmail ? (
+ <>
+
+ Thank you for your event request!
+
+
+ Hi {customer.first_name}, we've received your request for a private chef experience.
+ We'll review your details and get back to you within 24-48 hours.
+
+ >
+ ) : (
+ <>
+
+ New Event Request Received
+
+
+ You have a new chef event request that requires your review.
+
+ >
+ )}
+
+ {/* Event Details */}
+
+
+ Event Details
+
+
+
+
+ Date & Time
+
+
+ {booking.date} at {booking.time}
+
+
+
+
+
+ Event Type
+
+
+ {booking.event_type}
+
+
+
+
+
+ Party Size
+
+
+ {booking.party_size} guests
+
+
+
+
+
+ Location
+
+
+
+ {booking.location_type} - {booking.location_address}
+
+
+
+
+
+
+ Menu
+
+
+ {booking.menu}
+
+
+
+ {booking.notes && (
+
+
+ Special Notes
+
+
+ {booking.notes}
+
+
+ )}
+
+
+
+ Total Price
+
+
+ ${event.total_price}
+
+
+
+
+ {/* Customer Information */}
+
+
+ Customer Information
+
+
+
+
+ Name: {customer.first_name} {customer.last_name}
+
+
+
+
+
+ Email: {customer.email}
+
+
+
+
+
+ Phone: {customer.phone}
+
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+
+ {/* Action Buttons */}
+ {!isCustomerEmail && (
+
+
+
+
+ Accept Request
+
+
+ Decline Request
+
+
+
+
+ )}
+
+ {/* Next Steps */}
+ {isCustomerEmail && (
+
+
+ What happens next?
+
+
+ 1. We'll review your request within 24-48 hours
+
+
+ 2. You'll receive an email with our decision
+
+
+ 3. If accepted, you'll get a secure payment link
+
+
+ 4. We'll confirm all details before your event
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+
+ Questions? Contact us at {chefContact.email} or {chefContact.phone}
+
+
+ © {new Date().getFullYear()} Chef Elena Rodriguez. All rights reserved.
+
+
+
+
+
+
+
+
+ )
+}
+
+export const chefEventRequestedEmail = (props: ChefEventRequestedEmailProps) => (
+
+)
+```
+
+## 2. Chef Event Accepted Template
+
+### File: `src/modules/resend/emails/chef-event-accepted.tsx`
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Link,
+ Button,
+} from "@react-email/components"
+
+type ChefEventAcceptedEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ event: {
+ status: string
+ total_price: string
+ price_per_person: string
+ }
+ product: {
+ id: string
+ handle: string
+ title: string
+ purchase_url: string
+ }
+ chef: {
+ name: string
+ email: string
+ phone: string
+ }
+ requestReference: string
+ acceptanceDate: string
+ chefNotes: string
+ emailType: "customer_acceptance"
+}
+
+function ChefEventAcceptedEmailComponent({
+ customer,
+ booking,
+ event,
+ product,
+ chef,
+ requestReference,
+ acceptanceDate,
+ chefNotes
+}: ChefEventAcceptedEmailProps) {
+
+ return (
+
+
+
+ Great news! Your chef event has been accepted
+
+ {/* Header */}
+
+
+
+
+
+ 🎉 Event Accepted!
+
+
+ Your chef event request has been confirmed
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ Congratulations, {customer.first_name}!
+
+
+ We're excited to confirm your chef event! Your request has been accepted and we're looking forward to creating an amazing culinary experience for you.
+
+
+ {/* Chef Message */}
+
+
+ Message from Chef Elena
+
+
+ "{chefNotes}"
+
+
+
+ {/* Event Details */}
+
+
+ Confirmed Event Details
+
+
+
+
+ Date & Time
+
+
+ {booking.date} at {booking.time}
+
+
+
+
+
+ Event Type
+
+
+ {booking.event_type}
+
+
+
+
+
+ Party Size
+
+
+ {booking.party_size} guests
+
+
+
+
+
+ Location
+
+
+
+ {booking.location_type} - {booking.location_address}
+
+
+
+
+
+
+ Price per Person
+
+
+ ${event.price_per_person}
+
+
+
+
+
+ Total Price
+
+
+ ${event.total_price}
+
+
+
+
+ {/* Payment Section */}
+
+
+ Complete Your Booking
+
+
+ To secure your event, please complete your payment using the link below.
+ Your booking will be confirmed once payment is received.
+
+
+
+
+
+ Pay Now - ${event.total_price}
+
+
+
+
+
+ Secure payment powered by Stripe
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+ Accepted on: {acceptanceDate}
+
+
+
+ {/* Important Information */}
+
+
+ Important Information
+
+
+ • Payment is required within 48 hours to secure your booking
+
+
+ • Cancellations must be made at least 72 hours before the event
+
+
+ • Chef Elena will contact you 24 hours before the event
+
+
+ • Please ensure your kitchen is clean and ready for the chef
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ Questions? Contact Chef Elena at {chef.email} or {chef.phone}
+
+
+ © {new Date().getFullYear()} Chef Elena Rodriguez. All rights reserved.
+
+
+
+
+
+
+
+
+ )
+}
+
+export const chefEventAcceptedEmail = (props: ChefEventAcceptedEmailProps) => (
+
+)
+```
+
+## 3. Chef Event Rejected Template
+
+### File: `src/modules/resend/emails/chef-event-rejected.tsx`
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Link,
+ Button,
+} from "@react-email/components"
+
+type ChefEventRejectedEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ rejection: {
+ reason: string
+ chefNotes: string
+ }
+ chef: {
+ name: string
+ email: string
+ phone: string
+ }
+ requestReference: string
+ rejectionDate: string
+ emailType: "customer_rejection"
+}
+
+function ChefEventRejectedEmailComponent({
+ customer,
+ booking,
+ rejection,
+ chef,
+ requestReference,
+ rejectionDate
+}: ChefEventRejectedEmailProps) {
+
+ return (
+
+
+
+ Update regarding your chef event request
+
+ {/* Header */}
+
+
+
+
+
+ Event Request Update
+
+
+ Important information about your booking
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ Hi {customer.first_name},
+
+
+ Thank you for your interest in our chef services. After careful consideration,
+ we regret to inform you that we are unable to accommodate your event request at this time.
+
+
+ {/* Rejection Details */}
+
+
+ Request Details
+
+
+
+
+ Date & Time
+
+
+ {booking.date} at {booking.time}
+
+
+
+
+
+ Event Type
+
+
+ {booking.event_type}
+
+
+
+
+
+ Party Size
+
+
+ {booking.party_size} guests
+
+
+
+
+
+ Location
+
+
+
+ {booking.location_type} - {booking.location_address}
+
+
+
+
+
+ {/* Rejection Reason */}
+
+
+ Reason for Decline
+
+
+ {rejection.reason}
+
+
+ {rejection.chefNotes && (
+ <>
+
+ Additional Notes from Chef Elena:
+
+
+ "{rejection.chefNotes}"
+
+ >
+ )}
+
+
+ {/* Alternative Options */}
+
+
+ Alternative Options
+
+
+ We'd love to work with you in the future. Here are some alternatives:
+
+
+
+
+ • Consider a different date or time
+
+
+
+
+
+ • Explore our other event types
+
+
+
+
+
+ • Check our availability for smaller groups
+
+
+
+
+
+ • Contact us for custom arrangements
+
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+ Updated on: {rejectionDate}
+
+
+
+ {/* Contact Information */}
+
+
+ Get in Touch
+
+
+ We're here to help you plan the perfect event. Feel free to reach out with any questions or to discuss alternative arrangements.
+
+
+
+
+ Email: {chef.email}
+
+
+
+
+
+ Phone: {chef.phone}
+
+
+
+
+
+ Website: www.chefelenar.com
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ Thank you for considering Chef Elena Rodriguez for your event
+
+
+ © {new Date().getFullYear()} Chef Elena Rodriguez. All rights reserved.
+
+
+
+
+
+
+
+
+ )
+}
+
+export const chefEventRejectedEmail = (props: ChefEventRejectedEmailProps) => (
+
+)
+```
+
+## Template Usage
+
+### In Subscribers
+
+Update your subscribers to use the new template names:
+
+```typescript
+// In chef-event-requested.ts
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-requested",
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ event: { /* event data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ chefContact: { /* chef contact info */ },
+ emailType: "customer_confirmation"
+ }
+} as CreateNotificationDTO)
+
+// In chef-event-accepted.ts
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-accepted",
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ event: { /* event data */ },
+ product: { /* product data */ },
+ chef: { /* chef data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ acceptanceDate: DateTime.now().toFormat('LLL d, yyyy'),
+ chefNotes: chefEvent.chefNotes || "Looking forward to creating an amazing experience for you!",
+ emailType: "customer_acceptance"
+ }
+} as CreateNotificationDTO)
+
+// In chef-event-rejected.ts
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-rejected",
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ rejection: { /* rejection data */ },
+ chef: { /* chef data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ rejectionDate: DateTime.now().toFormat('LLL d, yyyy'),
+ emailType: "customer_rejection"
+ }
+} as CreateNotificationDTO)
+```
+
+## Testing Templates
+
+### Development Testing
+
+1. Install React Email CLI:
+```bash
+npm install -D react-email
+```
+
+2. Add test data to each template file:
+```typescript
+// Add at the end of each template file
+const mockData = {
+ customer: {
+ first_name: "John",
+ last_name: "Doe",
+ email: "john@example.com",
+ phone: "(555) 123-4567"
+ },
+ // ... other mock data
+}
+
+export default () =>
+```
+
+3. Test templates locally:
+```bash
+npm run dev:email
+```
+
+### Production Testing
+
+1. Send test emails through your application
+2. Verify email delivery in Resend dashboard
+3. Check email rendering across different email clients
+4. Test with real customer data
+
+## Customization
+
+### Branding
+
+Update colors, fonts, and styling to match your brand:
+
+```typescript
+// Update gradient colors
+className="bg-gradient-to-r from-your-primary to-your-secondary"
+
+// Update logo and branding
+
+ Your Brand Name
+
+```
+
+### Content
+
+Customize email content for your specific needs:
+
+- Update chef contact information
+- Modify pricing structure
+- Add custom terms and conditions
+- Include additional branding elements
+
+### Styling
+
+Use Tailwind classes to customize the appearance:
+
+```typescript
+// Custom button styling
+className="bg-your-brand-color text-white px-6 py-3 rounded-lg"
+
+// Custom section backgrounds
+className="bg-your-accent-color rounded-lg p-6"
+```
+
+These templates provide a complete email notification system for your chef event management system using Resend and React Email components.
\ No newline at end of file
diff --git a/docs/resend-integration-guide.md b/docs/resend-integration-guide.md
new file mode 100644
index 000000000..0ad2a7608
--- /dev/null
+++ b/docs/resend-integration-guide.md
@@ -0,0 +1,959 @@
+# Integrate Medusa with Resend (Email Notifications)
+
+## Overview
+
+This guide will help you integrate Medusa with Resend for email notifications, replacing the current SendGrid implementation. Resend provides a modern, developer-friendly email service with excellent deliverability and React-based email templates.
+
+## Prerequisites
+
+- Node.js v20+
+- Medusa v2 application (already set up)
+- Resend account (create at [resend.com](https://resend.com))
+
+## Step 1: Install Resend Dependencies
+
+```bash
+cd apps/medusa
+yarn add resend @react-email/components
+yarn add -D react-email
+```
+
+## Step 2: Create Resend Module Provider
+
+### Create Module Directory Structure
+
+```
+src/modules/resend/
+├── index.ts
+├── service.ts
+└── emails/
+ ├── order-placed.tsx
+ ├── chef-event-requested.tsx
+ ├── chef-event-accepted.tsx
+ └── chef-event-rejected.tsx
+```
+
+### Create Service (`src/modules/resend/service.ts`)
+
+```typescript
+import {
+ AbstractNotificationProviderService,
+ MedusaError,
+} from "@medusajs/framework/utils"
+import {
+ Logger,
+ ProviderSendNotificationDTO,
+ ProviderSendNotificationResultsDTO,
+} from "@medusajs/framework/types"
+import { Resend, CreateEmailOptions } from "resend"
+
+type ResendOptions = {
+ api_key: string
+ from: string
+ html_templates?: Record
+}
+
+type InjectedDependencies = {
+ logger: Logger
+}
+
+enum Templates {
+ ORDER_PLACED = "order-placed",
+ CHEF_EVENT_REQUESTED = "chef-event-requested",
+ CHEF_EVENT_ACCEPTED = "chef-event-accepted",
+ CHEF_EVENT_REJECTED = "chef-event-rejected",
+}
+
+const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
+ [Templates.ORDER_PLACED]: orderPlacedEmail,
+ [Templates.CHEF_EVENT_REQUESTED]: chefEventRequestedEmail,
+ [Templates.CHEF_EVENT_ACCEPTED]: chefEventAcceptedEmail,
+ [Templates.CHEF_EVENT_REJECTED]: chefEventRejectedEmail,
+}
+
+class ResendNotificationProviderService extends AbstractNotificationProviderService {
+ static identifier = "notification-resend"
+ private resendClient: Resend
+ private options: ResendOptions
+ private logger: Logger
+
+ constructor(
+ { logger }: InjectedDependencies,
+ options: ResendOptions
+ ) {
+ super()
+ this.resendClient = new Resend(options.api_key)
+ this.options = options
+ this.logger = logger
+ }
+
+ static validateOptions(options: Record) {
+ if (!options.api_key) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Option `api_key` is required in the provider's options."
+ )
+ }
+ if (!options.from) {
+ throw new MedusaError(
+ MedusaError.Types.INVALID_DATA,
+ "Option `from` is required in the provider's options."
+ )
+ }
+ }
+
+ getTemplate(template: Templates) {
+ if (this.options.html_templates?.[template]) {
+ return this.options.html_templates[template].content
+ }
+ const allowedTemplates = Object.keys(templates)
+
+ if (!allowedTemplates.includes(template)) {
+ return null
+ }
+
+ return templates[template]
+ }
+
+ getTemplateSubject(template: Templates) {
+ if (this.options.html_templates?.[template]?.subject) {
+ return this.options.html_templates[template].subject
+ }
+ switch(template) {
+ case Templates.ORDER_PLACED:
+ return "Order Confirmation"
+ case Templates.CHEF_EVENT_REQUESTED:
+ return "Chef Event Request Confirmation"
+ case Templates.CHEF_EVENT_ACCEPTED:
+ return "Booking Confirmed! 🎉"
+ case Templates.CHEF_EVENT_REJECTED:
+ return "Chef Event Update"
+ default:
+ return "New Email"
+ }
+ }
+
+ async send(
+ notification: ProviderSendNotificationDTO
+ ): Promise {
+ const template = this.getTemplate(notification.template as Templates)
+
+ if (!template) {
+ this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`)
+ return {}
+ }
+
+ const commonOptions = {
+ from: this.options.from,
+ to: [notification.to],
+ subject: this.getTemplateSubject(notification.template as Templates),
+ }
+
+ let emailOptions: CreateEmailOptions
+ if (typeof template === "string") {
+ emailOptions = {
+ ...commonOptions,
+ html: template,
+ }
+ } else {
+ emailOptions = {
+ ...commonOptions,
+ react: template(notification.data),
+ }
+ }
+
+ const { data, error } = await this.resendClient.emails.send(emailOptions)
+
+ if (error || !data) {
+ if (error) {
+ this.logger.error("Failed to send email", error)
+ } else {
+ this.logger.error("Failed to send email: unknown error")
+ }
+ return {}
+ }
+
+ return { id: data.id }
+ }
+}
+
+export default ResendNotificationProviderService
+```
+
+### Create Module Export (`src/modules/resend/index.ts`)
+
+```typescript
+import {
+ ModuleProvider,
+ Modules,
+} from "@medusajs/framework/utils"
+import ResendNotificationProviderService from "./service"
+
+export default ModuleProvider(Modules.NOTIFICATION, {
+ services: [ResendNotificationProviderService],
+})
+```
+
+## Step 3: Create Enhanced Email Templates
+
+### Chef Event Requested Template (`src/modules/resend/emails/chef-event-requested.tsx`)
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Link,
+ Button,
+} from "@react-email/components"
+
+type ChefEventRequestedEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ menu: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ event: {
+ status: string
+ total_price: string
+ conflict: boolean
+ }
+ requestReference: string
+ chefContact: {
+ email: string
+ phone: string
+ }
+ emailType: "customer_confirmation" | "chef_notification"
+}
+
+function ChefEventRequestedEmailComponent({
+ customer,
+ booking,
+ event,
+ requestReference,
+ chefContact,
+ emailType
+}: ChefEventRequestedEmailProps) {
+ const isCustomerEmail = emailType === "customer_confirmation"
+
+ return (
+
+
+
+
+ {isCustomerEmail
+ ? "Your chef event request has been received"
+ : "New chef event request received"
+ }
+
+
+ {/* Header */}
+
+
+
+
+
+ 🍳 Chef Elena Rodriguez
+
+
+ Private Chef & Culinary Experiences
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {isCustomerEmail ? (
+ <>
+
+ Thank you for your event request!
+
+
+ Hi {customer.first_name}, we've received your request for a private chef experience.
+ We'll review your details and get back to you within 24-48 hours.
+
+ >
+ ) : (
+ <>
+
+ New Event Request Received
+
+
+ You have a new chef event request that requires your review.
+
+ >
+ )}
+
+ {/* Event Details */}
+
+
+ Event Details
+
+
+
+
+ Date & Time
+
+
+ {booking.date} at {booking.time}
+
+
+
+
+
+ Event Type
+
+
+ {booking.event_type}
+
+
+
+
+
+ Party Size
+
+
+ {booking.party_size} guests
+
+
+
+
+
+ Location
+
+
+
+ {booking.location_type} - {booking.location_address}
+
+
+
+
+
+
+ Menu
+
+
+ {booking.menu}
+
+
+
+ {booking.notes && (
+
+
+ Special Notes
+
+
+ {booking.notes}
+
+
+ )}
+
+
+
+ Total Price
+
+
+ ${event.total_price}
+
+
+
+
+ {/* Customer Information */}
+
+
+ Customer Information
+
+
+
+
+ Name: {customer.first_name} {customer.last_name}
+
+
+
+
+
+ Email: {customer.email}
+
+
+
+
+
+ Phone: {customer.phone}
+
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+
+ {/* Action Buttons for Chef */}
+ {!isCustomerEmail && (
+
+
+
+
+ Accept Request
+
+
+ Decline Request
+
+
+
+
+ )}
+
+ {/* Next Steps for Customer */}
+ {isCustomerEmail && (
+
+
+ What happens next?
+
+
+ 1. We'll review your request within 24-48 hours
+
+
+ 2. You'll receive an email with our decision
+
+
+ 3. If accepted, you'll get a secure payment link
+
+
+ 4. We'll confirm all details before your event
+
+
+ )}
+
+
+ {/* Footer */}
+
+
+
+
+
+ Questions? Contact us at {chefContact.email} or {chefContact.phone}
+
+
+ © {new Date().getFullYear()} Chef Elena Rodriguez. All rights reserved.
+
+
+
+
+
+
+
+
+ )
+}
+
+export const chefEventRequestedEmail = (props: ChefEventRequestedEmailProps) => (
+
+)
+```
+
+### Chef Event Accepted Template (`src/modules/resend/emails/chef-event-accepted.tsx`)
+
+```typescript
+import {
+ Text,
+ Column,
+ Container,
+ Heading,
+ Html,
+ Row,
+ Section,
+ Tailwind,
+ Head,
+ Preview,
+ Body,
+ Link,
+ Button,
+} from "@react-email/components"
+
+type ChefEventAcceptedEmailProps = {
+ customer: {
+ first_name: string
+ last_name: string
+ email: string
+ phone: string
+ }
+ booking: {
+ date: string
+ time: string
+ event_type: string
+ location_type: string
+ location_address: string
+ party_size: number
+ notes: string
+ }
+ event: {
+ status: string
+ total_price: string
+ price_per_person: string
+ }
+ product: {
+ id: string
+ handle: string
+ title: string
+ purchase_url: string
+ }
+ chef: {
+ name: string
+ email: string
+ phone: string
+ }
+ requestReference: string
+ acceptanceDate: string
+ chefNotes: string
+ emailType: "customer_acceptance"
+}
+
+function ChefEventAcceptedEmailComponent({
+ customer,
+ booking,
+ event,
+ product,
+ chef,
+ requestReference,
+ acceptanceDate,
+ chefNotes
+}: ChefEventAcceptedEmailProps) {
+
+ return (
+
+
+
+ Great news! Your chef event has been accepted
+
+ {/* Header */}
+
+
+
+
+
+ 🎉 Booking Confirmed!
+
+
+ Your chef event request has been confirmed
+
+
+
+
+
+
+ {/* Main Content */}
+
+
+ Great News!
+
+
+ Dear {customer.first_name},
+
+
+ Your chef has confirmed your booking! Here are your event details:
+
+
+ {/* Event Details */}
+
+
+ Event Details
+
+
+
+
+ Date:
+
+
+ {booking.date}
+
+
+
+
+
+ Time:
+
+
+ {booking.time}
+
+
+
+
+
+ Menu:
+
+
+ {booking.menu}
+
+
+
+
+
+ Event Type:
+
+
+ {booking.event_type}
+
+
+
+
+
+ Location:
+
+
+ {booking.location_type}
+
+
+
+
+
+ Address:
+
+
+ {booking.location_address}
+
+
+
+
+
+ Party Size:
+
+
+ {booking.party_size} guests
+
+
+
+
+ {/* Payment Details */}
+
+
+ Payment Details
+
+
+
+
+ Total Price:
+
+
+ ${event.total_price}
+
+
+
+
+
+ Deposit Required:
+
+
+ ${event.total_price}
+
+
+
+
+ To secure your booking, please pay the deposit within the next 24 hours.
+
+
+
+ {/* Payment Button */}
+
+
+ {/* What's Next */}
+
+
+ What's Next?
+
+
+ 1. Pay your deposit to secure the booking
+
+
+ 2. Our chef will contact you to discuss menu details
+
+
+ 3. We'll send you a reminder 48 hours before the event
+
+
+
+ {/* Reference Number */}
+
+
+ Reference: {requestReference}
+
+
+ Accepted on: {acceptanceDate}
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ If you have any questions, please don't hesitate to contact us at {chef.email}
+
+
+ © {new Date().getFullYear()} Chef Elena Rodriguez. All rights reserved.
+
+
+
+
+
+
+
+
+ )
+}
+
+export const chefEventAcceptedEmail = (props: ChefEventAcceptedEmailProps) => (
+
+)
+```
+
+## Step 4: Update Configuration
+
+### Update `medusa-config.ts`
+
+Replace the SendGrid configuration with Resend:
+
+```typescript
+const notificationModule = {
+ resolve: "@medusajs/medusa/notification",
+ options: {
+ providers: [
+ {
+ resolve: "./src/modules/resend",
+ id: "resend",
+ options: {
+ channels: ["email"],
+ api_key: process.env.RESEND_API_KEY,
+ from: process.env.RESEND_FROM_EMAIL,
+ },
+ },
+ ],
+ },
+};
+```
+
+### Update Environment Variables
+
+Add to your `.env` file:
+
+```bash
+RESEND_API_KEY=your_resend_api_key_here
+RESEND_FROM_EMAIL=onboarding@resend.dev
+```
+
+## Step 5: Update Email Templates in Service
+
+Update the templates import in `src/modules/resend/service.ts`:
+
+```typescript
+import { orderPlacedEmail } from "./emails/order-placed"
+import { chefEventRequestedEmail } from "./emails/chef-event-requested"
+import { chefEventAcceptedEmail } from "./emails/chef-event-accepted"
+import { chefEventRejectedEmail } from "./emails/chef-event-rejected"
+
+const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {
+ [Templates.ORDER_PLACED]: orderPlacedEmail,
+ [Templates.CHEF_EVENT_REQUESTED]: chefEventRequestedEmail,
+ [Templates.CHEF_EVENT_ACCEPTED]: chefEventAcceptedEmail,
+ [Templates.CHEF_EVENT_REJECTED]: chefEventRejectedEmail,
+}
+```
+
+## Step 6: Update Subscribers
+
+Update your existing subscribers to use the new template names:
+
+### Update `src/subscribers/chef-event-requested.ts`
+
+```typescript
+// Send confirmation email to customer
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-requested", // Updated template name
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ event: { /* event data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ chefContact: { /* chef contact info */ },
+ emailType: "customer_confirmation"
+ }
+} as CreateNotificationDTO)
+```
+
+### Update `src/subscribers/chef-event-accepted.ts`
+
+```typescript
+// Send acceptance email to customer
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-accepted", // Updated template name
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ event: { /* event data */ },
+ product: { /* product data */ },
+ chef: { /* chef data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ acceptanceDate: DateTime.now().toFormat('LLL d, yyyy'),
+ chefNotes: chefEvent.chefNotes || "Looking forward to creating an amazing experience for you!",
+ emailType: "customer_acceptance"
+ }
+} as CreateNotificationDTO)
+```
+
+### Update `src/subscribers/chef-event-rejected.ts`
+
+```typescript
+// Send rejection email to customer
+await notificationService.createNotifications({
+ to: chefEvent.email,
+ channel: "email",
+ template: "chef-event-rejected", // Updated template name
+ data: {
+ customer: { /* customer data */ },
+ booking: { /* booking data */ },
+ rejection: { /* rejection data */ },
+ chef: { /* chef data */ },
+ requestReference: chefEvent.id.slice(0, 8).toUpperCase(),
+ rejectionDate: DateTime.now().toFormat('LLL d, yyyy'),
+ emailType: "customer_rejection"
+ }
+} as CreateNotificationDTO)
+```
+
+## Step 7: Test Email Templates
+
+Add a development script to test email templates:
+
+```json
+// package.json
+{
+ "scripts": {
+ "dev:email": "email dev --dir ./src/modules/resend/emails"
+ }
+}
+```
+
+Test your templates:
+
+```bash
+yarn dev:email
+```
+
+## Step 8: Migration Checklist
+
+### Before Migration
+- [ ] Backup current SendGrid configuration
+- [ ] Set up Resend account and domain
+- [ ] Get Resend API key
+- [ ] Test Resend with development domain
+
+### During Migration
+- [ ] Install Resend dependencies with yarn
+- [ ] Create Resend module provider
+- [ ] Create enhanced email templates
+- [ ] Update configuration
+- [ ] Update environment variables
+- [ ] Update subscribers
+
+### After Migration
+- [ ] Test order confirmation emails
+- [ ] Test chef event notification emails
+- [ ] Verify email delivery
+- [ ] Monitor email analytics in Resend dashboard
+- [ ] Remove SendGrid dependencies
+
+## Step 9: Environment Setup
+
+### Development Environment
+```bash
+RESEND_API_KEY=re_1234567890abcdef
+RESEND_FROM_EMAIL=onboarding@resend.dev
+```
+
+### Production Environment
+```bash
+RESEND_API_KEY=re_production_key_here
+RESEND_FROM_EMAIL=hello@yourdomain.com
+```
+
+## Benefits of Resend Integration
+
+1. **Modern API**: Clean, RESTful API with excellent TypeScript support
+2. **React Email Templates**: Use React components for email templates
+3. **Better Deliverability**: Advanced email infrastructure
+4. **Developer Experience**: Intuitive dashboard and documentation
+5. **Analytics**: Built-in email analytics and tracking
+6. **Webhooks**: Real-time delivery and bounce notifications
+
+## Troubleshooting
+
+### Common Issues
+
+1. **API Key Issues**: Ensure your Resend API key is correct and has proper permissions
+2. **Domain Verification**: Make sure your sending domain is verified in Resend
+3. **Template Errors**: Check that all email templates are properly exported
+4. **Environment Variables**: Verify all environment variables are set correctly
+
+### Debug Steps
+
+1. Check Resend dashboard for delivery status
+2. Review application logs for error messages
+3. Test email templates locally with React Email
+4. Verify module registration in Medusa configuration
+
+## Next Steps
+
+After successful integration:
+
+1. **Customize Templates**: Brand your email templates with your logo and colors
+2. **Add Analytics**: Implement email tracking and analytics
+3. **Set Up Webhooks**: Handle delivery and bounce notifications
+4. **Optimize Deliverability**: Configure SPF, DKIM, and DMARC records
+5. **Scale**: Monitor usage and upgrade your Resend plan as needed
+
+This implementation provides a complete migration path from SendGrid to Resend while maintaining all existing email functionality in your Medusa application.
\ No newline at end of file
diff --git a/docs/resend-migration-checklist.md b/docs/resend-migration-checklist.md
new file mode 100644
index 000000000..edb8b5d24
--- /dev/null
+++ b/docs/resend-migration-checklist.md
@@ -0,0 +1,224 @@
+# Resend Migration Checklist
+
+## Pre-Migration Tasks
+
+### 1. Resend Account Setup
+- [ ] Create Resend account at [resend.com](https://resend.com)
+- [ ] Verify your domain in Resend dashboard
+- [ ] Generate API key with appropriate permissions
+- [ ] Test sending emails with development domain
+
+### 2. Environment Preparation
+- [ ] Backup current SendGrid configuration
+- [ ] Document current email templates and their IDs
+- [ ] Note current SendGrid API key and sender email
+- [ ] Plan downtime window for migration
+
+## Migration Steps
+
+### 3. Install Dependencies
+```bash
+cd apps/medusa
+yarn add resend @react-email/components
+yarn add -D react-email
+```
+
+### 4. Create Resend Module
+- [ ] Create `src/modules/resend/` directory
+- [ ] Create `src/modules/resend/service.ts`
+- [ ] Create `src/modules/resend/index.ts`
+- [ ] Create `src/modules/resend/emails/` directory
+
+### 5. Create Email Templates
+- [ ] Create `src/modules/resend/emails/order-placed.tsx`
+- [ ] Create `src/modules/resend/emails/chef-event-requested.tsx`
+- [ ] Create `src/modules/resend/emails/chef-event-accepted.tsx`
+- [ ] Create `src/modules/resend/emails/chef-event-rejected.tsx`
+
+### 6. Update Configuration
+- [ ] Update `medusa-config.ts` to use Resend provider
+- [ ] Add Resend environment variables to `.env`
+- [ ] Remove SendGrid configuration
+
+### 7. Update Environment Variables
+```bash
+# Add to .env
+RESEND_API_KEY=your_resend_api_key_here
+RESEND_FROM_EMAIL=onboarding@resend.dev
+
+# Remove from .env
+SENDGRID_API_KEY=old_sendgrid_key
+SENDGRID_FROM=old_sendgrid_from
+```
+
+### 8. Update Subscribers
+- [ ] Update `src/subscribers/chef-event-requested.ts`
+- [ ] Update `src/subscribers/chef-event-accepted.ts`
+- [ ] Update `src/subscribers/chef-event-rejected.ts`
+- [ ] Update template names in all subscribers
+
+### 9. Test Email Templates
+```bash
+yarn dev:email
+```
+- [ ] Verify all templates render correctly
+- [ ] Test with mock data
+- [ ] Check email formatting across different clients
+
+## Post-Migration Tasks
+
+### 10. Testing
+- [ ] Test order confirmation emails
+- [ ] Test chef event request notifications
+- [ ] Test chef event acceptance emails
+- [ ] Test chef event rejection emails
+- [ ] Verify email delivery in Resend dashboard
+
+### 11. Monitoring
+- [ ] Set up email delivery monitoring
+- [ ] Configure bounce and complaint handling
+- [ ] Set up webhooks for delivery status
+- [ ] Monitor email analytics in Resend
+
+### 12. Cleanup
+- [ ] Remove SendGrid dependencies
+- [ ] Remove SendGrid environment variables
+- [ ] Archive old SendGrid templates
+- [ ] Update documentation
+
+## Verification Checklist
+
+### Email Functionality
+- [ ] Order confirmation emails sent successfully
+- [ ] Chef event request notifications working
+- [ ] Chef event acceptance emails delivered
+- [ ] Chef event rejection emails sent
+- [ ] All email templates render correctly
+
+### Technical Verification
+- [ ] Resend module properly registered
+- [ ] Environment variables correctly set
+- [ ] No SendGrid dependencies remaining
+- [ ] All subscribers updated with new template names
+- [ ] Email delivery confirmed in Resend dashboard
+
+### Business Verification
+- [ ] Customer receives order confirmations
+- [ ] Chef receives event request notifications
+- [ ] Customers receive acceptance/rejection emails
+- [ ] Payment links work correctly in acceptance emails
+- [ ] All email content matches business requirements
+
+## Rollback Plan
+
+If issues arise during migration:
+
+1. **Immediate Rollback**
+ - Revert `medusa-config.ts` to SendGrid configuration
+ - Restore SendGrid environment variables
+ - Restart Medusa application
+
+2. **Data Recovery**
+ - Check Resend dashboard for sent emails
+ - Verify no emails were lost during transition
+ - Confirm all notifications were processed
+
+3. **Investigation**
+ - Review application logs for errors
+ - Check Resend API response codes
+ - Verify template syntax and data structure
+
+## Performance Monitoring
+
+### Key Metrics to Track
+- [ ] Email delivery rate
+- [ ] Email open rate
+- [ ] Click-through rate on payment links
+- [ ] Bounce rate
+- [ ] Complaint rate
+
+### Alerts to Set Up
+- [ ] High bounce rate alerts
+- [ ] Failed email delivery alerts
+- [ ] API error rate monitoring
+- [ ] Template rendering error alerts
+
+## Documentation Updates
+
+### Update Required Files
+- [ ] Update README.md with Resend setup instructions
+- [ ] Update deployment documentation
+- [ ] Update environment variable documentation
+- [ ] Create email template documentation
+
+### Team Communication
+- [ ] Notify team of email service change
+- [ ] Update onboarding documentation
+- [ ] Train team on Resend dashboard usage
+- [ ] Document troubleshooting procedures
+
+## Security Considerations
+
+### API Key Management
+- [ ] Store API key securely in environment variables
+- [ ] Use different API keys for development and production
+- [ ] Rotate API keys regularly
+- [ ] Monitor API key usage
+
+### Domain Security
+- [ ] Verify domain ownership in Resend
+- [ ] Set up proper SPF, DKIM, and DMARC records
+- [ ] Monitor domain reputation
+- [ ] Set up domain authentication alerts
+
+## Cost Optimization
+
+### Resend Pricing
+- [ ] Monitor email volume usage
+- [ ] Set up usage alerts
+- [ ] Optimize template size and complexity
+- [ ] Consider volume discounts for high usage
+
+### Template Optimization
+- [ ] Minimize template file sizes
+- [ ] Optimize images and assets
+- [ ] Use efficient CSS and HTML
+- [ ] Test template rendering performance
+
+## Final Checklist
+
+### Before Going Live
+- [ ] All tests passing
+- [ ] Email templates approved by stakeholders
+- [ ] Environment variables configured correctly
+- [ ] Monitoring and alerts set up
+- [ ] Team trained on new system
+- [ ] Rollback plan documented
+
+### Post-Launch Verification
+- [ ] First production emails sent successfully
+- [ ] Customer feedback positive
+- [ ] No critical errors in logs
+- [ ] Email analytics tracking properly
+- [ ] All business processes working correctly
+
+## Support Resources
+
+### Resend Documentation
+- [Resend API Documentation](https://resend.com/docs)
+- [React Email Documentation](https://react.email/docs)
+- [Resend Dashboard](https://resend.com/emails)
+
+### Medusa Resources
+- [Medusa Notification Module](https://docs.medusajs.com/modules/notifications)
+- [Medusa Workflows](https://docs.medusajs.com/development/workflows)
+- [Medusa Subscribers](https://docs.medusajs.com/development/subscribers)
+
+### Troubleshooting
+- Check Resend dashboard for delivery status
+- Review application logs for errors
+- Test email templates locally with React Email
+- Verify environment variables are set correctly
+- Confirm module registration in Medusa configuration
+
+This checklist ensures a smooth migration from SendGrid to Resend while maintaining all email functionality in your Medusa application.
\ No newline at end of file
diff --git a/package.json b/package.json
index 812b8edf0..6a38acab8 100644
--- a/package.json
+++ b/package.json
@@ -29,5 +29,17 @@
"workspaces": [
"apps/*",
"packages/*"
- ]
+ ],
+ "dependencies": {
+ "@hookform/resolvers": "^5.1.0",
+ "@medusajs/icons": "^2.8.4",
+ "@medusajs/ui": "^4.0.14",
+ "@remix-run/react": "^2.16.8",
+ "@tanstack/react-query": "^5.80.6",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-hook-form": "^7.57.0",
+ "remix-hook-form": "^7.0.1",
+ "zod": "^3.25.56"
+ }
}
diff --git a/yarn.lock b/yarn.lock
index 2d373d339..1e4e87ceb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -753,6 +753,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/code-frame@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/code-frame@npm:7.27.1"
+ dependencies:
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ js-tokens: "npm:^4.0.0"
+ picocolors: "npm:^1.1.1"
+ checksum: 10c0/5dd9a18baa5fce4741ba729acc3a3272c49c25cb8736c4b18e113099520e7ef7b545a4096a26d600e4416157e63e87d66db46aa3fbf0a5f2286da2705c12da00
+ languageName: node
+ linkType: hard
+
"@babel/compat-data@npm:^7.26.5":
version: 7.26.8
resolution: "@babel/compat-data@npm:7.26.8"
@@ -760,49 +771,49 @@ __metadata:
languageName: node
linkType: hard
-"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.8, @babel/core@npm:^7.23.9, @babel/core@npm:^7.26.0":
- version: 7.26.9
- resolution: "@babel/core@npm:7.26.9"
+"@babel/core@npm:7.26.10, @babel/core@npm:^7.23.7":
+ version: 7.26.10
+ resolution: "@babel/core@npm:7.26.10"
dependencies:
"@ampproject/remapping": "npm:^2.2.0"
"@babel/code-frame": "npm:^7.26.2"
- "@babel/generator": "npm:^7.26.9"
+ "@babel/generator": "npm:^7.26.10"
"@babel/helper-compilation-targets": "npm:^7.26.5"
"@babel/helper-module-transforms": "npm:^7.26.0"
- "@babel/helpers": "npm:^7.26.9"
- "@babel/parser": "npm:^7.26.9"
+ "@babel/helpers": "npm:^7.26.10"
+ "@babel/parser": "npm:^7.26.10"
"@babel/template": "npm:^7.26.9"
- "@babel/traverse": "npm:^7.26.9"
- "@babel/types": "npm:^7.26.9"
+ "@babel/traverse": "npm:^7.26.10"
+ "@babel/types": "npm:^7.26.10"
convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
- checksum: 10c0/ed7212ff42a9453765787019b7d191b167afcacd4bd8fec10b055344ef53fa0cc648c9a80159ae4ecf870016a6318731e087042dcb68d1a2a9d34eb290dc014b
+ checksum: 10c0/e046e0e988ab53841b512ee9d263ca409f6c46e2a999fe53024688b92db394346fa3aeae5ea0866331f62133982eee05a675d22922a4603c3f603aa09a581d62
languageName: node
linkType: hard
-"@babel/core@npm:^7.23.7":
- version: 7.26.10
- resolution: "@babel/core@npm:7.26.10"
+"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.7, @babel/core@npm:^7.21.8, @babel/core@npm:^7.23.9, @babel/core@npm:^7.26.0":
+ version: 7.26.9
+ resolution: "@babel/core@npm:7.26.9"
dependencies:
"@ampproject/remapping": "npm:^2.2.0"
"@babel/code-frame": "npm:^7.26.2"
- "@babel/generator": "npm:^7.26.10"
+ "@babel/generator": "npm:^7.26.9"
"@babel/helper-compilation-targets": "npm:^7.26.5"
"@babel/helper-module-transforms": "npm:^7.26.0"
- "@babel/helpers": "npm:^7.26.10"
- "@babel/parser": "npm:^7.26.10"
+ "@babel/helpers": "npm:^7.26.9"
+ "@babel/parser": "npm:^7.26.9"
"@babel/template": "npm:^7.26.9"
- "@babel/traverse": "npm:^7.26.10"
- "@babel/types": "npm:^7.26.10"
+ "@babel/traverse": "npm:^7.26.9"
+ "@babel/types": "npm:^7.26.9"
convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
- checksum: 10c0/e046e0e988ab53841b512ee9d263ca409f6c46e2a999fe53024688b92db394346fa3aeae5ea0866331f62133982eee05a675d22922a4603c3f603aa09a581d62
+ checksum: 10c0/ed7212ff42a9453765787019b7d191b167afcacd4bd8fec10b055344ef53fa0cc648c9a80159ae4ecf870016a6318731e087042dcb68d1a2a9d34eb290dc014b
languageName: node
linkType: hard
@@ -832,6 +843,19 @@ __metadata:
languageName: node
linkType: hard
+"@babel/generator@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/generator@npm:7.28.0"
+ dependencies:
+ "@babel/parser": "npm:^7.28.0"
+ "@babel/types": "npm:^7.28.0"
+ "@jridgewell/gen-mapping": "npm:^0.3.12"
+ "@jridgewell/trace-mapping": "npm:^0.3.28"
+ jsesc: "npm:^3.0.2"
+ checksum: 10c0/1b3d122268ea3df50fde707ad864d9a55c72621357d5cebb972db3dd76859c45810c56e16ad23123f18f80cc2692f5a015d2858361300f0f224a05dc43d36a92
+ languageName: node
+ linkType: hard
+
"@babel/helper-annotate-as-pure@npm:^7.22.5, @babel/helper-annotate-as-pure@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-annotate-as-pure@npm:7.25.9"
@@ -871,6 +895,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-globals@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/helper-globals@npm:7.28.0"
+ checksum: 10c0/5a0cd0c0e8c764b5f27f2095e4243e8af6fa145daea2b41b53c0c1414fe6ff139e3640f4e2207ae2b3d2153a1abd346f901c26c290ee7cb3881dd922d4ee9232
+ languageName: node
+ linkType: hard
+
"@babel/helper-member-expression-to-functions@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-member-expression-to-functions@npm:7.25.9"
@@ -950,6 +981,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-string-parser@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-string-parser@npm:7.27.1"
+ checksum: 10c0/8bda3448e07b5583727c103560bcf9c4c24b3c1051a4c516d4050ef69df37bb9a4734a585fe12725b8c2763de0a265aa1e909b485a4e3270b7cfd3e4dbe4b602
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-identifier@npm:^7.24.7, @babel/helper-validator-identifier@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-identifier@npm:7.25.9"
@@ -957,6 +995,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/helper-validator-identifier@npm:^7.27.1":
+ version: 7.27.1
+ resolution: "@babel/helper-validator-identifier@npm:7.27.1"
+ checksum: 10c0/c558f11c4871d526498e49d07a84752d1800bf72ac0d3dad100309a2eaba24efbf56ea59af5137ff15e3a00280ebe588560534b0e894a4750f8b1411d8f78b84
+ languageName: node
+ linkType: hard
+
"@babel/helper-validator-option@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-validator-option@npm:7.25.9"
@@ -995,6 +1040,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/parser@npm:7.27.0, @babel/parser@npm:^7.23.6, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
+ version: 7.27.0
+ resolution: "@babel/parser@npm:7.27.0"
+ dependencies:
+ "@babel/types": "npm:^7.27.0"
+ bin:
+ parser: ./bin/babel-parser.js
+ checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859
+ languageName: node
+ linkType: hard
+
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.15, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.21.8, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.6, @babel/parser@npm:^7.26.9":
version: 7.26.9
resolution: "@babel/parser@npm:7.26.9"
@@ -1006,14 +1062,14 @@ __metadata:
languageName: node
linkType: hard
-"@babel/parser@npm:^7.23.6, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
- version: 7.27.0
- resolution: "@babel/parser@npm:7.27.0"
+"@babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.0":
+ version: 7.28.0
+ resolution: "@babel/parser@npm:7.28.0"
dependencies:
- "@babel/types": "npm:^7.27.0"
+ "@babel/types": "npm:^7.28.0"
bin:
parser: ./bin/babel-parser.js
- checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859
+ checksum: 10c0/c2ef81d598990fa949d1d388429df327420357cb5200271d0d0a2784f1e6d54afc8301eb8bdf96d8f6c77781e402da93c7dc07980fcc136ac5b9d5f1fce701b5
languageName: node
linkType: hard
@@ -1320,6 +1376,17 @@ __metadata:
languageName: node
linkType: hard
+"@babel/template@npm:^7.27.2":
+ version: 7.27.2
+ resolution: "@babel/template@npm:7.27.2"
+ dependencies:
+ "@babel/code-frame": "npm:^7.27.1"
+ "@babel/parser": "npm:^7.27.2"
+ "@babel/types": "npm:^7.27.1"
+ checksum: 10c0/ed9e9022651e463cc5f2cc21942f0e74544f1754d231add6348ff1b472985a3b3502041c0be62dc99ed2d12cfae0c51394bf827452b98a2f8769c03b87aadc81
+ languageName: node
+ linkType: hard
+
"@babel/traverse@npm:7.25.6":
version: 7.25.6
resolution: "@babel/traverse@npm:7.25.6"
@@ -1335,6 +1402,21 @@ __metadata:
languageName: node
linkType: hard
+"@babel/traverse@npm:7.27.0, @babel/traverse@npm:^7.23.7, @babel/traverse@npm:^7.26.10":
+ version: 7.27.0
+ resolution: "@babel/traverse@npm:7.27.0"
+ dependencies:
+ "@babel/code-frame": "npm:^7.26.2"
+ "@babel/generator": "npm:^7.27.0"
+ "@babel/parser": "npm:^7.27.0"
+ "@babel/template": "npm:^7.27.0"
+ "@babel/types": "npm:^7.27.0"
+ debug: "npm:^4.3.1"
+ globals: "npm:^11.1.0"
+ checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b
+ languageName: node
+ linkType: hard
+
"@babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.9, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.2":
version: 7.26.9
resolution: "@babel/traverse@npm:7.26.9"
@@ -1350,18 +1432,18 @@ __metadata:
languageName: node
linkType: hard
-"@babel/traverse@npm:^7.23.7, @babel/traverse@npm:^7.26.10":
- version: 7.27.0
- resolution: "@babel/traverse@npm:7.27.0"
+"@babel/traverse@npm:^7.27.0":
+ version: 7.28.0
+ resolution: "@babel/traverse@npm:7.28.0"
dependencies:
- "@babel/code-frame": "npm:^7.26.2"
- "@babel/generator": "npm:^7.27.0"
- "@babel/parser": "npm:^7.27.0"
- "@babel/template": "npm:^7.27.0"
- "@babel/types": "npm:^7.27.0"
+ "@babel/code-frame": "npm:^7.27.1"
+ "@babel/generator": "npm:^7.28.0"
+ "@babel/helper-globals": "npm:^7.28.0"
+ "@babel/parser": "npm:^7.28.0"
+ "@babel/template": "npm:^7.27.2"
+ "@babel/types": "npm:^7.28.0"
debug: "npm:^4.3.1"
- globals: "npm:^11.1.0"
- checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b
+ checksum: 10c0/32794402457827ac558173bcebdcc0e3a18fa339b7c41ca35621f9f645f044534d91bb923ff385f5f960f2e495f56ce18d6c7b0d064d2f0ccb55b285fa6bc7b9
languageName: node
linkType: hard
@@ -1396,6 +1478,16 @@ __metadata:
languageName: node
linkType: hard
+"@babel/types@npm:^7.27.1, @babel/types@npm:^7.28.0":
+ version: 7.28.2
+ resolution: "@babel/types@npm:7.28.2"
+ dependencies:
+ "@babel/helper-string-parser": "npm:^7.27.1"
+ "@babel/helper-validator-identifier": "npm:^7.27.1"
+ checksum: 10c0/24b11c9368e7e2c291fe3c1bcd1ed66f6593a3975f479cbb9dd7b8c8d8eab8a962b0d2fca616c043396ce82500ac7d23d594fbbbd013828182c01596370a0b10
+ languageName: node
+ linkType: hard
+
"@bcoe/v8-coverage@npm:^0.2.3":
version: 0.2.3
resolution: "@bcoe/v8-coverage@npm:0.2.3"
@@ -1589,6 +1681,15 @@ __metadata:
languageName: node
linkType: hard
+"@emnapi/runtime@npm:^1.4.0, @emnapi/runtime@npm:^1.4.4":
+ version: 1.4.5
+ resolution: "@emnapi/runtime@npm:1.4.5"
+ dependencies:
+ tslib: "npm:^2.4.0"
+ checksum: 10c0/37a0278be5ac81e918efe36f1449875cbafba947039c53c65a1f8fc238001b866446fc66041513b286baaff5d6f9bec667f5164b3ca481373a8d9cb65bfc984b
+ languageName: node
+ linkType: hard
+
"@emnapi/wasi-threads@npm:1.0.1":
version: 1.0.1
resolution: "@emnapi/wasi-threads@npm:1.0.1"
@@ -1742,6 +1843,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/aix-ppc64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/aix-ppc64@npm:0.25.0"
+ conditions: os=aix & cpu=ppc64
+ languageName: node
+ linkType: hard
+
"@esbuild/aix-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/aix-ppc64@npm:0.25.3"
@@ -1763,6 +1871,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/android-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/android-arm64@npm:0.25.0"
+ conditions: os=android & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/android-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm64@npm:0.25.3"
@@ -1784,6 +1899,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/android-arm@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/android-arm@npm:0.25.0"
+ conditions: os=android & cpu=arm
+ languageName: node
+ linkType: hard
+
"@esbuild/android-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-arm@npm:0.25.3"
@@ -1805,6 +1927,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/android-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/android-x64@npm:0.25.0"
+ conditions: os=android & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/android-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/android-x64@npm:0.25.3"
@@ -1826,6 +1955,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/darwin-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/darwin-arm64@npm:0.25.0"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/darwin-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-arm64@npm:0.25.3"
@@ -1847,6 +1983,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/darwin-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/darwin-x64@npm:0.25.0"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/darwin-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/darwin-x64@npm:0.25.3"
@@ -1868,6 +2011,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/freebsd-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/freebsd-arm64@npm:0.25.0"
+ conditions: os=freebsd & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/freebsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-arm64@npm:0.25.3"
@@ -1889,6 +2039,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/freebsd-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/freebsd-x64@npm:0.25.0"
+ conditions: os=freebsd & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/freebsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/freebsd-x64@npm:0.25.3"
@@ -1910,6 +2067,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-arm64@npm:0.25.0"
+ conditions: os=linux & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm64@npm:0.25.3"
@@ -1931,6 +2095,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-arm@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-arm@npm:0.25.0"
+ conditions: os=linux & cpu=arm
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-arm@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-arm@npm:0.25.3"
@@ -1952,6 +2123,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-ia32@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-ia32@npm:0.25.0"
+ conditions: os=linux & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ia32@npm:0.25.3"
@@ -1973,6 +2151,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-loong64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-loong64@npm:0.25.0"
+ conditions: os=linux & cpu=loong64
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-loong64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-loong64@npm:0.25.3"
@@ -1994,6 +2179,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-mips64el@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-mips64el@npm:0.25.0"
+ conditions: os=linux & cpu=mips64el
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-mips64el@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-mips64el@npm:0.25.3"
@@ -2015,6 +2207,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-ppc64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-ppc64@npm:0.25.0"
+ conditions: os=linux & cpu=ppc64
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-ppc64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-ppc64@npm:0.25.3"
@@ -2036,6 +2235,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-riscv64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-riscv64@npm:0.25.0"
+ conditions: os=linux & cpu=riscv64
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-riscv64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-riscv64@npm:0.25.3"
@@ -2057,6 +2263,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-s390x@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-s390x@npm:0.25.0"
+ conditions: os=linux & cpu=s390x
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-s390x@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-s390x@npm:0.25.3"
@@ -2078,6 +2291,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/linux-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/linux-x64@npm:0.25.0"
+ conditions: os=linux & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/linux-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/linux-x64@npm:0.25.3"
@@ -2085,6 +2305,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/netbsd-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/netbsd-arm64@npm:0.25.0"
+ conditions: os=netbsd & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/netbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-arm64@npm:0.25.3"
@@ -2106,6 +2333,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/netbsd-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/netbsd-x64@npm:0.25.0"
+ conditions: os=netbsd & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/netbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/netbsd-x64@npm:0.25.3"
@@ -2113,6 +2347,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/openbsd-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/openbsd-arm64@npm:0.25.0"
+ conditions: os=openbsd & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/openbsd-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-arm64@npm:0.25.3"
@@ -2134,6 +2375,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/openbsd-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/openbsd-x64@npm:0.25.0"
+ conditions: os=openbsd & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/openbsd-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/openbsd-x64@npm:0.25.3"
@@ -2155,6 +2403,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/sunos-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/sunos-x64@npm:0.25.0"
+ conditions: os=sunos & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/sunos-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/sunos-x64@npm:0.25.3"
@@ -2176,6 +2431,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/win32-arm64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/win32-arm64@npm:0.25.0"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@esbuild/win32-arm64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-arm64@npm:0.25.3"
@@ -2197,6 +2459,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/win32-ia32@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/win32-ia32@npm:0.25.0"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@esbuild/win32-ia32@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-ia32@npm:0.25.3"
@@ -2218,6 +2487,13 @@ __metadata:
languageName: node
linkType: hard
+"@esbuild/win32-x64@npm:0.25.0":
+ version: 0.25.0
+ resolution: "@esbuild/win32-x64@npm:0.25.0"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@esbuild/win32-x64@npm:0.25.3":
version: 0.25.3
resolution: "@esbuild/win32-x64@npm:0.25.3"
@@ -2581,6 +2857,17 @@ __metadata:
languageName: node
linkType: hard
+"@hookform/resolvers@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "@hookform/resolvers@npm:5.1.0"
+ dependencies:
+ "@standard-schema/utils": "npm:^0.3.0"
+ peerDependencies:
+ react-hook-form: ^7.55.0
+ checksum: 10c0/5bd28ef58a182102f40b7fa2bc73a5e423c8bcf9cb25fee91cb8933026e7efba6f9adfda1ee637294de59c5467c938f4fb99c77a73bbb8c6180482c69d31cbbd
+ languageName: node
+ linkType: hard
+
"@humanwhocodes/config-array@npm:^0.9.2":
version: 0.9.5
resolution: "@humanwhocodes/config-array@npm:0.9.5"
@@ -2611,6 +2898,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-darwin-arm64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-darwin-arm64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-darwin-arm64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@img/sharp-darwin-arm64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-darwin-arm64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-darwin-arm64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@img/sharp-darwin-x64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-darwin-x64@npm:0.33.5"
@@ -2623,6 +2934,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-darwin-x64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-darwin-x64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-darwin-x64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@img/sharp-darwin-x64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-darwin-x64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-darwin-x64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-darwin-arm64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.4"
@@ -2630,6 +2965,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-darwin-arm64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-darwin-arm64@npm:1.1.0"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-darwin-arm64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-darwin-arm64@npm:1.2.0"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-darwin-x64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.4"
@@ -2637,6 +2986,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-darwin-x64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-darwin-x64@npm:1.1.0"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-darwin-x64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-darwin-x64@npm:1.2.0"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-arm64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.4"
@@ -2644,6 +3007,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-arm64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linux-arm64@npm:1.1.0"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-arm64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linux-arm64@npm:1.2.0"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-arm@npm:1.0.5":
version: 1.0.5
resolution: "@img/sharp-libvips-linux-arm@npm:1.0.5"
@@ -2651,6 +3028,34 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-arm@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linux-arm@npm:1.1.0"
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-arm@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linux-arm@npm:1.2.0"
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-ppc64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linux-ppc64@npm:1.1.0"
+ conditions: os=linux & cpu=ppc64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-ppc64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linux-ppc64@npm:1.2.0"
+ conditions: os=linux & cpu=ppc64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-s390x@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.4"
@@ -2658,6 +3063,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-s390x@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linux-s390x@npm:1.1.0"
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-s390x@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linux-s390x@npm:1.2.0"
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linux-x64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-linux-x64@npm:1.0.4"
@@ -2665,6 +3084,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linux-x64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linux-x64@npm:1.1.0"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linux-x64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linux-x64@npm:1.2.0"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.4"
@@ -2672,6 +3105,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.1.0"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.2.0"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-libvips-linuxmusl-x64@npm:1.0.4":
version: 1.0.4
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.4"
@@ -2679,6 +3126,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-libvips-linuxmusl-x64@npm:1.1.0":
+ version: 1.1.0
+ resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.1.0"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@img/sharp-libvips-linuxmusl-x64@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.2.0"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-arm64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linux-arm64@npm:0.33.5"
@@ -2691,6 +3152,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-arm64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linux-arm64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linux-arm64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linux-arm64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linux-arm64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linux-arm64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-arm@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linux-arm@npm:0.33.5"
@@ -2703,6 +3188,42 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-arm@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linux-arm@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linux-arm": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linux-arm@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linux-arm@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linux-arm": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linux-ppc64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linux-ppc64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linux-ppc64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-ppc64":
+ optional: true
+ conditions: os=linux & cpu=ppc64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-s390x@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linux-s390x@npm:0.33.5"
@@ -2715,6 +3236,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-s390x@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linux-s390x@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linux-s390x": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linux-s390x@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linux-s390x@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linux-s390x": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linux-x64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linux-x64@npm:0.33.5"
@@ -2727,6 +3272,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linux-x64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linux-x64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linux-x64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linux-x64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linux-x64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linux-x64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@img/sharp-linuxmusl-arm64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.5"
@@ -2739,6 +3308,30 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-linuxmusl-arm64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linuxmusl-arm64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linuxmusl-arm64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@img/sharp-linuxmusl-x64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-linuxmusl-x64@npm:0.33.5"
@@ -2751,15 +3344,64 @@ __metadata:
languageName: node
linkType: hard
-"@img/sharp-wasm32@npm:0.33.5":
- version: 0.33.5
- resolution: "@img/sharp-wasm32@npm:0.33.5"
+"@img/sharp-linuxmusl-x64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-linuxmusl-x64@npm:0.34.1"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.1.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@img/sharp-linuxmusl-x64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-linuxmusl-x64@npm:0.34.3"
+ dependencies:
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0"
+ dependenciesMeta:
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@img/sharp-wasm32@npm:0.33.5":
+ version: 0.33.5
+ resolution: "@img/sharp-wasm32@npm:0.33.5"
+ dependencies:
+ "@emnapi/runtime": "npm:^1.2.0"
+ conditions: cpu=wasm32
+ languageName: node
+ linkType: hard
+
+"@img/sharp-wasm32@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-wasm32@npm:0.34.1"
+ dependencies:
+ "@emnapi/runtime": "npm:^1.4.0"
+ conditions: cpu=wasm32
+ languageName: node
+ linkType: hard
+
+"@img/sharp-wasm32@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-wasm32@npm:0.34.3"
dependencies:
- "@emnapi/runtime": "npm:^1.2.0"
+ "@emnapi/runtime": "npm:^1.4.4"
conditions: cpu=wasm32
languageName: node
linkType: hard
+"@img/sharp-win32-arm64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-win32-arm64@npm:0.34.3"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@img/sharp-win32-ia32@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-win32-ia32@npm:0.33.5"
@@ -2767,6 +3409,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-win32-ia32@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-win32-ia32@npm:0.34.1"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
+"@img/sharp-win32-ia32@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-win32-ia32@npm:0.34.3"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@img/sharp-win32-x64@npm:0.33.5":
version: 0.33.5
resolution: "@img/sharp-win32-x64@npm:0.33.5"
@@ -2774,6 +3430,20 @@ __metadata:
languageName: node
linkType: hard
+"@img/sharp-win32-x64@npm:0.34.1":
+ version: 0.34.1
+ resolution: "@img/sharp-win32-x64@npm:0.34.1"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@img/sharp-win32-x64@npm:0.34.3":
+ version: 0.34.3
+ resolution: "@img/sharp-win32-x64@npm:0.34.3"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@inquirer/checkbox@npm:^2.3.11":
version: 2.5.0
resolution: "@inquirer/checkbox@npm:2.5.0"
@@ -2886,6 +3556,22 @@ __metadata:
languageName: node
linkType: hard
+"@isaacs/balanced-match@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "@isaacs/balanced-match@npm:4.0.1"
+ checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420
+ languageName: node
+ linkType: hard
+
+"@isaacs/brace-expansion@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "@isaacs/brace-expansion@npm:5.0.0"
+ dependencies:
+ "@isaacs/balanced-match": "npm:^4.0.1"
+ checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977
+ languageName: node
+ linkType: hard
+
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@@ -3410,6 +4096,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/gen-mapping@npm:^0.3.12":
+ version: 0.3.12
+ resolution: "@jridgewell/gen-mapping@npm:0.3.12"
+ dependencies:
+ "@jridgewell/sourcemap-codec": "npm:^1.5.0"
+ "@jridgewell/trace-mapping": "npm:^0.3.24"
+ checksum: 10c0/32f771ae2467e4d440be609581f7338d786d3d621bac3469e943b9d6d116c23c4becb36f84898a92bbf2f3c0511365c54a945a3b86a83141547a2a360a5ec0c7
+ languageName: node
+ linkType: hard
+
"@jridgewell/gen-mapping@npm:^0.3.2, @jridgewell/gen-mapping@npm:^0.3.5":
version: 0.3.8
resolution: "@jridgewell/gen-mapping@npm:0.3.8"
@@ -3435,6 +4131,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/source-map@npm:^0.3.3":
+ version: 0.3.10
+ resolution: "@jridgewell/source-map@npm:0.3.10"
+ dependencies:
+ "@jridgewell/gen-mapping": "npm:^0.3.5"
+ "@jridgewell/trace-mapping": "npm:^0.3.25"
+ checksum: 10c0/cf6a51808bc710eb91a9e6c5e250c1af5714299c8de3db2b74e273a27ba7313f37c198ba332a512b7657fa23fed125c0147bfb1b925cadc9697a89cebecad0d8
+ languageName: node
+ linkType: hard
+
"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15":
version: 1.5.0
resolution: "@jridgewell/sourcemap-codec@npm:1.5.0"
@@ -3442,6 +4148,13 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/sourcemap-codec@npm:^1.5.0":
+ version: 1.5.4
+ resolution: "@jridgewell/sourcemap-codec@npm:1.5.4"
+ checksum: 10c0/c5aab3e6362a8dd94ad80ab90845730c825fc4c8d9cf07ebca7a2eb8a832d155d62558800fc41d42785f989ddbb21db6df004d1786e8ecb65e428ab8dff71309
+ languageName: node
+ linkType: hard
+
"@jridgewell/trace-mapping@npm:0.3.9":
version: 0.3.9
resolution: "@jridgewell/trace-mapping@npm:0.3.9"
@@ -3462,6 +4175,16 @@ __metadata:
languageName: node
linkType: hard
+"@jridgewell/trace-mapping@npm:^0.3.28":
+ version: 0.3.29
+ resolution: "@jridgewell/trace-mapping@npm:0.3.29"
+ dependencies:
+ "@jridgewell/resolve-uri": "npm:^3.1.0"
+ "@jridgewell/sourcemap-codec": "npm:^1.4.14"
+ checksum: 10c0/fb547ba31658c4d74eb17e7389f4908bf7c44cef47acb4c5baa57289daf68e6fe53c639f41f751b3923aca67010501264f70e7b49978ad1f040294b22c37b333
+ languageName: node
+ linkType: hard
+
"@jsdoc/salty@npm:^0.2.1":
version: 0.2.9
resolution: "@jsdoc/salty@npm:0.2.9"
@@ -3568,6 +4291,24 @@ __metadata:
languageName: node
linkType: hard
+"@lottiefiles/dotlottie-react@npm:0.13.3":
+ version: 0.13.3
+ resolution: "@lottiefiles/dotlottie-react@npm:0.13.3"
+ dependencies:
+ "@lottiefiles/dotlottie-web": "npm:0.42.0"
+ peerDependencies:
+ react: ^17 || ^18 || ^19
+ checksum: 10c0/195e56a886574c87f5cc15d4e72d26a6350a6a5d1606391a2e4b3ef64764b3e126d6dc9d8e860bf5f64539377b9a2a70740f1ad67d2729a2e5c011aaf001e764
+ languageName: node
+ linkType: hard
+
+"@lottiefiles/dotlottie-web@npm:0.42.0":
+ version: 0.42.0
+ resolution: "@lottiefiles/dotlottie-web@npm:0.42.0"
+ checksum: 10c0/c0f7cd42c343585e2cc5e2a8242b8f1fe69be9f72adf7a649f3c275133824046a242e2c6a895b9e2cff867330947b88de4a6ad1f9820c55105bb8b7f886e3683
+ languageName: node
+ linkType: hard
+
"@medusajs/admin-bundler@npm:2.7.0":
version: 2.7.0
resolution: "@medusajs/admin-bundler@npm:2.7.0"
@@ -3974,6 +4715,15 @@ __metadata:
languageName: node
linkType: hard
+"@medusajs/icons@npm:2.8.4, @medusajs/icons@npm:^2.8.4":
+ version: 2.8.4
+ resolution: "@medusajs/icons@npm:2.8.4"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/a673d5c97a591daff07097f45bb43a3fdecf3228f53f845ab58c6da3fdf1e9e4813459b53290405bd08fe8c70f5b82886bf1f3c8c55e4bd2e00865aafea71fe0
+ languageName: node
+ linkType: hard
+
"@medusajs/index@npm:2.7.0":
version: 2.7.0
resolution: "@medusajs/index@npm:2.7.0"
@@ -4436,6 +5186,30 @@ __metadata:
languageName: node
linkType: hard
+"@medusajs/ui@npm:^4.0.14":
+ version: 4.0.14
+ resolution: "@medusajs/ui@npm:4.0.14"
+ dependencies:
+ "@medusajs/icons": "npm:2.8.4"
+ "@tanstack/react-table": "npm:8.20.5"
+ clsx: "npm:^1.2.1"
+ copy-to-clipboard: "npm:^3.3.3"
+ cva: "npm:1.0.0-beta.1"
+ prism-react-renderer: "npm:^2.0.6"
+ prismjs: "npm:^1.29.0"
+ radix-ui: "npm:1.1.2"
+ react-aria: "npm:^3.33.1"
+ react-currency-input-field: "npm:^3.6.11"
+ react-stately: "npm:^3.31.1"
+ sonner: "npm:^1.5.0"
+ tailwind-merge: "npm:^2.2.1"
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/d6c5fbd622b19591f59993ca3e3f004de214b36196d8ab22129b4b03420a0d0b565f647f6f39c5386546b79676319ed78ff8992b531761f48d1fcc36166ba2b8
+ languageName: node
+ linkType: hard
+
"@medusajs/user@npm:2.7.0":
version: 2.7.0
resolution: "@medusajs/user@npm:2.7.0"
@@ -4738,6 +5512,69 @@ __metadata:
languageName: node
linkType: hard
+"@next/env@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/env@npm:15.4.1"
+ checksum: 10c0/389c4c5d126c252a742d78336d0b8d99678a83cb003a2f80f805adc89fcb9e9f617a279c0db07ca94d8c7c94458f34992dd49fcdd10c5d00940c45ad5cf79fbd
+ languageName: node
+ linkType: hard
+
+"@next/swc-darwin-arm64@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-darwin-arm64@npm:15.4.1"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@next/swc-darwin-x64@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-darwin-x64@npm:15.4.1"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@next/swc-linux-arm64-gnu@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-linux-arm64-gnu@npm:15.4.1"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@next/swc-linux-arm64-musl@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-linux-arm64-musl@npm:15.4.1"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@next/swc-linux-x64-gnu@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-linux-x64-gnu@npm:15.4.1"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
+"@next/swc-linux-x64-musl@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-linux-x64-musl@npm:15.4.1"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@next/swc-win32-arm64-msvc@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-win32-arm64-msvc@npm:15.4.1"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@next/swc-win32-x64-msvc@npm:15.4.1":
+ version: 15.4.1
+ resolution: "@next/swc-win32-x64-msvc@npm:15.4.1"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -5645,6 +6482,13 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/colors@npm:3.0.0":
+ version: 3.0.0
+ resolution: "@radix-ui/colors@npm:3.0.0"
+ checksum: 10c0/612c6bb4efe987f2d49aaca2f785687d08150da59a248328bc2b4e187ece4ef731c1dc5c6a9430fffcac4af9e65cf2f89ceb9806f8809940c9eb238b79d2cf2f
+ languageName: node
+ linkType: hard
+
"@radix-ui/number@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/number@npm:1.0.1"
@@ -6047,6 +6891,32 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-collapsible@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-collapsible@npm:1.1.7"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/5b0e55a76f368440cd5a6d11537500f93fb4466c536cee7b1d48130c9bc4337c3441fb5d9eac741f7a3cef5d16279c6beb3f050be1db8e8bfa519a30a1dac113
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-collection@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-collection@npm:1.0.3"
@@ -6482,6 +7352,31 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-dropdown-menu@npm:2.1.10":
+ version: 2.1.10
+ resolution: "@radix-ui/react-dropdown-menu@npm:2.1.10"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-menu": "npm:2.1.10"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/bc43be1aefc02081215e091e995e4a510cc44ac1bf371b4cde7f87cf4690298ba535392dbccdda53e3b33794bc8b21c5ebf9f7f35c6348d53a38ca018032b625
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-dropdown-menu@npm:2.1.5":
version: 2.1.5
resolution: "@radix-ui/react-dropdown-menu@npm:2.1.5"
@@ -6819,6 +7714,42 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-menu@npm:2.1.10":
+ version: 2.1.10
+ resolution: "@radix-ui/react-menu@npm:2.1.10"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-collection": "npm:1.1.4"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.7"
+ "@radix-ui/react-focus-guards": "npm:1.1.2"
+ "@radix-ui/react-focus-scope": "npm:1.1.4"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.4"
+ "@radix-ui/react-portal": "npm:1.1.6"
+ "@radix-ui/react-presence": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-roving-focus": "npm:1.1.6"
+ "@radix-ui/react-slot": "npm:1.2.0"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/557fff885aca5e349320dd6448eec5e6a01abe15d748d3651e3a0bf3e9bc31bc78812a39aac3a0e43489ac7ee6750e1f8606bac655a740b6039fe41213aad171
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-menu@npm:2.1.12":
version: 2.1.12
resolution: "@radix-ui/react-menu@npm:2.1.12"
@@ -6951,6 +7882,39 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-popover@npm:1.1.10":
+ version: 1.1.10
+ resolution: "@radix-ui/react-popover@npm:1.1.10"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.7"
+ "@radix-ui/react-focus-guards": "npm:1.1.2"
+ "@radix-ui/react-focus-scope": "npm:1.1.4"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.4"
+ "@radix-ui/react-portal": "npm:1.1.6"
+ "@radix-ui/react-presence": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-slot": "npm:1.2.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/9708f6f9a56de41b67c488141eb4c79aead0f2a5a3861b82f028f80f8d5f287d9c08955fbc00a16157431815c2b934775c861cd5ce5ad3a32cf8f040f03e11ec
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-popover@npm:1.1.5":
version: 1.1.5
resolution: "@radix-ui/react-popover@npm:1.1.5"
@@ -7189,12 +8153,32 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-presence@npm:1.1.2":
- version: 1.1.2
- resolution: "@radix-ui/react-presence@npm:1.1.2"
+"@radix-ui/react-presence@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@radix-ui/react-presence@npm:1.1.2"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.1"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/0c6fa281368636308044df3be4c1f02733094b5e35ba04f26e610dd1c4315a245ffc758e0e176c444742a7a46f4328af1a9d8181e860175ec39338d06525a78d
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-presence@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/react-presence@npm:1.1.3"
dependencies:
- "@radix-ui/react-compose-refs": "npm:1.1.1"
- "@radix-ui/react-use-layout-effect": "npm:1.1.0"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -7205,7 +8189,7 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10c0/0c6fa281368636308044df3be4c1f02733094b5e35ba04f26e610dd1c4315a245ffc758e0e176c444742a7a46f4328af1a9d8181e860175ec39338d06525a78d
+ checksum: 10c0/1035e5ac32e35e281f54ffd543c1f794931e538c43e553336fc2cab449f83d6aa9f003d328db7f51506a31114d20183f9cbfeab196a8beeed6781f7e58c16a3c
languageName: node
linkType: hard
@@ -7422,6 +8406,33 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-roving-focus@npm:1.1.6":
+ version: 1.1.6
+ resolution: "@radix-ui/react-roving-focus@npm:1.1.6"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-collection": "npm:1.1.4"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/f922d02c7e63a86f7e4df3fd5a766961b98309f8c2d01e77bf545d73ed0ea434ddd26d0e3c23c1a87e7f6685840463544bdd23bb48c9ae7d9cf5f105d3087c78
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-roving-focus@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-roving-focus@npm:1.1.7"
@@ -7798,6 +8809,32 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-tabs@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-tabs@npm:1.1.7"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-roving-focus": "npm:1.1.6"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/5ad6ed8524d340a2d053b075fb06e8e8493217ba3ae5b2c1247a274a3c68cc5921295b092d44eb907af8aed8129b800f086009ba19694b6c72c19a5f8f769bb0
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-toast@npm:1.2.5":
version: 1.2.5
resolution: "@radix-ui/react-toast@npm:1.2.5"
@@ -7853,6 +8890,31 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-toggle-group@npm:1.1.6":
+ version: 1.1.6
+ resolution: "@radix-ui/react-toggle-group@npm:1.1.6"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-roving-focus": "npm:1.1.6"
+ "@radix-ui/react-toggle": "npm:1.1.6"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/e1a0da90f34b6307cdb55821e10887d8c777de5a9647c3c352ba9318aabef8c92dea312529dfa5c15e757bfff53af7435c2a6d82c6ec407650be959a2dfddcad
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-toggle@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-toggle@npm:1.1.1"
@@ -7874,6 +8936,27 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-toggle@npm:1.1.6":
+ version: 1.1.6
+ resolution: "@radix-ui/react-toggle@npm:1.1.6"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/f1bffbea682d023566dea5962b7756b8792edcd815e87faf5ac8e8b1e6b3252cd817dcba6f47b306d7188f9ce7dac7914811c4c8e17c678a90e606e7ceef65e7
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-toolbar@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-toolbar@npm:1.1.1"
@@ -7929,6 +9012,36 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-tooltip@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-tooltip@npm:1.2.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.2"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.4"
+ "@radix-ui/react-portal": "npm:1.1.6"
+ "@radix-ui/react-presence": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.0"
+ "@radix-ui/react-slot": "npm:1.2.0"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-visually-hidden": "npm:1.2.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10c0/d323d719a533aa3b4830ddb29eb812c54810060cca89be236dbbc7e99fe85e06718fa1f3f418b36e0526bc31dcc41d7d80aeffc0ea031b7a382efa2fe200d3df
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-tooltip@npm:^1.1.6":
version: 1.2.4
resolution: "@radix-ui/react-tooltip@npm:1.2.4"
@@ -9237,6 +10350,289 @@ __metadata:
languageName: node
linkType: hard
+"@react-email/body@npm:0.0.11":
+ version: 0.0.11
+ resolution: "@react-email/body@npm:0.0.11"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/4a86ea8041bf94992acde12efd79cd91f980b3c40a86792f98bd546e0b49f19fbdca1fc87d5d3e472a70535a3195f2738bbec1f9dec862d48c4bc625f599d3d6
+ languageName: node
+ linkType: hard
+
+"@react-email/button@npm:0.2.0":
+ version: 0.2.0
+ resolution: "@react-email/button@npm:0.2.0"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/1145d096ace8a459271f550faa941668122bd816adc6547390ede5becab5b0dc99a6f19c838e539fe53a73953bd4a4de790b1a7ad3d5981c44f5d34149045bfc
+ languageName: node
+ linkType: hard
+
+"@react-email/code-block@npm:0.1.0":
+ version: 0.1.0
+ resolution: "@react-email/code-block@npm:0.1.0"
+ dependencies:
+ prismjs: "npm:^1.30.0"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/55d84654cbf593e49102a11d1615e3beefb6943a82842ab1e51f435c6130a62f4ea2ebc034b127ebcc96822789b58cf352df340823278c9759bb3f031d8a796e
+ languageName: node
+ linkType: hard
+
+"@react-email/code-inline@npm:0.0.5":
+ version: 0.0.5
+ resolution: "@react-email/code-inline@npm:0.0.5"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/4081233686dd09575c580303f2822c415e1acc3c21847e64536afee60d37108810f5325e50a417ca39ab796fb21aae488108b4dcb55b0bb5e5b750f1019f32d7
+ languageName: node
+ linkType: hard
+
+"@react-email/column@npm:0.0.13":
+ version: 0.0.13
+ resolution: "@react-email/column@npm:0.0.13"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/56ebc6af1c546daca2c32b0700538242ab7d90ffff4be37bf9dcf929311b6996c66b39f506dfd37063bacd4382abdeab93baf8c1e3266aa62fe3b282b16a023f
+ languageName: node
+ linkType: hard
+
+"@react-email/components@npm:^0.3.2":
+ version: 0.3.2
+ resolution: "@react-email/components@npm:0.3.2"
+ dependencies:
+ "@react-email/body": "npm:0.0.11"
+ "@react-email/button": "npm:0.2.0"
+ "@react-email/code-block": "npm:0.1.0"
+ "@react-email/code-inline": "npm:0.0.5"
+ "@react-email/column": "npm:0.0.13"
+ "@react-email/container": "npm:0.0.15"
+ "@react-email/font": "npm:0.0.9"
+ "@react-email/head": "npm:0.0.12"
+ "@react-email/heading": "npm:0.0.15"
+ "@react-email/hr": "npm:0.0.11"
+ "@react-email/html": "npm:0.0.11"
+ "@react-email/img": "npm:0.0.11"
+ "@react-email/link": "npm:0.0.12"
+ "@react-email/markdown": "npm:0.0.15"
+ "@react-email/preview": "npm:0.0.13"
+ "@react-email/render": "npm:1.1.3"
+ "@react-email/row": "npm:0.0.12"
+ "@react-email/section": "npm:0.0.16"
+ "@react-email/tailwind": "npm:1.2.2"
+ "@react-email/text": "npm:0.1.5"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/8175a31f60f049f3d3babf952f318530e1807b31d002e1dea2d63e64503bcea6553524fa0ca67d9ecf9ea0f6e298934e61e83383b672adacef200e8d3d3d2e94
+ languageName: node
+ linkType: hard
+
+"@react-email/container@npm:0.0.15":
+ version: 0.0.15
+ resolution: "@react-email/container@npm:0.0.15"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/5098468b5336682f0f833a57cea1182bfe4a46bb03d5aa9fc85f96ab1e79845e30a9487c4c56094a9482465e94aa4395f10978c99b1772ca4bd0823d2d9071f6
+ languageName: node
+ linkType: hard
+
+"@react-email/font@npm:0.0.9":
+ version: 0.0.9
+ resolution: "@react-email/font@npm:0.0.9"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/b51c9bc22f3ea6e26c34fd5e4186be0b42a0414558f318b5784079145b267f7316729d54c8ba8c955ee5100e4b9ca9f5048c123a66ad2e277d49dd07c718287c
+ languageName: node
+ linkType: hard
+
+"@react-email/head@npm:0.0.12":
+ version: 0.0.12
+ resolution: "@react-email/head@npm:0.0.12"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/511147ea52330f5752e2d226a3417192adb5c0f301e35077a07fd66b99504eda6c3bfa49713a10400cf8542d75dfb317e5050b5383b285129db9854d5760d97d
+ languageName: node
+ linkType: hard
+
+"@react-email/heading@npm:0.0.15":
+ version: 0.0.15
+ resolution: "@react-email/heading@npm:0.0.15"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/74e58d2933eb6d37f4f3a6f82c63af6c9ffe2ce8cd14c7f6e27f8b76d39800cbf1a215d86e17a55d69eca4c73d5e9ea747ae1cd0a7df4a08a861a13afb9989dc
+ languageName: node
+ linkType: hard
+
+"@react-email/hr@npm:0.0.11":
+ version: 0.0.11
+ resolution: "@react-email/hr@npm:0.0.11"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/9d8199bdcfdc7e180636cdc11b7af5c44d185598fedd97dd0734103d662074ed8b7204e362c31f0dfd3925c37181e2fe22dbf1f76908c862d489f34e7efea7a9
+ languageName: node
+ linkType: hard
+
+"@react-email/html@npm:0.0.11":
+ version: 0.0.11
+ resolution: "@react-email/html@npm:0.0.11"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/04be96e135677cd507636877c2f9bf1a507d40f6d4442ff3fac58bc70d478e7f23dd03d94ecc1605d3e368d50129281e71f4b7afb242f1b32d285bfc37236a09
+ languageName: node
+ linkType: hard
+
+"@react-email/img@npm:0.0.11":
+ version: 0.0.11
+ resolution: "@react-email/img@npm:0.0.11"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/082e9f7d7290de4d340b06bc0ab0e4ffec08604885fdf06d93594e3684726279a3a65c0be2c83c9397bb832289078d533f0499b423ed3558f7736ad7d333d457
+ languageName: node
+ linkType: hard
+
+"@react-email/link@npm:0.0.12":
+ version: 0.0.12
+ resolution: "@react-email/link@npm:0.0.12"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/d831666bd52af9ba3a50bd4853e2b70387a041264e3781d3dab4708b755e8b2cb6df339f71eecd889b35221c205bd9997d47e950d1e3b226b4be50a22405ca87
+ languageName: node
+ linkType: hard
+
+"@react-email/markdown@npm:0.0.15":
+ version: 0.0.15
+ resolution: "@react-email/markdown@npm:0.0.15"
+ dependencies:
+ md-to-react-email: "npm:^5.0.5"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/68dc46d929ad0daf86f7d25e159455bdfbf4c0f0ca8ad152caf407f2eb154f4edb88912728cd5bba0c01f4806217e533e66baa02138c8ddcb5f2049719da7137
+ languageName: node
+ linkType: hard
+
+"@react-email/preview-server@npm:4.2.4":
+ version: 4.2.4
+ resolution: "@react-email/preview-server@npm:4.2.4"
+ dependencies:
+ "@babel/core": "npm:7.26.10"
+ "@babel/parser": "npm:7.27.0"
+ "@babel/traverse": "npm:7.27.0"
+ "@lottiefiles/dotlottie-react": "npm:0.13.3"
+ "@radix-ui/colors": "npm:3.0.0"
+ "@radix-ui/react-collapsible": "npm:1.1.7"
+ "@radix-ui/react-dropdown-menu": "npm:2.1.10"
+ "@radix-ui/react-popover": "npm:1.1.10"
+ "@radix-ui/react-slot": "npm:1.2.0"
+ "@radix-ui/react-tabs": "npm:1.1.7"
+ "@radix-ui/react-toggle-group": "npm:1.1.6"
+ "@radix-ui/react-tooltip": "npm:1.2.3"
+ "@types/node": "npm:22.14.1"
+ "@types/normalize-path": "npm:3.0.2"
+ "@types/react": "npm:19.0.10"
+ "@types/react-dom": "npm:19.0.4"
+ "@types/webpack": "npm:5.28.5"
+ autoprefixer: "npm:10.4.21"
+ chalk: "npm:4.1.2"
+ clsx: "npm:2.1.1"
+ esbuild: "npm:0.25.0"
+ framer-motion: "npm:12.7.5"
+ json5: "npm:2.2.3"
+ log-symbols: "npm:4.1.0"
+ module-punycode: "npm:punycode@2.3.1"
+ next: "npm:15.4.1"
+ node-html-parser: "npm:7.0.1"
+ ora: "npm:5.4.1"
+ pretty-bytes: "npm:6.1.1"
+ prism-react-renderer: "npm:2.4.1"
+ react: "npm:19.0.0"
+ react-dom: "npm:19.0.0"
+ sharp: "npm:0.34.1"
+ socket.io-client: "npm:4.8.1"
+ sonner: "npm:2.0.3"
+ source-map-js: "npm:1.2.1"
+ spamc: "npm:0.0.5"
+ stacktrace-parser: "npm:0.1.11"
+ tailwind-merge: "npm:3.2.0"
+ tailwindcss: "npm:3.4.0"
+ use-debounce: "npm:10.0.4"
+ zod: "npm:3.24.3"
+ checksum: 10c0/2501736146b0bc87b033501949a58e43ab0fced67a187a1e495db7dcee5c029fb3afd36f3fb0c67d47c027753688c12514342392de4efe1976fa731b68b0a760
+ languageName: node
+ linkType: hard
+
+"@react-email/preview@npm:0.0.13":
+ version: 0.0.13
+ resolution: "@react-email/preview@npm:0.0.13"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/223cff5238f0bfc4f1c3cdd1ae4f8b92cde16a60594d4d5073607b7af0b3cfaf5ae10234ca531d9403c7143afcba2e71bc12c1aba75e51f0ae58fd9c0b8bf542
+ languageName: node
+ linkType: hard
+
+"@react-email/render@npm:1.1.2":
+ version: 1.1.2
+ resolution: "@react-email/render@npm:1.1.2"
+ dependencies:
+ html-to-text: "npm:^9.0.5"
+ prettier: "npm:^3.5.3"
+ react-promise-suspense: "npm:^0.3.4"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/def0039a24b797d962d7dcb3a3401c093aa0115545284e00863de4066a826a3e4977b8f2c65b1f3bf77352bee5595e0dedf166ea52467dcb7080e14785e1c3ac
+ languageName: node
+ linkType: hard
+
+"@react-email/render@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@react-email/render@npm:1.1.3"
+ dependencies:
+ html-to-text: "npm:^9.0.5"
+ prettier: "npm:^3.5.3"
+ react-promise-suspense: "npm:^0.3.4"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/d230346d05c30b5ac3becdea73e6e4a4774a3bc7bf9f93b4edf2b4058a0050835edf54622375be4e5cb924e589b14908643165ebcf301fe3d0286cc3f5cd1e23
+ languageName: node
+ linkType: hard
+
+"@react-email/row@npm:0.0.12":
+ version: 0.0.12
+ resolution: "@react-email/row@npm:0.0.12"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/440c54071543700ce7a0db4749c93c56fa0cfae1057464231b25d9d7598f81a2f395816b922376acd5445c29163ae2858e4fc840d8cf30f3d4520104f7f5e3ed
+ languageName: node
+ linkType: hard
+
+"@react-email/section@npm:0.0.16":
+ version: 0.0.16
+ resolution: "@react-email/section@npm:0.0.16"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/48bfacdd78d403b50c4c1098d100a96f3afe6159d910374a0306d6186daa1bf3803b4a2b3163fb38d9137835041b9f70145c7bb2f652ba347a82a0d396fe2ef2
+ languageName: node
+ linkType: hard
+
+"@react-email/tailwind@npm:1.2.2":
+ version: 1.2.2
+ resolution: "@react-email/tailwind@npm:1.2.2"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/fa7d0a4d518a77cf0ec26c57aeac8a24509344225a19d6f3c1320a4357d88b2d48e592f312286ec59ef17333d875e3f3437cd09102d793896229e3f5502d079e
+ languageName: node
+ linkType: hard
+
+"@react-email/text@npm:0.1.5":
+ version: 0.1.5
+ resolution: "@react-email/text@npm:0.1.5"
+ peerDependencies:
+ react: ^18.0 || ^19.0 || ^19.0.0-rc
+ checksum: 10c0/ad825332903917d1185ce7c5e4d19846496565996f82b71a873648dffc6342d382606a96a5a00a15a16abef5322540d56b481ad3ca21c8b34de9b598715ec149
+ languageName: node
+ linkType: hard
+
"@react-router/dev@npm:^7.5.3":
version: 7.5.3
resolution: "@react-router/dev@npm:7.5.3"
@@ -10037,6 +11433,26 @@ __metadata:
languageName: node
linkType: hard
+"@remix-run/react@npm:^2.16.8":
+ version: 2.16.8
+ resolution: "@remix-run/react@npm:2.16.8"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ "@remix-run/server-runtime": "npm:2.16.8"
+ react-router: "npm:6.30.0"
+ react-router-dom: "npm:6.30.0"
+ turbo-stream: "npm:2.4.1"
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ typescript: ^5.1.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/f263064254559ac06c2c39547517f89dd0e8bc7ac8bf4b7d86933f60cfaa848816d47c99865b6db1ba63222bde52d92dd8da2ae4c676981b190cfe855878ee96
+ languageName: node
+ linkType: hard
+
"@remix-run/router@npm:1.13.1":
version: 1.13.1
resolution: "@remix-run/router@npm:1.13.1"
@@ -10044,6 +11460,13 @@ __metadata:
languageName: node
linkType: hard
+"@remix-run/router@npm:1.23.0":
+ version: 1.23.0
+ resolution: "@remix-run/router@npm:1.23.0"
+ checksum: 10c0/eaef5cb46a1e413f7d1019a75990808307e08e53a39d4cf69c339432ddc03143d725decef3d6b9b5071b898da07f72a4a57c4e73f787005fcf10162973d8d7d7
+ languageName: node
+ linkType: hard
+
"@remix-run/router@npm:1.x":
version: 1.22.0
resolution: "@remix-run/router@npm:1.22.0"
@@ -10051,6 +11474,26 @@ __metadata:
languageName: node
linkType: hard
+"@remix-run/server-runtime@npm:2.16.8":
+ version: 2.16.8
+ resolution: "@remix-run/server-runtime@npm:2.16.8"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ "@types/cookie": "npm:^0.6.0"
+ "@web3-storage/multipart-parser": "npm:^1.0.0"
+ cookie: "npm:^0.7.2"
+ set-cookie-parser: "npm:^2.4.8"
+ source-map: "npm:^0.7.3"
+ turbo-stream: "npm:2.4.1"
+ peerDependencies:
+ typescript: ^5.1.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/81cbb16b8ce263463b1ca837bd92fee8d93c2f63ce5cebbc7f80befc699d9976d6b234e3a8601cf3e86a547a17da70233a32b7b0c3ff39b4cb676574217c61df
+ languageName: node
+ linkType: hard
+
"@rollup/pluginutils@npm:^5.0.5":
version: 5.1.4
resolution: "@rollup/pluginutils@npm:5.1.4"
@@ -10388,6 +11831,16 @@ __metadata:
languageName: node
linkType: hard
+"@selderee/plugin-htmlparser2@npm:^0.11.0":
+ version: 0.11.0
+ resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
+ dependencies:
+ domhandler: "npm:^5.0.3"
+ selderee: "npm:^0.11.0"
+ checksum: 10c0/e938ba9aeb31a9cf30dcb2977ef41685c598bf744bedc88c57aa9e8b7e71b51781695cf99c08aac50773fd7714eba670bd2a079e46db0788abe40c6d220084eb
+ languageName: node
+ linkType: hard
+
"@sendgrid/client@npm:^8.1.4":
version: 8.1.4
resolution: "@sendgrid/client@npm:8.1.4"
@@ -11320,6 +12773,20 @@ __metadata:
languageName: node
linkType: hard
+"@socket.io/component-emitter@npm:~3.1.0":
+ version: 3.1.2
+ resolution: "@socket.io/component-emitter@npm:3.1.2"
+ checksum: 10c0/c4242bad66f67e6f7b712733d25b43cbb9e19a595c8701c3ad99cbeb5901555f78b095e24852f862fffb43e96f1d8552e62def885ca82ae1bb05da3668fd87d7
+ languageName: node
+ linkType: hard
+
+"@standard-schema/utils@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "@standard-schema/utils@npm:0.3.0"
+ checksum: 10c0/6eb74cd13e52d5fc74054df51e37d947ef53f3ab9e02c085665dcca3c38c60ece8d735cebbdf18fbb13c775fbcb9becb3f53109b0e092a63f0f7389ce0993fd0
+ languageName: node
+ linkType: hard
+
"@stdlib/array-float32@npm:^0.0.x":
version: 0.0.6
resolution: "@stdlib/array-float32@npm:0.0.6"
@@ -12663,7 +14130,7 @@ __metadata:
languageName: node
linkType: hard
-"@swc/helpers@npm:^0.5.0":
+"@swc/helpers@npm:0.5.15, @swc/helpers@npm:^0.5.0":
version: 0.5.15
resolution: "@swc/helpers@npm:0.5.15"
dependencies:
@@ -12771,6 +14238,13 @@ __metadata:
languageName: node
linkType: hard
+"@tanstack/query-core@npm:5.80.6":
+ version: 5.80.6
+ resolution: "@tanstack/query-core@npm:5.80.6"
+ checksum: 10c0/ce52d962036bf84845c9dafb654075bbc8eafd9236069b7c234b7f72a30e4e61daf222940d9f28b4359858277cc1e1d08dd1f8e6cc0adac72acc083fc5c0195c
+ languageName: node
+ linkType: hard
+
"@tanstack/react-query@npm:5.64.2":
version: 5.64.2
resolution: "@tanstack/react-query@npm:5.64.2"
@@ -12782,6 +14256,17 @@ __metadata:
languageName: node
linkType: hard
+"@tanstack/react-query@npm:^5.80.6":
+ version: 5.80.6
+ resolution: "@tanstack/react-query@npm:5.80.6"
+ dependencies:
+ "@tanstack/query-core": "npm:5.80.6"
+ peerDependencies:
+ react: ^18 || ^19
+ checksum: 10c0/ec2dc548ff28d92778e851d64b0c24e3289218b496694c0b7bed1e92b5b96b03f8b047d4a8a04c6924937c56e257d53fbe42e046745da10d9dababe48505f843
+ languageName: node
+ linkType: hard
+
"@tanstack/react-table@npm:8.20.5":
version: 8.20.5
resolution: "@tanstack/react-table@npm:8.20.5"
@@ -13040,6 +14525,49 @@ __metadata:
languageName: node
linkType: hard
+"@types/cookie@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "@types/cookie@npm:0.6.0"
+ checksum: 10c0/5b326bd0188120fb32c0be086b141b1481fec9941b76ad537f9110e10d61ee2636beac145463319c71e4be67a17e85b81ca9e13ceb6e3bb63b93d16824d6c149
+ languageName: node
+ linkType: hard
+
+"@types/cors@npm:^2.8.12":
+ version: 2.8.19
+ resolution: "@types/cors@npm:2.8.19"
+ dependencies:
+ "@types/node": "npm:*"
+ checksum: 10c0/b5dd407040db7d8aa1bd36e79e5f3f32292f6b075abc287529e9f48df1a25fda3e3799ba30b4656667ffb931d3b75690c1d6ca71e39f7337ea6dfda8581916d0
+ languageName: node
+ linkType: hard
+
+"@types/eslint-scope@npm:^3.7.7":
+ version: 3.7.7
+ resolution: "@types/eslint-scope@npm:3.7.7"
+ dependencies:
+ "@types/eslint": "npm:*"
+ "@types/estree": "npm:*"
+ checksum: 10c0/a0ecbdf2f03912679440550817ff77ef39a30fa8bfdacaf6372b88b1f931828aec392f52283240f0d648cf3055c5ddc564544a626bcf245f3d09fcb099ebe3cc
+ languageName: node
+ linkType: hard
+
+"@types/eslint@npm:*":
+ version: 9.6.1
+ resolution: "@types/eslint@npm:9.6.1"
+ dependencies:
+ "@types/estree": "npm:*"
+ "@types/json-schema": "npm:*"
+ checksum: 10c0/69ba24fee600d1e4c5abe0df086c1a4d798abf13792d8cfab912d76817fe1a894359a1518557d21237fbaf6eda93c5ab9309143dee4c59ef54336d1b3570420e
+ languageName: node
+ linkType: hard
+
+"@types/estree@npm:*, @types/estree@npm:^1.0.8":
+ version: 1.0.8
+ resolution: "@types/estree@npm:1.0.8"
+ checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
+ languageName: node
+ linkType: hard
+
"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0":
version: 1.0.6
resolution: "@types/estree@npm:1.0.6"
@@ -13198,7 +14726,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/json-schema@npm:^7.0.9":
+"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
@@ -13288,6 +14816,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/luxon@npm:^3.6.2":
+ version: 3.6.2
+ resolution: "@types/luxon@npm:3.6.2"
+ checksum: 10c0/7572ee52b3d3e9dd10464b90561a728b90f34b9a257751cc3ce23762693dd1d14fa98b7f103e2efe2c6f49033331f91de5681ffd65cca88618cefe555be326db
+ languageName: node
+ linkType: hard
+
"@types/markdown-it@npm:^14.1.1":
version: 14.1.2
resolution: "@types/markdown-it@npm:14.1.2"
@@ -13369,6 +14904,24 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:22.14.1":
+ version: 22.14.1
+ resolution: "@types/node@npm:22.14.1"
+ dependencies:
+ undici-types: "npm:~6.21.0"
+ checksum: 10c0/d49c4d00403b1c2348cf0701b505fd636d80aabe18102105998dc62fdd36dcaf911e73c7a868c48c21c1022b825c67b475b65b1222d84b704d8244d152bb7f86
+ languageName: node
+ linkType: hard
+
+"@types/node@npm:>=10.0.0":
+ version: 24.1.0
+ resolution: "@types/node@npm:24.1.0"
+ dependencies:
+ undici-types: "npm:~7.8.0"
+ checksum: 10c0/6c4686bc144f6ce7bffd4cadc3e1196e2217c1da4c639c637213719c8a3ee58b6c596b994befcbffeacd9d9eb0c3bff6529d2bc27da5d1cb9d58b1da0056f9f4
+ languageName: node
+ linkType: hard
+
"@types/node@npm:^17.0.8":
version: 17.0.45
resolution: "@types/node@npm:17.0.45"
@@ -13376,6 +14929,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/normalize-path@npm:3.0.2":
+ version: 3.0.2
+ resolution: "@types/normalize-path@npm:3.0.2"
+ checksum: 10c0/c9ad01c8b396b88b415e35d53bbe9d6012fc09b431046d94a98bf2849e969d68d36be89ecacf1897aec7b3477f006b68a40d869143e273c902c35f715b1251fe
+ languageName: node
+ linkType: hard
+
"@types/parse-json@npm:^4.0.0":
version: 4.0.2
resolution: "@types/parse-json@npm:4.0.2"
@@ -13456,6 +15016,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-dom@npm:19.0.4":
+ version: 19.0.4
+ resolution: "@types/react-dom@npm:19.0.4"
+ peerDependencies:
+ "@types/react": ^19.0.0
+ checksum: 10c0/4e71853919b94df9e746a4bd73f8180e9ae13016333ce9c543dcba9f4f4c8fe6e28b038ca6ee61c24e291af8e03ca3bc5ded17c46dee938fcb32d71186fda7a3
+ languageName: node
+ linkType: hard
+
"@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.14":
version: 18.3.5
resolution: "@types/react-dom@npm:18.3.5"
@@ -13465,7 +15034,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/react@npm:*":
+"@types/react@npm:*, @types/react@npm:19.0.10":
version: 19.0.10
resolution: "@types/react@npm:19.0.10"
dependencies:
@@ -13577,6 +15146,17 @@ __metadata:
languageName: node
linkType: hard
+"@types/webpack@npm:5.28.5":
+ version: 5.28.5
+ resolution: "@types/webpack@npm:5.28.5"
+ dependencies:
+ "@types/node": "npm:*"
+ tapable: "npm:^2.2.0"
+ webpack: "npm:^5"
+ checksum: 10c0/d1fec1f678af79dd0d84333740bdd2ce128c7f4fd1f8d14bb0ec0f4c5af70580af68350f677e3696003a8cb2e2b5f510f8c0ed7ee5ad2bc7874c87f80ecd7c8d
+ languageName: node
+ linkType: hard
+
"@types/wrap-ansi@npm:^3.0.0":
version: 3.0.0
resolution: "@types/wrap-ansi@npm:3.0.0"
@@ -13817,6 +15397,178 @@ __metadata:
languageName: node
linkType: hard
+"@web3-storage/multipart-parser@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "@web3-storage/multipart-parser@npm:1.0.0"
+ checksum: 10c0/1cdf5bbb5a40d151a4c6ebf00e7e2f1075bd91d08d5c7259e683a4b5d31e697ad594024644dcf547f297fdef39d39b75a7edb2b234720f80e8e860284022aa96
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/ast@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/helper-numbers": "npm:1.13.2"
+ "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2"
+ checksum: 10c0/67a59be8ed50ddd33fbb2e09daa5193ac215bf7f40a9371be9a0d9797a114d0d1196316d2f3943efdb923a3d809175e1563a3cb80c814fb8edccd1e77494972b
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/floating-point-hex-parser@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/floating-point-hex-parser@npm:1.13.2"
+ checksum: 10c0/0e88bdb8b50507d9938be64df0867f00396b55eba9df7d3546eb5dc0ca64d62e06f8d881ec4a6153f2127d0f4c11d102b6e7d17aec2f26bb5ff95a5e60652412
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/helper-api-error@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-api-error@npm:1.13.2"
+ checksum: 10c0/31be497f996ed30aae4c08cac3cce50c8dcd5b29660383c0155fce1753804fc55d47fcba74e10141c7dd2899033164e117b3bcfcda23a6b043e4ded4f1003dfb
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/helper-buffer@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/helper-buffer@npm:1.14.1"
+ checksum: 10c0/0d54105dc373c0fe6287f1091e41e3a02e36cdc05e8cf8533cdc16c59ff05a646355415893449d3768cda588af451c274f13263300a251dc11a575bc4c9bd210
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/helper-numbers@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-numbers@npm:1.13.2"
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser": "npm:1.13.2"
+ "@webassemblyjs/helper-api-error": "npm:1.13.2"
+ "@xtuc/long": "npm:4.2.2"
+ checksum: 10c0/9c46852f31b234a8fb5a5a9d3f027bc542392a0d4de32f1a9c0075d5e8684aa073cb5929b56df565500b3f9cc0a2ab983b650314295b9bf208d1a1651bfc825a
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/helper-wasm-bytecode@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/helper-wasm-bytecode@npm:1.13.2"
+ checksum: 10c0/c4355d14f369b30cf3cbdd3acfafc7d0488e086be6d578e3c9780bd1b512932352246be96e034e2a7fcfba4f540ec813352f312bfcbbfe5bcfbf694f82ccc682
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/helper-wasm-section@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/helper-wasm-section@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@webassemblyjs/helper-buffer": "npm:1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2"
+ "@webassemblyjs/wasm-gen": "npm:1.14.1"
+ checksum: 10c0/1f9b33731c3c6dbac3a9c483269562fa00d1b6a4e7133217f40e83e975e636fd0f8736e53abd9a47b06b66082ecc976c7384391ab0a68e12d509ea4e4b948d64
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/ieee754@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/ieee754@npm:1.13.2"
+ dependencies:
+ "@xtuc/ieee754": "npm:^1.2.0"
+ checksum: 10c0/2e732ca78c6fbae3c9b112f4915d85caecdab285c0b337954b180460290ccd0fb00d2b1dc4bb69df3504abead5191e0d28d0d17dfd6c9d2f30acac8c4961c8a7
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/leb128@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/leb128@npm:1.13.2"
+ dependencies:
+ "@xtuc/long": "npm:4.2.2"
+ checksum: 10c0/dad5ef9e383c8ab523ce432dfd80098384bf01c45f70eb179d594f85ce5db2f80fa8c9cba03adafd85684e6d6310f0d3969a882538975989919329ac4c984659
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/utf8@npm:1.13.2":
+ version: 1.13.2
+ resolution: "@webassemblyjs/utf8@npm:1.13.2"
+ checksum: 10c0/d3fac9130b0e3e5a1a7f2886124a278e9323827c87a2b971e6d0da22a2ba1278ac9f66a4f2e363ecd9fac8da42e6941b22df061a119e5c0335f81006de9ee799
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/wasm-edit@npm:^1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-edit@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@webassemblyjs/helper-buffer": "npm:1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2"
+ "@webassemblyjs/helper-wasm-section": "npm:1.14.1"
+ "@webassemblyjs/wasm-gen": "npm:1.14.1"
+ "@webassemblyjs/wasm-opt": "npm:1.14.1"
+ "@webassemblyjs/wasm-parser": "npm:1.14.1"
+ "@webassemblyjs/wast-printer": "npm:1.14.1"
+ checksum: 10c0/5ac4781086a2ca4b320bdbfd965a209655fe8a208ca38d89197148f8597e587c9a2c94fb6bd6f1a7dbd4527c49c6844fcdc2af981f8d793a97bf63a016aa86d2
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/wasm-gen@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-gen@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2"
+ "@webassemblyjs/ieee754": "npm:1.13.2"
+ "@webassemblyjs/leb128": "npm:1.13.2"
+ "@webassemblyjs/utf8": "npm:1.13.2"
+ checksum: 10c0/d678810d7f3f8fecb2e2bdadfb9afad2ec1d2bc79f59e4711ab49c81cec578371e22732d4966f59067abe5fba8e9c54923b57060a729d28d408e608beef67b10
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/wasm-opt@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-opt@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@webassemblyjs/helper-buffer": "npm:1.14.1"
+ "@webassemblyjs/wasm-gen": "npm:1.14.1"
+ "@webassemblyjs/wasm-parser": "npm:1.14.1"
+ checksum: 10c0/515bfb15277ee99ba6b11d2232ddbf22aed32aad6d0956fe8a0a0a004a1b5a3a277a71d9a3a38365d0538ac40d1b7b7243b1a244ad6cd6dece1c1bb2eb5de7ee
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/wasm-parser@npm:1.14.1, @webassemblyjs/wasm-parser@npm:^1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wasm-parser@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@webassemblyjs/helper-api-error": "npm:1.13.2"
+ "@webassemblyjs/helper-wasm-bytecode": "npm:1.13.2"
+ "@webassemblyjs/ieee754": "npm:1.13.2"
+ "@webassemblyjs/leb128": "npm:1.13.2"
+ "@webassemblyjs/utf8": "npm:1.13.2"
+ checksum: 10c0/95427b9e5addbd0f647939bd28e3e06b8deefdbdadcf892385b5edc70091bf9b92fa5faac3fce8333554437c5d85835afef8c8a7d9d27ab6ba01ffab954db8c6
+ languageName: node
+ linkType: hard
+
+"@webassemblyjs/wast-printer@npm:1.14.1":
+ version: 1.14.1
+ resolution: "@webassemblyjs/wast-printer@npm:1.14.1"
+ dependencies:
+ "@webassemblyjs/ast": "npm:1.14.1"
+ "@xtuc/long": "npm:4.2.2"
+ checksum: 10c0/8d7768608996a052545251e896eac079c98e0401842af8dd4de78fba8d90bd505efb6c537e909cd6dae96e09db3fa2e765a6f26492553a675da56e2db51f9d24
+ languageName: node
+ linkType: hard
+
+"@xtuc/ieee754@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "@xtuc/ieee754@npm:1.2.0"
+ checksum: 10c0/a8565d29d135039bd99ae4b2220d3e167d22cf53f867e491ed479b3f84f895742d0097f935b19aab90265a23d5d46711e4204f14c479ae3637fbf06c4666882f
+ languageName: node
+ linkType: hard
+
+"@xtuc/long@npm:4.2.2":
+ version: 4.2.2
+ resolution: "@xtuc/long@npm:4.2.2"
+ checksum: 10c0/8582cbc69c79ad2d31568c412129bf23d2b1210a1dfb60c82d5a1df93334da4ee51f3057051658569e2c196d8dc33bc05ae6b974a711d0d16e801e1d0647ccd1
+ languageName: node
+ linkType: hard
+
"abab@npm:^2.0.6":
version: 2.0.6
resolution: "abab@npm:2.0.6"
@@ -13840,7 +15592,7 @@ __metadata:
languageName: node
linkType: hard
-"accepts@npm:~1.3.5, accepts@npm:~1.3.8":
+"accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8":
version: 1.3.8
resolution: "accepts@npm:1.3.8"
dependencies:
@@ -13869,6 +15621,15 @@ __metadata:
languageName: node
linkType: hard
+"acorn-import-phases@npm:^1.0.3":
+ version: 1.0.4
+ resolution: "acorn-import-phases@npm:1.0.4"
+ peerDependencies:
+ acorn: ^8.14.0
+ checksum: 10c0/338eb46fc1aed5544f628344cb9af189450b401d152ceadbf1f5746901a5d923016cd0e7740d5606062d374fdf6941c29bb515d2bd133c4f4242d5d4cd73a3c7
+ languageName: node
+ linkType: hard
+
"acorn-jsx@npm:^5.3.2":
version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2"
@@ -13896,6 +15657,15 @@ __metadata:
languageName: node
linkType: hard
+"acorn@npm:^8.15.0":
+ version: 8.15.0
+ resolution: "acorn@npm:8.15.0"
+ bin:
+ acorn: bin/acorn
+ checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec
+ languageName: node
+ linkType: hard
+
"agent-base@npm:6":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
@@ -13924,6 +15694,20 @@ __metadata:
languageName: node
linkType: hard
+"ajv-formats@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "ajv-formats@npm:2.1.1"
+ dependencies:
+ ajv: "npm:^8.0.0"
+ peerDependencies:
+ ajv: ^8.0.0
+ peerDependenciesMeta:
+ ajv:
+ optional: true
+ checksum: 10c0/e43ba22e91b6a48d96224b83d260d3a3a561b42d391f8d3c6d2c1559f9aa5b253bfb306bc94bbeca1d967c014e15a6efe9a207309e95b3eaae07fcbcdc2af662
+ languageName: node
+ linkType: hard
+
"ajv-formats@npm:~3.0.1":
version: 3.0.1
resolution: "ajv-formats@npm:3.0.1"
@@ -13938,6 +15722,17 @@ __metadata:
languageName: node
linkType: hard
+"ajv-keywords@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "ajv-keywords@npm:5.1.0"
+ dependencies:
+ fast-deep-equal: "npm:^3.1.3"
+ peerDependencies:
+ ajv: ^8.8.2
+ checksum: 10c0/18bec51f0171b83123ba1d8883c126e60c6f420cef885250898bf77a8d3e65e3bfb9e8564f497e30bdbe762a83e0d144a36931328616a973ee669dc74d4a9590
+ languageName: node
+ linkType: hard
+
"ajv@npm:^6.10.0, ajv@npm:^6.12.4":
version: 6.12.6
resolution: "ajv@npm:6.12.6"
@@ -13950,7 +15745,7 @@ __metadata:
languageName: node
linkType: hard
-"ajv@npm:^8.0.0":
+"ajv@npm:^8.0.0, ajv@npm:^8.9.0":
version: 8.17.1
resolution: "ajv@npm:8.17.1"
dependencies:
@@ -14282,6 +16077,24 @@ __metadata:
languageName: node
linkType: hard
+"autoprefixer@npm:10.4.21":
+ version: 10.4.21
+ resolution: "autoprefixer@npm:10.4.21"
+ dependencies:
+ browserslist: "npm:^4.24.4"
+ caniuse-lite: "npm:^1.0.30001702"
+ fraction.js: "npm:^4.3.7"
+ normalize-range: "npm:^0.1.2"
+ picocolors: "npm:^1.1.1"
+ postcss-value-parser: "npm:^4.2.0"
+ peerDependencies:
+ postcss: ^8.1.0
+ bin:
+ autoprefixer: bin/autoprefixer
+ checksum: 10c0/de5b71d26d0baff4bbfb3d59f7cf7114a6030c9eeb66167acf49a32c5b61c68e308f1e0f869d92334436a221035d08b51cd1b2f2c4689b8d955149423c16d4d4
+ languageName: node
+ linkType: hard
+
"autoprefixer@npm:^10.4.16":
version: 10.4.20
resolution: "autoprefixer@npm:10.4.20"
@@ -14535,6 +16348,13 @@ __metadata:
languageName: node
linkType: hard
+"base64id@npm:2.0.0, base64id@npm:~2.0.0":
+ version: 2.0.0
+ resolution: "base64id@npm:2.0.0"
+ checksum: 10c0/6919efd237ed44b9988cbfc33eca6f173a10e810ce50292b271a1a421aac7748ef232a64d1e6032b08f19aae48dce6ee8f66c5ae2c9e5066c82b884861d4d453
+ languageName: node
+ linkType: hard
+
"basic-auth@npm:~2.0.1":
version: 2.0.1
resolution: "basic-auth@npm:2.0.1"
@@ -14640,6 +16460,13 @@ __metadata:
languageName: node
linkType: hard
+"boolbase@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "boolbase@npm:1.0.0"
+ checksum: 10c0/e4b53deb4f2b85c52be0e21a273f2045c7b6a6ea002b0e139c744cb6f95e9ec044439a52883b0d74dedd1ff3da55ed140cfdddfed7fb0cccbed373de5dce1bcf
+ languageName: node
+ linkType: hard
+
"bowser@npm:^2.11.0":
version: 2.11.0
resolution: "bowser@npm:2.11.0"
@@ -14705,6 +16532,20 @@ __metadata:
languageName: node
linkType: hard
+"browserslist@npm:^4.24.4":
+ version: 4.25.1
+ resolution: "browserslist@npm:4.25.1"
+ dependencies:
+ caniuse-lite: "npm:^1.0.30001726"
+ electron-to-chromium: "npm:^1.5.173"
+ node-releases: "npm:^2.0.19"
+ update-browserslist-db: "npm:^1.1.3"
+ bin:
+ browserslist: cli.js
+ checksum: 10c0/acba5f0bdbd5e72dafae1e6ec79235b7bad305ed104e082ed07c34c38c7cb8ea1bc0f6be1496958c40482e40166084458fc3aee15111f15faa79212ad9081b2a
+ languageName: node
+ linkType: hard
+
"bs-logger@npm:0.x":
version: 0.2.6
resolution: "bs-logger@npm:0.2.6"
@@ -14928,6 +16769,13 @@ __metadata:
languageName: node
linkType: hard
+"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001726":
+ version: 1.0.30001727
+ resolution: "caniuse-lite@npm:1.0.30001727"
+ checksum: 10c0/f0a441c05d8925d728c2d02ce23b001935f52183a3bf669556f302568fe258d1657940c7ac0b998f92bc41383e185b390279a7d779e6d96a2b47881f56400221
+ languageName: node
+ linkType: hard
+
"capital-case@npm:^1.0.4":
version: 1.0.4
resolution: "capital-case@npm:1.0.4"
@@ -14960,7 +16808,7 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
+"chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
dependencies:
@@ -14970,7 +16818,7 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:^5.3.0":
+"chalk@npm:^5.0.0, chalk@npm:^5.3.0":
version: 5.4.1
resolution: "chalk@npm:5.4.1"
checksum: 10c0/b23e88132c702f4855ca6d25cb5538b1114343e41472d5263ee8a37cccfccd9c4216d111e1097c6a27830407a1dc81fecdf2a56f2c63033d4dbbd88c10b0dcef
@@ -15067,7 +16915,7 @@ __metadata:
languageName: node
linkType: hard
-"chokidar@npm:^4.0.0":
+"chokidar@npm:^4.0.0, chokidar@npm:^4.0.3":
version: 4.0.3
resolution: "chokidar@npm:4.0.3"
dependencies:
@@ -15083,6 +16931,13 @@ __metadata:
languageName: node
linkType: hard
+"chrome-trace-event@npm:^1.0.2":
+ version: 1.0.4
+ resolution: "chrome-trace-event@npm:1.0.4"
+ checksum: 10c0/3058da7a5f4934b87cf6a90ef5fb68ebc5f7d06f143ed5a4650208e5d7acae47bc03ec844b29fbf5ba7e46e8daa6acecc878f7983a4f4bb7271593da91e61ff5
+ languageName: node
+ linkType: hard
+
"ci-info@npm:^3.2.0":
version: 3.9.0
resolution: "ci-info@npm:3.9.0"
@@ -15090,6 +16945,15 @@ __metadata:
languageName: node
linkType: hard
+"citty@npm:^0.1.6":
+ version: 0.1.6
+ resolution: "citty@npm:0.1.6"
+ dependencies:
+ consola: "npm:^3.2.3"
+ checksum: 10c0/d26ad82a9a4a8858c7e149d90b878a3eceecd4cfd3e2ed3cd5f9a06212e451fb4f8cbe0fa39a3acb1b3e8f18e22db8ee5def5829384bad50e823d4b301609b48
+ languageName: node
+ linkType: hard
+
"cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2":
version: 1.4.3
resolution: "cjs-module-lexer@npm:1.4.3"
@@ -15138,6 +17002,15 @@ __metadata:
languageName: node
linkType: hard
+"cli-cursor@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "cli-cursor@npm:5.0.0"
+ dependencies:
+ restore-cursor: "npm:^5.0.0"
+ checksum: 10c0/7ec62f69b79f6734ab209a3e4dbdc8af7422d44d360a7cb1efa8a0887bbe466a6e625650c466fe4359aee44dbe2dc0b6994b583d40a05d0808a5cb193641d220
+ languageName: node
+ linkType: hard
+
"cli-progress@npm:^3.4.0":
version: 3.12.0
resolution: "cli-progress@npm:3.12.0"
@@ -15147,7 +17020,7 @@ __metadata:
languageName: node
linkType: hard
-"cli-spinners@npm:^2.5.0":
+"cli-spinners@npm:^2.5.0, cli-spinners@npm:^2.9.2":
version: 2.9.2
resolution: "cli-spinners@npm:2.9.2"
checksum: 10c0/907a1c227ddf0d7a101e7ab8b300affc742ead4b4ebe920a5bf1bc6d45dce2958fcd195eb28fa25275062fe6fa9b109b93b63bc8033396ed3bcb50297008b3a3
@@ -15202,6 +17075,13 @@ __metadata:
languageName: node
linkType: hard
+"client-only@npm:0.0.1":
+ version: 0.0.1
+ resolution: "client-only@npm:0.0.1"
+ checksum: 10c0/9d6cfd0c19e1c96a434605added99dff48482152af791ec4172fb912a71cff9027ff174efd8cdb2160cc7f377543e0537ffc462d4f279bc4701de3f2a3c4b358
+ languageName: node
+ linkType: hard
+
"cliui@npm:^6.0.0":
version: 6.0.0
resolution: "cliui@npm:6.0.0"
@@ -15265,6 +17145,13 @@ __metadata:
languageName: node
linkType: hard
+"clsx@npm:2.1.1, clsx@npm:^2.0.0, clsx@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "clsx@npm:2.1.1"
+ checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839
+ languageName: node
+ linkType: hard
+
"clsx@npm:^1.2.1":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
@@ -15272,13 +17159,6 @@ __metadata:
languageName: node
linkType: hard
-"clsx@npm:^2.0.0, clsx@npm:^2.1.1":
- version: 2.1.1
- resolution: "clsx@npm:2.1.1"
- checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839
- languageName: node
- linkType: hard
-
"cluster-key-slot@npm:^1.1.0":
version: 1.1.2
resolution: "cluster-key-slot@npm:1.1.2"
@@ -15429,6 +17309,20 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^13.0.0":
+ version: 13.1.0
+ resolution: "commander@npm:13.1.0"
+ checksum: 10c0/7b8c5544bba704fbe84b7cab2e043df8586d5c114a4c5b607f83ae5060708940ed0b5bd5838cf8ce27539cde265c1cbd59ce3c8c6b017ed3eec8943e3a415164
+ languageName: node
+ linkType: hard
+
+"commander@npm:^2.20.0":
+ version: 2.20.3
+ resolution: "commander@npm:2.20.3"
+ checksum: 10c0/74c781a5248c2402a0a3e966a0a2bba3c054aad144f5c023364be83265e796b20565aa9feff624132ff629aa64e16999fa40a743c10c12f7c61e96a794b99288
+ languageName: node
+ linkType: hard
+
"commander@npm:^4.0.0":
version: 4.1.1
resolution: "commander@npm:4.1.1"
@@ -15535,6 +17429,13 @@ __metadata:
languageName: node
linkType: hard
+"confbox@npm:^0.2.2":
+ version: 0.2.2
+ resolution: "confbox@npm:0.2.2"
+ checksum: 10c0/7c246588d533d31e8cdf66cb4701dff6de60f9be77ab54c0d0338e7988750ac56863cc0aca1b3f2046f45ff223a765d3e5d4977a7674485afcd37b6edf3fd129
+ languageName: node
+ linkType: hard
+
"config-chain@npm:^1.1.13":
version: 1.1.13
resolution: "config-chain@npm:1.1.13"
@@ -15566,6 +17467,13 @@ __metadata:
languageName: node
linkType: hard
+"consola@npm:^3.2.3, consola@npm:^3.4.0":
+ version: 3.4.2
+ resolution: "consola@npm:3.4.2"
+ checksum: 10c0/7cebe57ecf646ba74b300bcce23bff43034ed6fbec9f7e39c27cee1dc00df8a21cd336b466ad32e304ea70fba04ec9e890c200270de9a526ce021ba8a7e4c11a
+ languageName: node
+ linkType: hard
+
"constant-case@npm:^3.0.4":
version: 3.0.4
resolution: "constant-case@npm:3.0.4"
@@ -15638,7 +17546,7 @@ __metadata:
languageName: node
linkType: hard
-"cookie@npm:0.7.2":
+"cookie@npm:0.7.2, cookie@npm:^0.7.2, cookie@npm:~0.7.2":
version: 0.7.2
resolution: "cookie@npm:0.7.2"
checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2
@@ -15675,7 +17583,7 @@ __metadata:
languageName: node
linkType: hard
-"cors@npm:^2.8.5":
+"cors@npm:^2.8.5, cors@npm:~2.8.5":
version: 2.8.5
resolution: "cors@npm:2.8.5"
dependencies:
@@ -15794,6 +17702,19 @@ __metadata:
languageName: node
linkType: hard
+"css-select@npm:^5.1.0":
+ version: 5.2.2
+ resolution: "css-select@npm:5.2.2"
+ dependencies:
+ boolbase: "npm:^1.0.0"
+ css-what: "npm:^6.1.0"
+ domhandler: "npm:^5.0.2"
+ domutils: "npm:^3.0.1"
+ nth-check: "npm:^2.0.1"
+ checksum: 10c0/d79fffa97106007f2802589f3ed17b8c903f1c961c0fc28aa8a051eee0cbad394d8446223862efd4c1b40445a6034f626bb639cf2035b0bfc468544177593c99
+ languageName: node
+ linkType: hard
+
"css-to-react-native@npm:^3.0.0":
version: 3.2.0
resolution: "css-to-react-native@npm:3.2.0"
@@ -16060,6 +17981,13 @@ __metadata:
languageName: node
linkType: hard
+"debounce@npm:^2.0.0":
+ version: 2.2.0
+ resolution: "debounce@npm:2.2.0"
+ checksum: 10c0/684536fe9a04c9eea46cf02f119f69aa4071813ba25b9b43370e1503ea60b34fd38d694c7145fa2be504f9d40e7e39354cb9ed155c70954a0eefa391aefe271c
+ languageName: node
+ linkType: hard
+
"debug@npm:2.6.9, debug@npm:^2.6.9":
version: 2.6.9
resolution: "debug@npm:2.6.9"
@@ -16102,6 +18030,18 @@ __metadata:
languageName: node
linkType: hard
+"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4":
+ version: 4.3.7
+ resolution: "debug@npm:4.3.7"
+ dependencies:
+ ms: "npm:^2.1.3"
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ checksum: 10c0/1471db19c3b06d485a622d62f65947a19a23fbd0dd73f7fd3eafb697eec5360cde447fb075919987899b1a2096e85d35d4eb5a4de09a57600ac9cf7e6c8e768b
+ languageName: node
+ linkType: hard
+
"decamelize@npm:^1.2.0":
version: 1.2.0
resolution: "decamelize@npm:1.2.0"
@@ -16191,7 +18131,7 @@ __metadata:
languageName: node
linkType: hard
-"deepmerge@npm:^4.0.0, deepmerge@npm:^4.2.2":
+"deepmerge@npm:^4.0.0, deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1":
version: 4.3.1
resolution: "deepmerge@npm:4.3.1"
checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
@@ -16285,6 +18225,13 @@ __metadata:
languageName: node
linkType: hard
+"detect-libc@npm:^2.0.4":
+ version: 2.0.4
+ resolution: "detect-libc@npm:2.0.4"
+ checksum: 10c0/c15541f836eba4b1f521e4eecc28eefefdbc10a94d3b8cb4c507689f332cc111babb95deda66f2de050b22122113189986d5190be97d51b5a2b23b938415e67c
+ languageName: node
+ linkType: hard
+
"detect-newline@npm:^3.0.0":
version: 3.1.0
resolution: "detect-newline@npm:3.1.0"
@@ -16382,6 +18329,17 @@ __metadata:
languageName: node
linkType: hard
+"dom-serializer@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "dom-serializer@npm:2.0.0"
+ dependencies:
+ domelementtype: "npm:^2.3.0"
+ domhandler: "npm:^5.0.2"
+ entities: "npm:^4.2.0"
+ checksum: 10c0/d5ae2b7110ca3746b3643d3ef60ef823f5f078667baf530cec096433f1627ec4b6fa8c072f09d079d7cda915fd2c7bc1b7b935681e9b09e591e1e15f4040b8e2
+ languageName: node
+ linkType: hard
+
"dom-walk@npm:^0.1.0":
version: 0.1.2
resolution: "dom-walk@npm:0.1.2"
@@ -16389,6 +18347,13 @@ __metadata:
languageName: node
linkType: hard
+"domelementtype@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "domelementtype@npm:2.3.0"
+ checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9
+ languageName: node
+ linkType: hard
+
"domexception@npm:^4.0.0":
version: 4.0.0
resolution: "domexception@npm:4.0.0"
@@ -16398,6 +18363,26 @@ __metadata:
languageName: node
linkType: hard
+"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3":
+ version: 5.0.3
+ resolution: "domhandler@npm:5.0.3"
+ dependencies:
+ domelementtype: "npm:^2.3.0"
+ checksum: 10c0/bba1e5932b3e196ad6862286d76adc89a0dbf0c773e5ced1eb01f9af930c50093a084eff14b8de5ea60b895c56a04d5de8bbc4930c5543d029091916770b2d2a
+ languageName: node
+ linkType: hard
+
+"domutils@npm:^3.0.1":
+ version: 3.2.2
+ resolution: "domutils@npm:3.2.2"
+ dependencies:
+ dom-serializer: "npm:^2.0.0"
+ domelementtype: "npm:^2.3.0"
+ domhandler: "npm:^5.0.3"
+ checksum: 10c0/47938f473b987ea71cd59e59626eb8666d3aa8feba5266e45527f3b636c7883cca7e582d901531961f742c519d7514636b7973353b648762b2e3bedbf235fada
+ languageName: node
+ linkType: hard
+
"dot-case@npm:^3.0.4":
version: 3.0.4
resolution: "dot-case@npm:3.0.4"
@@ -16521,6 +18506,13 @@ __metadata:
languageName: node
linkType: hard
+"electron-to-chromium@npm:^1.5.173":
+ version: 1.5.191
+ resolution: "electron-to-chromium@npm:1.5.191"
+ checksum: 10c0/26b22ec2ae2a152da09f062d8582e54384a15ddc2a27149cdc2747a0c3f46154370a37b9e687de2d6d71ea1ebc1319f8394283ffb1581f1d4495cdefffd7a2a6
+ languageName: node
+ linkType: hard
+
"electron-to-chromium@npm:^1.5.73":
version: 1.5.102
resolution: "electron-to-chromium@npm:1.5.102"
@@ -16542,6 +18534,13 @@ __metadata:
languageName: node
linkType: hard
+"emoji-regex@npm:^10.3.0":
+ version: 10.4.0
+ resolution: "emoji-regex@npm:10.4.0"
+ checksum: 10c0/a3fcedfc58bfcce21a05a5f36a529d81e88d602100145fcca3dc6f795e3c8acc4fc18fe773fbf9b6d6e9371205edb3afa2668ec3473fa2aa7fd47d2a9d46482d
+ languageName: node
+ linkType: hard
+
"emoji-regex@npm:^8.0.0":
version: 8.0.0
resolution: "emoji-regex@npm:8.0.0"
@@ -16595,7 +18594,54 @@ __metadata:
languageName: node
linkType: hard
-"entities@npm:^4.4.0, entities@npm:^4.5.0":
+"engine.io-client@npm:~6.6.1":
+ version: 6.6.3
+ resolution: "engine.io-client@npm:6.6.3"
+ dependencies:
+ "@socket.io/component-emitter": "npm:~3.1.0"
+ debug: "npm:~4.3.1"
+ engine.io-parser: "npm:~5.2.1"
+ ws: "npm:~8.17.1"
+ xmlhttprequest-ssl: "npm:~2.1.1"
+ checksum: 10c0/ebe0b1da6831d5a68564f9ffb80efe682da4f0538488eaffadf0bcf5177a8b4472cdb01d18a9f20dece2f8de30e2df951eb4635bef2f1b492e9f08a523db91a0
+ languageName: node
+ linkType: hard
+
+"engine.io-parser@npm:~5.2.1":
+ version: 5.2.3
+ resolution: "engine.io-parser@npm:5.2.3"
+ checksum: 10c0/ed4900d8dbef470ab3839ccf3bfa79ee518ea8277c7f1f2759e8c22a48f64e687ea5e474291394d0c94f84054749fd93f3ef0acb51fa2f5f234cc9d9d8e7c536
+ languageName: node
+ linkType: hard
+
+"engine.io@npm:~6.6.0":
+ version: 6.6.4
+ resolution: "engine.io@npm:6.6.4"
+ dependencies:
+ "@types/cors": "npm:^2.8.12"
+ "@types/node": "npm:>=10.0.0"
+ accepts: "npm:~1.3.4"
+ base64id: "npm:2.0.0"
+ cookie: "npm:~0.7.2"
+ cors: "npm:~2.8.5"
+ debug: "npm:~4.3.1"
+ engine.io-parser: "npm:~5.2.1"
+ ws: "npm:~8.17.1"
+ checksum: 10c0/845761163f8ea7962c049df653b75dafb6b3693ad6f59809d4474751d7b0392cbf3dc2730b8a902ff93677a91fd28711d34ab29efd348a8a4b49c6b0724021ab
+ languageName: node
+ linkType: hard
+
+"enhanced-resolve@npm:^5.17.2":
+ version: 5.18.2
+ resolution: "enhanced-resolve@npm:5.18.2"
+ dependencies:
+ graceful-fs: "npm:^4.2.4"
+ tapable: "npm:^2.2.0"
+ checksum: 10c0/2a45105daded694304b0298d1c0351a981842249a9867513d55e41321a4ccf37dfd35b0c1e9ceae290eab73654b09aa7a910d618ea6f9441e97c52bc424a2372
+ languageName: node
+ linkType: hard
+
+"entities@npm:^4.2.0, entities@npm:^4.4.0, entities@npm:^4.5.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
@@ -16715,6 +18761,13 @@ __metadata:
languageName: node
linkType: hard
+"es-module-lexer@npm:^1.2.1, es-module-lexer@npm:^1.5.4":
+ version: 1.7.0
+ resolution: "es-module-lexer@npm:1.7.0"
+ checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
+ languageName: node
+ linkType: hard
+
"es-module-lexer@npm:^1.3.1, es-module-lexer@npm:^1.4.1":
version: 1.6.0
resolution: "es-module-lexer@npm:1.6.0"
@@ -16722,13 +18775,6 @@ __metadata:
languageName: node
linkType: hard
-"es-module-lexer@npm:^1.5.4":
- version: 1.7.0
- resolution: "es-module-lexer@npm:1.7.0"
- checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b
- languageName: node
- linkType: hard
-
"es-object-atoms@npm:^1.0.0":
version: 1.1.1
resolution: "es-object-atoms@npm:1.1.1"
@@ -16770,6 +18816,92 @@ __metadata:
languageName: node
linkType: hard
+"esbuild@npm:0.25.0":
+ version: 0.25.0
+ resolution: "esbuild@npm:0.25.0"
+ dependencies:
+ "@esbuild/aix-ppc64": "npm:0.25.0"
+ "@esbuild/android-arm": "npm:0.25.0"
+ "@esbuild/android-arm64": "npm:0.25.0"
+ "@esbuild/android-x64": "npm:0.25.0"
+ "@esbuild/darwin-arm64": "npm:0.25.0"
+ "@esbuild/darwin-x64": "npm:0.25.0"
+ "@esbuild/freebsd-arm64": "npm:0.25.0"
+ "@esbuild/freebsd-x64": "npm:0.25.0"
+ "@esbuild/linux-arm": "npm:0.25.0"
+ "@esbuild/linux-arm64": "npm:0.25.0"
+ "@esbuild/linux-ia32": "npm:0.25.0"
+ "@esbuild/linux-loong64": "npm:0.25.0"
+ "@esbuild/linux-mips64el": "npm:0.25.0"
+ "@esbuild/linux-ppc64": "npm:0.25.0"
+ "@esbuild/linux-riscv64": "npm:0.25.0"
+ "@esbuild/linux-s390x": "npm:0.25.0"
+ "@esbuild/linux-x64": "npm:0.25.0"
+ "@esbuild/netbsd-arm64": "npm:0.25.0"
+ "@esbuild/netbsd-x64": "npm:0.25.0"
+ "@esbuild/openbsd-arm64": "npm:0.25.0"
+ "@esbuild/openbsd-x64": "npm:0.25.0"
+ "@esbuild/sunos-x64": "npm:0.25.0"
+ "@esbuild/win32-arm64": "npm:0.25.0"
+ "@esbuild/win32-ia32": "npm:0.25.0"
+ "@esbuild/win32-x64": "npm:0.25.0"
+ dependenciesMeta:
+ "@esbuild/aix-ppc64":
+ optional: true
+ "@esbuild/android-arm":
+ optional: true
+ "@esbuild/android-arm64":
+ optional: true
+ "@esbuild/android-x64":
+ optional: true
+ "@esbuild/darwin-arm64":
+ optional: true
+ "@esbuild/darwin-x64":
+ optional: true
+ "@esbuild/freebsd-arm64":
+ optional: true
+ "@esbuild/freebsd-x64":
+ optional: true
+ "@esbuild/linux-arm":
+ optional: true
+ "@esbuild/linux-arm64":
+ optional: true
+ "@esbuild/linux-ia32":
+ optional: true
+ "@esbuild/linux-loong64":
+ optional: true
+ "@esbuild/linux-mips64el":
+ optional: true
+ "@esbuild/linux-ppc64":
+ optional: true
+ "@esbuild/linux-riscv64":
+ optional: true
+ "@esbuild/linux-s390x":
+ optional: true
+ "@esbuild/linux-x64":
+ optional: true
+ "@esbuild/netbsd-arm64":
+ optional: true
+ "@esbuild/netbsd-x64":
+ optional: true
+ "@esbuild/openbsd-arm64":
+ optional: true
+ "@esbuild/openbsd-x64":
+ optional: true
+ "@esbuild/sunos-x64":
+ optional: true
+ "@esbuild/win32-arm64":
+ optional: true
+ "@esbuild/win32-ia32":
+ optional: true
+ "@esbuild/win32-x64":
+ optional: true
+ bin:
+ esbuild: bin/esbuild
+ checksum: 10c0/5767b72da46da3cfec51661647ec850ddbf8a8d0662771139f10ef0692a8831396a0004b2be7966cecdb08264fb16bdc16290dcecd92396fac5f12d722fa013d
+ languageName: node
+ linkType: hard
+
"esbuild@npm:^0.21.3":
version: 0.21.5
resolution: "esbuild@npm:0.21.5"
@@ -17209,7 +19341,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint-scope@npm:^5.1.1":
+"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1":
version: 5.1.1
resolution: "eslint-scope@npm:5.1.1"
dependencies:
@@ -17397,7 +19529,7 @@ __metadata:
languageName: node
linkType: hard
-"events@npm:^3.3.0":
+"events@npm:^3.2.0, events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6
@@ -17547,6 +19679,13 @@ __metadata:
languageName: node
linkType: hard
+"exsolve@npm:^1.0.7":
+ version: 1.0.7
+ resolution: "exsolve@npm:1.0.7"
+ checksum: 10c0/4479369d0bd84bb7e0b4f5d9bc18d26a89b6dbbbccd73f9d383d14892ef78ddbe159e01781055342f83dc00ebe90044036daf17ddf55cc21e2cac6609aa15631
+ languageName: node
+ linkType: hard
+
"ext-list@npm:^2.0.0":
version: 2.2.2
resolution: "ext-list@npm:2.2.2"
@@ -17591,6 +19730,13 @@ __metadata:
languageName: node
linkType: hard
+"fast-deep-equal@npm:^2.0.1":
+ version: 2.0.1
+ resolution: "fast-deep-equal@npm:2.0.1"
+ checksum: 10c0/1602e0d6ed63493c865cc6b03f9070d6d3926e8cd086a123060b58f80a295f3f08b1ecfb479ae7c45b7fd45535202aea7cf5b49bc31bffb81c20b1502300be84
+ languageName: node
+ linkType: hard
+
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
version: 3.1.3
resolution: "fast-deep-equal@npm:3.1.3"
@@ -17898,6 +20044,16 @@ __metadata:
languageName: node
linkType: hard
+"foreground-child@npm:^3.3.1":
+ version: 3.3.1
+ resolution: "foreground-child@npm:3.3.1"
+ dependencies:
+ cross-spawn: "npm:^7.0.6"
+ signal-exit: "npm:^4.0.1"
+ checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3
+ languageName: node
+ linkType: hard
+
"form-data@npm:^4.0.0, form-data@npm:^4.0.2":
version: 4.0.2
resolution: "form-data@npm:4.0.2"
@@ -17931,6 +20087,28 @@ __metadata:
languageName: node
linkType: hard
+"framer-motion@npm:12.7.5":
+ version: 12.7.5
+ resolution: "framer-motion@npm:12.7.5"
+ dependencies:
+ motion-dom: "npm:^12.7.5"
+ motion-utils: "npm:^12.7.5"
+ tslib: "npm:^2.4.0"
+ peerDependencies:
+ "@emotion/is-prop-valid": "*"
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ "@emotion/is-prop-valid":
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ checksum: 10c0/8e3f10e9956019cb39fc5ecf8fd901efe6e06b4cd5a4cb9742ed7e738f0fbdbfc484b2966c90529d39ae18d98fd35d16878eaa112fad4cf74926838421d6cd01
+ languageName: node
+ linkType: hard
+
"framer-motion@npm:^11.18.2":
version: 11.18.2
resolution: "framer-motion@npm:11.18.2"
@@ -18117,6 +20295,13 @@ __metadata:
languageName: node
linkType: hard
+"get-east-asian-width@npm:^1.0.0":
+ version: 1.3.0
+ resolution: "get-east-asian-width@npm:1.3.0"
+ checksum: 10c0/1a049ba697e0f9a4d5514c4623781c5246982bdb61082da6b5ae6c33d838e52ce6726407df285cdbb27ec1908b333cf2820989bd3e986e37bb20979437fdf34b
+ languageName: node
+ linkType: hard
+
"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.2, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7":
version: 1.2.7
resolution: "get-intrinsic@npm:1.2.7"
@@ -18225,6 +20410,13 @@ __metadata:
languageName: node
linkType: hard
+"glob-to-regexp@npm:^0.4.1":
+ version: 0.4.1
+ resolution: "glob-to-regexp@npm:0.4.1"
+ checksum: 10c0/0486925072d7a916f052842772b61c3e86247f0a80cc0deb9b5a3e8a1a9faad5b04fb6f58986a09f34d3e96cd2a22a24b7e9882fb1cf904c31e9a310de96c429
+ languageName: node
+ linkType: hard
+
"glob@npm:7.2.3, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@@ -18255,6 +20447,22 @@ __metadata:
languageName: node
linkType: hard
+"glob@npm:^11.0.0":
+ version: 11.0.3
+ resolution: "glob@npm:11.0.3"
+ dependencies:
+ foreground-child: "npm:^3.3.1"
+ jackspeak: "npm:^4.1.1"
+ minimatch: "npm:^10.0.3"
+ minipass: "npm:^7.1.2"
+ package-json-from-dist: "npm:^1.0.0"
+ path-scurry: "npm:^2.0.0"
+ bin:
+ glob: dist/esm/bin.mjs
+ checksum: 10c0/7d24457549ec2903920dfa3d8e76850e7c02aa709122f0164b240c712f5455c0b457e6f2a1eee39344c6148e39895be8094ae8cfef7ccc3296ed30bce250c661
+ languageName: node
+ linkType: hard
+
"glob@npm:^8.0.0":
version: 8.1.0
resolution: "glob@npm:8.1.0"
@@ -18405,7 +20613,7 @@ __metadata:
languageName: node
linkType: hard
-"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
+"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -18512,6 +20720,15 @@ __metadata:
languageName: node
linkType: hard
+"he@npm:1.2.0":
+ version: 1.2.0
+ resolution: "he@npm:1.2.0"
+ bin:
+ he: bin/he
+ checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17
+ languageName: node
+ linkType: hard
+
"header-case@npm:^2.0.4":
version: 2.0.4
resolution: "header-case@npm:2.0.4"
@@ -18574,6 +20791,19 @@ __metadata:
languageName: node
linkType: hard
+"html-to-text@npm:^9.0.5":
+ version: 9.0.5
+ resolution: "html-to-text@npm:9.0.5"
+ dependencies:
+ "@selderee/plugin-htmlparser2": "npm:^0.11.0"
+ deepmerge: "npm:^4.3.1"
+ dom-serializer: "npm:^2.0.0"
+ htmlparser2: "npm:^8.0.2"
+ selderee: "npm:^0.11.0"
+ checksum: 10c0/5d2c77b798cf88a81b1da2fc1ea1a3b3e2ff49fe5a3d812392f802fff18ec315cf0969bd7846ef2eb7df8c37f463bc63e8cbdcf84e42696c6f3e15dfa61cdf4f
+ languageName: node
+ linkType: hard
+
"html@npm:^1.0.0":
version: 1.0.0
resolution: "html@npm:1.0.0"
@@ -18585,6 +20815,18 @@ __metadata:
languageName: node
linkType: hard
+"htmlparser2@npm:^8.0.2":
+ version: 8.0.2
+ resolution: "htmlparser2@npm:8.0.2"
+ dependencies:
+ domelementtype: "npm:^2.3.0"
+ domhandler: "npm:^5.0.3"
+ domutils: "npm:^3.0.1"
+ entities: "npm:^4.4.0"
+ checksum: 10c0/609cca85886d0bf2c9a5db8c6926a89f3764596877492e2caa7a25a789af4065bc6ee2cdc81807fe6b1d03a87bf8a373b5a754528a4cc05146b713c20575aab4
+ languageName: node
+ linkType: hard
+
"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
@@ -19164,6 +21406,13 @@ __metadata:
languageName: node
linkType: hard
+"is-interactive@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "is-interactive@npm:2.0.0"
+ checksum: 10c0/801c8f6064f85199dc6bf99b5dd98db3282e930c3bc197b32f2c5b89313bb578a07d1b8a01365c4348c2927229234f3681eb861b9c2c92bee72ff397390fa600
+ languageName: node
+ linkType: hard
+
"is-invalid-path@npm:^0.1.0":
version: 0.1.0
resolution: "is-invalid-path@npm:0.1.0"
@@ -19352,6 +21601,20 @@ __metadata:
languageName: node
linkType: hard
+"is-unicode-supported@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "is-unicode-supported@npm:1.3.0"
+ checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a
+ languageName: node
+ linkType: hard
+
+"is-unicode-supported@npm:^2.0.0":
+ version: 2.1.0
+ resolution: "is-unicode-supported@npm:2.1.0"
+ checksum: 10c0/a0f53e9a7c1fdbcf2d2ef6e40d4736fdffff1c9f8944c75e15425118ff3610172c87bf7bc6c34d3903b04be59790bb2212ddbe21ee65b5a97030fc50370545a5
+ languageName: node
+ linkType: hard
+
"is-upper-case@npm:^2.0.2":
version: 2.0.2
resolution: "is-upper-case@npm:2.0.2"
@@ -19525,6 +21788,15 @@ __metadata:
languageName: node
linkType: hard
+"jackspeak@npm:^4.1.1":
+ version: 4.1.1
+ resolution: "jackspeak@npm:4.1.1"
+ dependencies:
+ "@isaacs/cliui": "npm:^8.0.2"
+ checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042
+ languageName: node
+ linkType: hard
+
"javascript-stringify@npm:^2.0.1":
version: 2.1.0
resolution: "javascript-stringify@npm:2.1.0"
@@ -20358,6 +22630,17 @@ __metadata:
languageName: node
linkType: hard
+"jest-worker@npm:^27.4.5":
+ version: 27.5.1
+ resolution: "jest-worker@npm:27.5.1"
+ dependencies:
+ "@types/node": "npm:*"
+ merge-stream: "npm:^2.0.0"
+ supports-color: "npm:^8.0.0"
+ checksum: 10c0/8c4737ffd03887b3c6768e4cc3ca0269c0336c1e4b1b120943958ddb035ed2a0fc6acab6dc99631720a3720af4e708ff84fb45382ad1e83c27946adf3623969b
+ languageName: node
+ linkType: hard
+
"jest-worker@npm:^28.1.3":
version: 28.1.3
resolution: "jest-worker@npm:28.1.3"
@@ -20419,6 +22702,15 @@ __metadata:
languageName: node
linkType: hard
+"jiti@npm:2.4.2":
+ version: 2.4.2
+ resolution: "jiti@npm:2.4.2"
+ bin:
+ jiti: lib/jiti-cli.mjs
+ checksum: 10c0/4ceac133a08c8faff7eac84aabb917e85e8257f5ad659e843004ce76e981c457c390a220881748ac67ba1b940b9b729b30fb85cbaf6e7989f04b6002c94da331
+ languageName: node
+ linkType: hard
+
"jiti@npm:^1.19.1, jiti@npm:^1.21.6":
version: 1.21.7
resolution: "jiti@npm:1.21.7"
@@ -20620,7 +22912,7 @@ __metadata:
languageName: node
linkType: hard
-"json-parse-even-better-errors@npm:^2.3.0":
+"json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1":
version: 2.3.1
resolution: "json-parse-even-better-errors@npm:2.3.1"
checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3
@@ -20655,6 +22947,15 @@ __metadata:
languageName: node
linkType: hard
+"json5@npm:2.2.3, json5@npm:^2.2.1, json5@npm:^2.2.2, json5@npm:^2.2.3":
+ version: 2.2.3
+ resolution: "json5@npm:2.2.3"
+ bin:
+ json5: lib/cli.js
+ checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c
+ languageName: node
+ linkType: hard
+
"json5@npm:^1.0.2":
version: 1.0.2
resolution: "json5@npm:1.0.2"
@@ -20666,15 +22967,6 @@ __metadata:
languageName: node
linkType: hard
-"json5@npm:^2.2.1, json5@npm:^2.2.2, json5@npm:^2.2.3":
- version: 2.2.3
- resolution: "json5@npm:2.2.3"
- bin:
- json5: lib/cli.js
- checksum: 10c0/5a04eed94810fa55c5ea138b2f7a5c12b97c3750bc63d11e511dcecbfef758003861522a070c2272764ee0f4e3e323862f386945aeb5b85b87ee43f084ba586c
- languageName: node
- linkType: hard
-
"jsonc-eslint-parser@npm:^2.1.0":
version: 2.4.0
resolution: "jsonc-eslint-parser@npm:2.4.0"
@@ -20885,6 +23177,13 @@ __metadata:
languageName: node
linkType: hard
+"leac@npm:^0.6.0":
+ version: 0.6.0
+ resolution: "leac@npm:0.6.0"
+ checksum: 10c0/5257781e10791ef8462eb1cbe5e48e3cda7692486f2a775265d6f5216cc088960c62f138163b8df0dcf2119d18673bfe7b050d6b41543d92a7b7ac90e4eb1e8b
+ languageName: node
+ linkType: hard
+
"leven@npm:^3.1.0":
version: 3.1.0
resolution: "leven@npm:3.1.0"
@@ -20949,6 +23248,13 @@ __metadata:
languageName: node
linkType: hard
+"loader-runner@npm:^4.2.0":
+ version: 4.3.0
+ resolution: "loader-runner@npm:4.3.0"
+ checksum: 10c0/a44d78aae0907a72f73966fe8b82d1439c8c485238bd5a864b1b9a2a3257832effa858790241e6b37876b5446a78889adf2fcc8dd897ce54c089ecc0a0ce0bf0
+ languageName: node
+ linkType: hard
+
"locate-path@npm:^5.0.0":
version: 5.0.0
resolution: "locate-path@npm:5.0.0"
@@ -21086,13 +23392,33 @@ __metadata:
languageName: node
linkType: hard
-"log-symbols@npm:^4.1.0":
+"log-symbols@npm:4.1.0, log-symbols@npm:^4.1.0":
version: 4.1.0
resolution: "log-symbols@npm:4.1.0"
dependencies:
- chalk: "npm:^4.1.0"
- is-unicode-supported: "npm:^0.1.0"
- checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6
+ chalk: "npm:^4.1.0"
+ is-unicode-supported: "npm:^0.1.0"
+ checksum: 10c0/67f445a9ffa76db1989d0fa98586e5bc2fd5247260dafb8ad93d9f0ccd5896d53fb830b0e54dade5ad838b9de2006c826831a3c528913093af20dff8bd24aca6
+ languageName: node
+ linkType: hard
+
+"log-symbols@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "log-symbols@npm:6.0.0"
+ dependencies:
+ chalk: "npm:^5.3.0"
+ is-unicode-supported: "npm:^1.3.0"
+ checksum: 10c0/36636cacedba8f067d2deb4aad44e91a89d9efb3ead27e1846e7b82c9a10ea2e3a7bd6ce28a7ca616bebc60954ff25c67b0f92d20a6a746bb3cc52c3701891f6
+ languageName: node
+ linkType: hard
+
+"log-symbols@npm:^7.0.0":
+ version: 7.0.1
+ resolution: "log-symbols@npm:7.0.1"
+ dependencies:
+ is-unicode-supported: "npm:^2.0.0"
+ yoctocolors: "npm:^2.1.1"
+ checksum: 10c0/71d30f9a44b8604b14df5e7c9b579d739997253db7385339d493ece41ee2cc74c1f96c5b4c0b2c1e0829b05348d4f287e68faab495b7a094a80f51351c816075
languageName: node
linkType: hard
@@ -21167,6 +23493,13 @@ __metadata:
languageName: node
linkType: hard
+"lru-cache@npm:^11.0.0":
+ version: 11.1.0
+ resolution: "lru-cache@npm:11.1.0"
+ checksum: 10c0/85c312f7113f65fae6a62de7985348649937eb34fb3d212811acbf6704dc322a421788aca253b62838f1f07049a84cc513d88f494e373d3756514ad263670a64
+ languageName: node
+ linkType: hard
+
"lru-cache@npm:^4.0.1":
version: 4.1.5
resolution: "lru-cache@npm:4.1.5"
@@ -21322,6 +23655,15 @@ __metadata:
languageName: node
linkType: hard
+"marked@npm:7.0.4":
+ version: 7.0.4
+ resolution: "marked@npm:7.0.4"
+ bin:
+ marked: bin/marked.js
+ checksum: 10c0/7f5993bfb2d260806ffada81051c45952857117cba0fd82790779dc696c7ebd35a96be47409b4bdabd75e7ede286f5f5142a75a47200e3fa54eaf8b0cd6f74f6
+ languageName: node
+ linkType: hard
+
"marked@npm:^4.0.10":
version: 4.3.0
resolution: "marked@npm:4.3.0"
@@ -21355,6 +23697,17 @@ __metadata:
languageName: node
linkType: hard
+"md-to-react-email@npm:^5.0.5":
+ version: 5.0.5
+ resolution: "md-to-react-email@npm:5.0.5"
+ dependencies:
+ marked: "npm:7.0.4"
+ peerDependencies:
+ react: ^18.0 || ^19.0
+ checksum: 10c0/0d6eedb905562d88025fbf45c79117d9d668c86427eac9a3ecc2f5082a7fac9cf55c271f8892083e994d794322e8f0ede928ee2380465c9f4f878cbd2a392999
+ languageName: node
+ linkType: hard
+
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
@@ -21402,18 +23755,24 @@ __metadata:
"@mikro-orm/knex": "npm:6.4.3"
"@mikro-orm/migrations": "npm:6.4.3"
"@mikro-orm/postgresql": "npm:6.4.3"
+ "@react-email/components": "npm:^0.3.2"
+ "@react-email/preview-server": "npm:4.2.4"
"@stdlib/number-float64-base-normalize": "npm:0.0.8"
"@swc/core": "npm:1.5.7"
"@swc/jest": "npm:^0.2.36"
"@types/express": "npm:^4.17.13"
"@types/jest": "npm:^29.5.12"
+ "@types/luxon": "npm:^3.6.2"
"@types/mime": "npm:1.3.5"
"@types/node": "npm:^17.0.8"
"@types/react": "npm:^18.3.2"
awilix: "npm:^8.0.1"
+ date-fns: "npm:^4.1.0"
jest: "npm:^29.7.0"
pg: "npm:^8.13.0"
prop-types: "npm:^15.8.1"
+ react-email: "npm:^4.2.4"
+ resend: "npm:^4.7.0"
ts-node: "npm:^10.9.2"
typescript: "npm:^5.7.3"
yalc: "npm:^1.0.0-pre.53"
@@ -21493,7 +23852,14 @@ __metadata:
languageName: node
linkType: hard
-"mime-types@npm:^2.1.12, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
+"mime-db@npm:^1.54.0":
+ version: 1.54.0
+ resolution: "mime-db@npm:1.54.0"
+ checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284
+ languageName: node
+ linkType: hard
+
+"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
@@ -21502,6 +23868,15 @@ __metadata:
languageName: node
linkType: hard
+"mime-types@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "mime-types@npm:3.0.1"
+ dependencies:
+ mime-db: "npm:^1.54.0"
+ checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5
+ languageName: node
+ linkType: hard
+
"mime@npm:1.6.0":
version: 1.6.0
resolution: "mime@npm:1.6.0"
@@ -21518,6 +23893,13 @@ __metadata:
languageName: node
linkType: hard
+"mimic-function@npm:^5.0.0":
+ version: 5.0.1
+ resolution: "mimic-function@npm:5.0.1"
+ checksum: 10c0/f3d9464dd1816ecf6bdf2aec6ba32c0728022039d992f178237d8e289b48764fee4131319e72eedd4f7f094e22ded0af836c3187a7edc4595d28dd74368fd81d
+ languageName: node
+ linkType: hard
+
"mimic-response@npm:^1.0.0":
version: 1.0.1
resolution: "mimic-response@npm:1.0.1"
@@ -21559,6 +23941,15 @@ __metadata:
languageName: node
linkType: hard
+"minimatch@npm:^10.0.3":
+ version: 10.0.3
+ resolution: "minimatch@npm:10.0.3"
+ dependencies:
+ "@isaacs/brace-expansion": "npm:^5.0.0"
+ checksum: 10c0/e43e4a905c5d70ac4cec8530ceaeccb9c544b1ba8ac45238e2a78121a01c17ff0c373346472d221872563204eabe929ad02669bb575cb1f0cc30facab369f70f
+ languageName: node
+ linkType: hard
+
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
@@ -21725,6 +24116,13 @@ __metadata:
languageName: node
linkType: hard
+"module-punycode@npm:punycode@2.3.1, punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1":
+ version: 2.3.1
+ resolution: "punycode@npm:2.3.1"
+ checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
+ languageName: node
+ linkType: hard
+
"morgan@npm:^1.10.0, morgan@npm:^1.9.1":
version: 1.10.0
resolution: "morgan@npm:1.10.0"
@@ -21747,6 +24145,15 @@ __metadata:
languageName: node
linkType: hard
+"motion-dom@npm:^12.7.5":
+ version: 12.23.9
+ resolution: "motion-dom@npm:12.23.9"
+ dependencies:
+ motion-utils: "npm:^12.23.6"
+ checksum: 10c0/a1d50490622eed75ef32d4fab1b97cbf45537fdfe37b6dcae8b5ca423de63a377a261552cb7527f5b70397e8b672df09342a4cae032e7fe6c4516c6c1d982043
+ languageName: node
+ linkType: hard
+
"motion-utils@npm:^11.18.1":
version: 11.18.1
resolution: "motion-utils@npm:11.18.1"
@@ -21754,6 +24161,13 @@ __metadata:
languageName: node
linkType: hard
+"motion-utils@npm:^12.23.6, motion-utils@npm:^12.7.5":
+ version: 12.23.6
+ resolution: "motion-utils@npm:12.23.6"
+ checksum: 10c0/c058e8ba6423b3baa63e985bcc669877ee7d9579d938f5348b4e60c5ea1b4b33dd7f4877434436a4a5807f3cf00370d3fd4079a6fdd6309c5c87aa62b311a897
+ languageName: node
+ linkType: hard
+
"motion@npm:^11.15.0":
version: 11.18.2
resolution: "motion@npm:11.18.2"
@@ -21895,6 +24309,15 @@ __metadata:
languageName: node
linkType: hard
+"nanoid@npm:^3.3.6":
+ version: 3.3.11
+ resolution: "nanoid@npm:3.3.11"
+ bin:
+ nanoid: bin/nanoid.cjs
+ checksum: 10c0/40e7f70b3d15f725ca072dfc4f74e81fcf1fbb02e491cf58ac0c79093adc9b0a73b152bcde57df4b79cd097e13023d7504acb38404a4da7bc1cd8e887b82fe0b
+ languageName: node
+ linkType: hard
+
"natural-compare-lite@npm:^1.4.0":
version: 1.4.0
resolution: "natural-compare-lite@npm:1.4.0"
@@ -21937,6 +24360,13 @@ __metadata:
languageName: node
linkType: hard
+"neo-async@npm:^2.6.2":
+ version: 2.6.2
+ resolution: "neo-async@npm:2.6.2"
+ checksum: 10c0/c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d
+ languageName: node
+ linkType: hard
+
"next-themes@npm:^0.4.4":
version: 0.4.6
resolution: "next-themes@npm:0.4.6"
@@ -21947,6 +24377,65 @@ __metadata:
languageName: node
linkType: hard
+"next@npm:15.4.1":
+ version: 15.4.1
+ resolution: "next@npm:15.4.1"
+ dependencies:
+ "@next/env": "npm:15.4.1"
+ "@next/swc-darwin-arm64": "npm:15.4.1"
+ "@next/swc-darwin-x64": "npm:15.4.1"
+ "@next/swc-linux-arm64-gnu": "npm:15.4.1"
+ "@next/swc-linux-arm64-musl": "npm:15.4.1"
+ "@next/swc-linux-x64-gnu": "npm:15.4.1"
+ "@next/swc-linux-x64-musl": "npm:15.4.1"
+ "@next/swc-win32-arm64-msvc": "npm:15.4.1"
+ "@next/swc-win32-x64-msvc": "npm:15.4.1"
+ "@swc/helpers": "npm:0.5.15"
+ caniuse-lite: "npm:^1.0.30001579"
+ postcss: "npm:8.4.31"
+ sharp: "npm:^0.34.3"
+ styled-jsx: "npm:5.1.6"
+ peerDependencies:
+ "@opentelemetry/api": ^1.1.0
+ "@playwright/test": ^1.51.1
+ babel-plugin-react-compiler: "*"
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ dependenciesMeta:
+ "@next/swc-darwin-arm64":
+ optional: true
+ "@next/swc-darwin-x64":
+ optional: true
+ "@next/swc-linux-arm64-gnu":
+ optional: true
+ "@next/swc-linux-arm64-musl":
+ optional: true
+ "@next/swc-linux-x64-gnu":
+ optional: true
+ "@next/swc-linux-x64-musl":
+ optional: true
+ "@next/swc-win32-arm64-msvc":
+ optional: true
+ "@next/swc-win32-x64-msvc":
+ optional: true
+ sharp:
+ optional: true
+ peerDependenciesMeta:
+ "@opentelemetry/api":
+ optional: true
+ "@playwright/test":
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+ bin:
+ next: dist/bin/next
+ checksum: 10c0/5a3c6a5d411c21917457eb66be577def739d3237f95663eed94dbe8274b7cdee64a76c2b05ec792930c6488587b613d125413927d1d5202414b8d576e527f8e0
+ languageName: node
+ linkType: hard
+
"no-case@npm:^3.0.4":
version: 3.0.4
resolution: "no-case@npm:3.0.4"
@@ -22018,6 +24507,16 @@ __metadata:
languageName: node
linkType: hard
+"node-html-parser@npm:7.0.1":
+ version: 7.0.1
+ resolution: "node-html-parser@npm:7.0.1"
+ dependencies:
+ css-select: "npm:^5.1.0"
+ he: "npm:1.2.0"
+ checksum: 10c0/70a4d6db83340e1c93be3c9d28b924e8a2d8c53e085805d9a4916132c56e76e105fedc2201682ff20e0af0468893d0f77ecaa69865b78206b02982d0b4a649a4
+ languageName: node
+ linkType: hard
+
"node-int64@npm:^0.4.0":
version: 0.4.0
resolution: "node-int64@npm:0.4.0"
@@ -22175,6 +24674,15 @@ __metadata:
languageName: node
linkType: hard
+"nth-check@npm:^2.0.1":
+ version: 2.1.1
+ resolution: "nth-check@npm:2.1.1"
+ dependencies:
+ boolbase: "npm:^1.0.0"
+ checksum: 10c0/5fee7ff309727763689cfad844d979aedd2204a817fbaaf0e1603794a7c20db28548d7b024692f953557df6ce4a0ee4ae46cd8ebd9b36cfb300b9226b567c479
+ languageName: node
+ linkType: hard
+
"nullthrows@npm:^1.1.1":
version: 1.1.1
resolution: "nullthrows@npm:1.1.1"
@@ -22189,6 +24697,21 @@ __metadata:
languageName: node
linkType: hard
+"nypm@npm:0.6.0":
+ version: 0.6.0
+ resolution: "nypm@npm:0.6.0"
+ dependencies:
+ citty: "npm:^0.1.6"
+ consola: "npm:^3.4.0"
+ pathe: "npm:^2.0.3"
+ pkg-types: "npm:^2.0.0"
+ tinyexec: "npm:^0.3.2"
+ bin:
+ nypm: dist/cli.mjs
+ checksum: 10c0/899f16c2df1bdf3ef4de5f7d4ed5530e2e1ca097cc7dedbaa25abb6b8e44bb470c25cd26639f6e3e4f5734867e61f7f77c4ed5dfbe86b2a1bdef4525a2dc0026
+ languageName: node
+ linkType: hard
+
"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
@@ -22353,6 +24876,15 @@ __metadata:
languageName: node
linkType: hard
+"onetime@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "onetime@npm:7.0.0"
+ dependencies:
+ mimic-function: "npm:^5.0.0"
+ checksum: 10c0/5cb9179d74b63f52a196a2e7037ba2b9a893245a5532d3f44360012005c9cadb60851d56716ebff18a6f47129dab7168022445df47c2aff3b276d92585ed1221
+ languageName: node
+ linkType: hard
+
"opentelemetry-instrumentation-remix@npm:0.8.0":
version: 0.8.0
resolution: "opentelemetry-instrumentation-remix@npm:0.8.0"
@@ -22393,7 +24925,7 @@ __metadata:
languageName: node
linkType: hard
-"ora@npm:^5.4.1":
+"ora@npm:5.4.1, ora@npm:^5.4.1":
version: 5.4.1
resolution: "ora@npm:5.4.1"
dependencies:
@@ -22410,6 +24942,23 @@ __metadata:
languageName: node
linkType: hard
+"ora@npm:^8.0.0":
+ version: 8.2.0
+ resolution: "ora@npm:8.2.0"
+ dependencies:
+ chalk: "npm:^5.3.0"
+ cli-cursor: "npm:^5.0.0"
+ cli-spinners: "npm:^2.9.2"
+ is-interactive: "npm:^2.0.0"
+ is-unicode-supported: "npm:^2.0.0"
+ log-symbols: "npm:^6.0.0"
+ stdin-discarder: "npm:^0.2.2"
+ string-width: "npm:^7.2.0"
+ strip-ansi: "npm:^7.1.0"
+ checksum: 10c0/7d9291255db22e293ea164f520b6042a3e906576ab06c9cf408bf9ef5664ba0a9f3bd258baa4ada058cfcc2163ef9b6696d51237a866682ce33295349ba02c3a
+ languageName: node
+ linkType: hard
+
"os-filter-obj@npm:^2.0.0":
version: 2.0.0
resolution: "os-filter-obj@npm:2.0.0"
@@ -22615,6 +25164,16 @@ __metadata:
languageName: node
linkType: hard
+"parseley@npm:^0.12.0":
+ version: 0.12.1
+ resolution: "parseley@npm:0.12.1"
+ dependencies:
+ leac: "npm:^0.6.0"
+ peberminta: "npm:^0.9.0"
+ checksum: 10c0/df3de74172b72305b867298a71e5882c413df75d30f2bafb5fb70779dfd349c5e4db03441fbf8ca83da8e4aa72bd0ef2b5c73086c4825d27d1c649d61bc0bcc0
+ languageName: node
+ linkType: hard
+
"parseurl@npm:~1.3.3":
version: 1.3.3
resolution: "parseurl@npm:1.3.3"
@@ -22713,6 +25272,16 @@ __metadata:
languageName: node
linkType: hard
+"path-scurry@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "path-scurry@npm:2.0.0"
+ dependencies:
+ lru-cache: "npm:^11.0.0"
+ minipass: "npm:^7.1.2"
+ checksum: 10c0/3da4adedaa8e7ef8d6dc4f35a0ff8f05a9b4d8365f2b28047752b62d4c1ad73eec21e37b1579ef2d075920157856a3b52ae8309c480a6f1a8bbe06ff8e52b33c
+ languageName: node
+ linkType: hard
+
"path-to-regexp@npm:0.1.12, path-to-regexp@npm:^0.1.10":
version: 0.1.12
resolution: "path-to-regexp@npm:0.1.12"
@@ -22734,13 +25303,20 @@ __metadata:
languageName: node
linkType: hard
-"pathe@npm:^2.0.1":
+"pathe@npm:^2.0.1, pathe@npm:^2.0.3":
version: 2.0.3
resolution: "pathe@npm:2.0.3"
checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1
languageName: node
linkType: hard
+"peberminta@npm:^0.9.0":
+ version: 0.9.0
+ resolution: "peberminta@npm:0.9.0"
+ checksum: 10c0/59c2c39269d9f7f559cf44582f1c0503524c6a9bc3478e0309adba2b41c71ab98745a239a4e6f98f46105291256e6d8f12ae9860d9f016b1c9a6f52c0b63bfe7
+ languageName: node
+ linkType: hard
+
"peek-readable@npm:^5.1.3":
version: 5.4.2
resolution: "peek-readable@npm:5.4.2"
@@ -22951,6 +25527,17 @@ __metadata:
languageName: node
linkType: hard
+"pkg-types@npm:^2.0.0":
+ version: 2.2.0
+ resolution: "pkg-types@npm:2.2.0"
+ dependencies:
+ confbox: "npm:^0.2.2"
+ exsolve: "npm:^1.0.7"
+ pathe: "npm:^2.0.3"
+ checksum: 10c0/df14eada1aeaaf73f72d3ec08d360bbfb44f2dfec5612358e0ce30f306a395a51fc7bfa96a2ca6ba005e9f56ddb1d2ee5b4cdd2e7b87ff075e5bf52e6fbc1cd6
+ languageName: node
+ linkType: hard
+
"pluralize@npm:^8.0.0":
version: 8.0.0
resolution: "pluralize@npm:8.0.0"
@@ -23063,6 +25650,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss@npm:8.4.31":
+ version: 8.4.31
+ resolution: "postcss@npm:8.4.31"
+ dependencies:
+ nanoid: "npm:^3.3.6"
+ picocolors: "npm:^1.0.0"
+ source-map-js: "npm:^1.0.2"
+ checksum: 10c0/748b82e6e5fc34034dcf2ae88ea3d11fd09f69b6c50ecdd3b4a875cfc7cdca435c958b211e2cb52355422ab6fccb7d8f2f2923161d7a1b281029e4a913d59acf
+ languageName: node
+ linkType: hard
+
"postcss@npm:^8.3.6, postcss@npm:^8.4.23, postcss@npm:^8.4.32, postcss@npm:^8.4.43, postcss@npm:^8.4.47":
version: 8.5.2
resolution: "postcss@npm:8.5.2"
@@ -23200,6 +25798,22 @@ __metadata:
languageName: node
linkType: hard
+"prettier@npm:^3.5.3":
+ version: 3.6.2
+ resolution: "prettier@npm:3.6.2"
+ bin:
+ prettier: bin/prettier.cjs
+ checksum: 10c0/488cb2f2b99ec13da1e50074912870217c11edaddedeadc649b1244c749d15ba94e846423d062e2c4c9ae683e2d65f754de28889ba06e697ac4f988d44f45812
+ languageName: node
+ linkType: hard
+
+"pretty-bytes@npm:6.1.1":
+ version: 6.1.1
+ resolution: "pretty-bytes@npm:6.1.1"
+ checksum: 10c0/c7a660b933355f3b4587ad3f001c266a8dd6afd17db9f89ebc50812354bb142df4b9600396ba5999bdb1f9717300387dc311df91895c5f0f2a1780e22495b5f8
+ languageName: node
+ linkType: hard
+
"pretty-format@npm:^27.0.2":
version: 27.5.1
resolution: "pretty-format@npm:27.5.1"
@@ -23234,7 +25848,7 @@ __metadata:
languageName: node
linkType: hard
-"prism-react-renderer@npm:^2.0.6":
+"prism-react-renderer@npm:2.4.1, prism-react-renderer@npm:^2.0.6":
version: 2.4.1
resolution: "prism-react-renderer@npm:2.4.1"
dependencies:
@@ -23253,6 +25867,13 @@ __metadata:
languageName: node
linkType: hard
+"prismjs@npm:^1.30.0":
+ version: 1.30.0
+ resolution: "prismjs@npm:1.30.0"
+ checksum: 10c0/f56205bfd58ef71ccfcbcb691fd0eb84adc96c6ff21b0b69fc6fdcf02be42d6ef972ba4aed60466310de3d67733f6a746f89f2fb79c00bf217406d465b3e8f23
+ languageName: node
+ linkType: hard
+
"proc-log@npm:^3.0.0":
version: 3.0.0
resolution: "proc-log@npm:3.0.0"
@@ -23314,7 +25935,7 @@ __metadata:
languageName: node
linkType: hard
-"prompts@npm:^2.0.1, prompts@npm:^2.4.2":
+"prompts@npm:2.4.2, prompts@npm:^2.0.1, prompts@npm:^2.4.2":
version: 2.4.2
resolution: "prompts@npm:2.4.2"
dependencies:
@@ -23471,13 +26092,6 @@ __metadata:
languageName: node
linkType: hard
-"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1":
- version: 2.3.1
- resolution: "punycode@npm:2.3.1"
- checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9
- languageName: node
- linkType: hard
-
"pure-rand@npm:^6.0.0":
version: 6.1.0
resolution: "pure-rand@npm:6.1.0"
@@ -23610,6 +26224,15 @@ __metadata:
languageName: node
linkType: hard
+"randombytes@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "randombytes@npm:2.1.0"
+ dependencies:
+ safe-buffer: "npm:^5.1.0"
+ checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3
+ languageName: node
+ linkType: hard
+
"range-parser@npm:~1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
@@ -23734,6 +26357,17 @@ __metadata:
languageName: node
linkType: hard
+"react-dom@npm:19.1.0, react-dom@npm:^19.1.0":
+ version: 19.1.0
+ resolution: "react-dom@npm:19.1.0"
+ dependencies:
+ scheduler: "npm:^0.26.0"
+ peerDependencies:
+ react: ^19.1.0
+ checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc
+ languageName: node
+ linkType: hard
+
"react-dom@npm:^18.2.0":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
@@ -23759,6 +26393,33 @@ __metadata:
languageName: node
linkType: hard
+"react-email@npm:^4.2.4":
+ version: 4.2.4
+ resolution: "react-email@npm:4.2.4"
+ dependencies:
+ "@babel/parser": "npm:^7.27.0"
+ "@babel/traverse": "npm:^7.27.0"
+ chalk: "npm:^5.0.0"
+ chokidar: "npm:^4.0.3"
+ commander: "npm:^13.0.0"
+ debounce: "npm:^2.0.0"
+ esbuild: "npm:^0.25.0"
+ glob: "npm:^11.0.0"
+ jiti: "npm:2.4.2"
+ log-symbols: "npm:^7.0.0"
+ mime-types: "npm:^3.0.0"
+ normalize-path: "npm:^3.0.0"
+ nypm: "npm:0.6.0"
+ ora: "npm:^8.0.0"
+ prompts: "npm:2.4.2"
+ socket.io: "npm:^4.8.1"
+ tsconfig-paths: "npm:4.2.0"
+ bin:
+ email: dist/index.js
+ checksum: 10c0/93a9f54815217eef137d93900072090175aa199e867197674aa145c659cd048fe684ca6eec4db049c7bc51a936aa20204e64ba7029eebd56db52763c6ca4deaf
+ languageName: node
+ linkType: hard
+
"react-fast-compare@npm:^3.0.1, react-fast-compare@npm:^3.2.2":
version: 3.2.2
resolution: "react-fast-compare@npm:3.2.2"
@@ -23797,6 +26458,15 @@ __metadata:
languageName: node
linkType: hard
+"react-hook-form@npm:^7.57.0":
+ version: 7.57.0
+ resolution: "react-hook-form@npm:7.57.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+ checksum: 10c0/6db0b44b2e88d4db541514e96557723e39381ce9f71b3787bf041635f829143dbd0ae46a1f6c16dee23afe3413fd25539484ba02bf2a35d90aaa1b7483193ea9
+ languageName: node
+ linkType: hard
+
"react-hotkeys-hook@npm:^4.5.0":
version: 4.6.1
resolution: "react-hotkeys-hook@npm:4.6.1"
@@ -23891,6 +26561,15 @@ __metadata:
languageName: node
linkType: hard
+"react-promise-suspense@npm:^0.3.4":
+ version: 0.3.4
+ resolution: "react-promise-suspense@npm:0.3.4"
+ dependencies:
+ fast-deep-equal: "npm:^2.0.1"
+ checksum: 10c0/ab7a22f5400f9e9933995537bf6430a4c79e33a121aedb51864968e7604e5c40421fd539ead62554f32300b7d49755c79636de06caa36fe52973b626b4ddfebf
+ languageName: node
+ linkType: hard
+
"react-refresh@npm:^0.14.0, react-refresh@npm:^0.14.2":
version: 0.14.2
resolution: "react-refresh@npm:0.14.2"
@@ -23984,6 +26663,19 @@ __metadata:
languageName: node
linkType: hard
+"react-router-dom@npm:6.30.0":
+ version: 6.30.0
+ resolution: "react-router-dom@npm:6.30.0"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ react-router: "npm:6.30.0"
+ peerDependencies:
+ react: ">=16.8"
+ react-dom: ">=16.8"
+ checksum: 10c0/262954ba894d6a241ceda5f61098f7d6a292d0018a6ebb9c9c67425b7deb6e59b6191a9233a03d38e287e60f7ac3702e9e84c8e20b39a6487698fe088b71e27a
+ languageName: node
+ linkType: hard
+
"react-router-dom@npm:^7.0.0, react-router-dom@npm:^7.5.3":
version: 7.5.3
resolution: "react-router-dom@npm:7.5.3"
@@ -24007,6 +26699,17 @@ __metadata:
languageName: node
linkType: hard
+"react-router@npm:6.30.0":
+ version: 6.30.0
+ resolution: "react-router@npm:6.30.0"
+ dependencies:
+ "@remix-run/router": "npm:1.23.0"
+ peerDependencies:
+ react: ">=16.8"
+ checksum: 10c0/e6f20cf5c47ec057a057a4cfb9a55983d0a5b4b3314d20e07f0a70e59e004f51778d4dac415aee1e4e64db69cc4cd72e5acf8fd60dcf07d909895b8863b0b023
+ languageName: node
+ linkType: hard
+
"react-router@npm:7.5.3, react-router@npm:^7.0.0, react-router@npm:^7.5.3":
version: 7.5.3
resolution: "react-router@npm:7.5.3"
@@ -24094,6 +26797,13 @@ __metadata:
languageName: node
linkType: hard
+"react@npm:19.1.0, react@npm:^19.1.0":
+ version: 19.1.0
+ resolution: "react@npm:19.1.0"
+ checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698
+ languageName: node
+ linkType: hard
+
"react@npm:^18.2.0":
version: 18.3.1
resolution: "react@npm:18.3.1"
@@ -24314,7 +27024,7 @@ __metadata:
languageName: node
linkType: hard
-"remix-hook-form@npm:7.0.1":
+"remix-hook-form@npm:7.0.1, remix-hook-form@npm:^7.0.1":
version: 7.0.1
resolution: "remix-hook-form@npm:7.0.1"
peerDependencies:
@@ -24450,6 +27160,15 @@ __metadata:
languageName: node
linkType: hard
+"resend@npm:^4.7.0":
+ version: 4.7.0
+ resolution: "resend@npm:4.7.0"
+ dependencies:
+ "@react-email/render": "npm:1.1.2"
+ checksum: 10c0/c5bdb06670097dcbf49c012f5ccfe290c15a9d5cb2ddc97de4d6fde201edaac7037f03b0f1f09b87a054e6097b7c104dafa1c85f981615962fd2b5220a2a5e96
+ languageName: node
+ linkType: hard
+
"resolve-alpn@npm:^1.0.0":
version: 1.2.1
resolution: "resolve-alpn@npm:1.2.1"
@@ -24565,6 +27284,16 @@ __metadata:
languageName: node
linkType: hard
+"restore-cursor@npm:^5.0.0":
+ version: 5.1.0
+ resolution: "restore-cursor@npm:5.1.0"
+ dependencies:
+ onetime: "npm:^7.0.0"
+ signal-exit: "npm:^4.1.0"
+ checksum: 10c0/c2ba89131eea791d1b25205bdfdc86699767e2b88dee2a590b1a6caa51737deac8bad0260a5ded2f7c074b7db2f3a626bcf1fcf3cdf35974cbeea5e2e6764f60
+ languageName: node
+ linkType: hard
+
"retry-request@npm:^5.0.0":
version: 5.0.2
resolution: "retry-request@npm:5.0.2"
@@ -24763,9 +27492,19 @@ __metadata:
resolution: "root@workspace:."
dependencies:
"@biomejs/biome": "npm:1.9.3"
+ "@hookform/resolvers": "npm:^5.1.0"
+ "@medusajs/icons": "npm:^2.8.4"
+ "@medusajs/ui": "npm:^4.0.14"
+ "@remix-run/react": "npm:^2.16.8"
+ "@tanstack/react-query": "npm:^5.80.6"
prettier: "npm:^3.2.5"
+ react: "npm:^19.1.0"
+ react-dom: "npm:^19.1.0"
+ react-hook-form: "npm:^7.57.0"
+ remix-hook-form: "npm:^7.0.1"
turbo: "npm:^2.1.2"
typescript: "npm:^5.6.2"
+ zod: "npm:^3.25.56"
languageName: unknown
linkType: soft
@@ -24814,7 +27553,7 @@ __metadata:
languageName: node
linkType: hard
-"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
+"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
@@ -24881,6 +27620,25 @@ __metadata:
languageName: node
linkType: hard
+"scheduler@npm:^0.26.0":
+ version: 0.26.0
+ resolution: "scheduler@npm:0.26.0"
+ checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356
+ languageName: node
+ linkType: hard
+
+"schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.2":
+ version: 4.3.2
+ resolution: "schema-utils@npm:4.3.2"
+ dependencies:
+ "@types/json-schema": "npm:^7.0.9"
+ ajv: "npm:^8.9.0"
+ ajv-formats: "npm:^2.1.1"
+ ajv-keywords: "npm:^5.1.0"
+ checksum: 10c0/981632f9bf59f35b15a9bcdac671dd183f4946fe4b055ae71a301e66a9797b95e5dd450de581eb6cca56fb6583ce8f24d67b2d9f8e1b2936612209697f6c277e
+ languageName: node
+ linkType: hard
+
"scrypt-kdf@npm:^2.0.1":
version: 2.0.1
resolution: "scrypt-kdf@npm:2.0.1"
@@ -24888,6 +27646,15 @@ __metadata:
languageName: node
linkType: hard
+"selderee@npm:^0.11.0":
+ version: 0.11.0
+ resolution: "selderee@npm:0.11.0"
+ dependencies:
+ parseley: "npm:^0.12.0"
+ checksum: 10c0/c2ad8313a0dbf3c0b74752a8d03cfbc0931ae77a36679cdb64733eb732c1762f95a5174249bf7e8b8103874cb0e013a030f9c8b72f5d41e62f1d847d4a845d39
+ languageName: node
+ linkType: hard
+
"semver-regex@npm:^4.0.5":
version: 4.0.5
resolution: "semver-regex@npm:4.0.5"
@@ -24922,6 +27689,15 @@ __metadata:
languageName: node
linkType: hard
+"semver@npm:^7.7.1, semver@npm:^7.7.2":
+ version: 7.7.2
+ resolution: "semver@npm:7.7.2"
+ bin:
+ semver: bin/semver.js
+ checksum: 10c0/aca305edfbf2383c22571cb7714f48cadc7ac95371b4b52362fb8eeffdfbc0de0669368b82b2b15978f8848f01d7114da65697e56cd8c37b0dab8c58e543f9ea
+ languageName: node
+ linkType: hard
+
"semver@npm:~7.5.4":
version: 7.5.4
resolution: "semver@npm:7.5.4"
@@ -24965,6 +27741,15 @@ __metadata:
languageName: node
linkType: hard
+"serialize-javascript@npm:^6.0.2":
+ version: 6.0.2
+ resolution: "serialize-javascript@npm:6.0.2"
+ dependencies:
+ randombytes: "npm:^2.1.0"
+ checksum: 10c0/2dd09ef4b65a1289ba24a788b1423a035581bef60817bea1f01eda8e3bda623f86357665fe7ac1b50f6d4f583f97db9615b3f07b2a2e8cbcb75033965f771dd2
+ languageName: node
+ linkType: hard
+
"serve-static@npm:1.16.2":
version: 1.16.2
resolution: "serve-static@npm:1.16.2"
@@ -24984,7 +27769,7 @@ __metadata:
languageName: node
linkType: hard
-"set-cookie-parser@npm:^2.6.0":
+"set-cookie-parser@npm:^2.4.8, set-cookie-parser@npm:^2.6.0":
version: 2.7.1
resolution: "set-cookie-parser@npm:2.7.1"
checksum: 10c0/060c198c4c92547ac15988256f445eae523f57f2ceefeccf52d30d75dedf6bff22b9c26f756bd44e8e560d44ff4ab2130b178bd2e52ef5571bf7be3bd7632d9a
@@ -25049,6 +27834,78 @@ __metadata:
languageName: node
linkType: hard
+"sharp@npm:0.34.1":
+ version: 0.34.1
+ resolution: "sharp@npm:0.34.1"
+ dependencies:
+ "@img/sharp-darwin-arm64": "npm:0.34.1"
+ "@img/sharp-darwin-x64": "npm:0.34.1"
+ "@img/sharp-libvips-darwin-arm64": "npm:1.1.0"
+ "@img/sharp-libvips-darwin-x64": "npm:1.1.0"
+ "@img/sharp-libvips-linux-arm": "npm:1.1.0"
+ "@img/sharp-libvips-linux-arm64": "npm:1.1.0"
+ "@img/sharp-libvips-linux-ppc64": "npm:1.1.0"
+ "@img/sharp-libvips-linux-s390x": "npm:1.1.0"
+ "@img/sharp-libvips-linux-x64": "npm:1.1.0"
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.1.0"
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.1.0"
+ "@img/sharp-linux-arm": "npm:0.34.1"
+ "@img/sharp-linux-arm64": "npm:0.34.1"
+ "@img/sharp-linux-s390x": "npm:0.34.1"
+ "@img/sharp-linux-x64": "npm:0.34.1"
+ "@img/sharp-linuxmusl-arm64": "npm:0.34.1"
+ "@img/sharp-linuxmusl-x64": "npm:0.34.1"
+ "@img/sharp-wasm32": "npm:0.34.1"
+ "@img/sharp-win32-ia32": "npm:0.34.1"
+ "@img/sharp-win32-x64": "npm:0.34.1"
+ color: "npm:^4.2.3"
+ detect-libc: "npm:^2.0.3"
+ semver: "npm:^7.7.1"
+ dependenciesMeta:
+ "@img/sharp-darwin-arm64":
+ optional: true
+ "@img/sharp-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ "@img/sharp-libvips-linux-ppc64":
+ optional: true
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ "@img/sharp-linux-arm":
+ optional: true
+ "@img/sharp-linux-arm64":
+ optional: true
+ "@img/sharp-linux-s390x":
+ optional: true
+ "@img/sharp-linux-x64":
+ optional: true
+ "@img/sharp-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-linuxmusl-x64":
+ optional: true
+ "@img/sharp-wasm32":
+ optional: true
+ "@img/sharp-win32-ia32":
+ optional: true
+ "@img/sharp-win32-x64":
+ optional: true
+ checksum: 10c0/50f5ffb18a775ec9f0d4d39bdc4356fdfa1fc97e69d8800d68e960b93b1c0cce7ee5242225d3b86ffae5801890fd7f93acfee00018f247e7df70fee2b4de7945
+ languageName: node
+ linkType: hard
+
"sharp@npm:^0.33.4":
version: 0.33.5
resolution: "sharp@npm:0.33.5"
@@ -25118,6 +27975,84 @@ __metadata:
languageName: node
linkType: hard
+"sharp@npm:^0.34.3":
+ version: 0.34.3
+ resolution: "sharp@npm:0.34.3"
+ dependencies:
+ "@img/sharp-darwin-arm64": "npm:0.34.3"
+ "@img/sharp-darwin-x64": "npm:0.34.3"
+ "@img/sharp-libvips-darwin-arm64": "npm:1.2.0"
+ "@img/sharp-libvips-darwin-x64": "npm:1.2.0"
+ "@img/sharp-libvips-linux-arm": "npm:1.2.0"
+ "@img/sharp-libvips-linux-arm64": "npm:1.2.0"
+ "@img/sharp-libvips-linux-ppc64": "npm:1.2.0"
+ "@img/sharp-libvips-linux-s390x": "npm:1.2.0"
+ "@img/sharp-libvips-linux-x64": "npm:1.2.0"
+ "@img/sharp-libvips-linuxmusl-arm64": "npm:1.2.0"
+ "@img/sharp-libvips-linuxmusl-x64": "npm:1.2.0"
+ "@img/sharp-linux-arm": "npm:0.34.3"
+ "@img/sharp-linux-arm64": "npm:0.34.3"
+ "@img/sharp-linux-ppc64": "npm:0.34.3"
+ "@img/sharp-linux-s390x": "npm:0.34.3"
+ "@img/sharp-linux-x64": "npm:0.34.3"
+ "@img/sharp-linuxmusl-arm64": "npm:0.34.3"
+ "@img/sharp-linuxmusl-x64": "npm:0.34.3"
+ "@img/sharp-wasm32": "npm:0.34.3"
+ "@img/sharp-win32-arm64": "npm:0.34.3"
+ "@img/sharp-win32-ia32": "npm:0.34.3"
+ "@img/sharp-win32-x64": "npm:0.34.3"
+ color: "npm:^4.2.3"
+ detect-libc: "npm:^2.0.4"
+ semver: "npm:^7.7.2"
+ dependenciesMeta:
+ "@img/sharp-darwin-arm64":
+ optional: true
+ "@img/sharp-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-darwin-arm64":
+ optional: true
+ "@img/sharp-libvips-darwin-x64":
+ optional: true
+ "@img/sharp-libvips-linux-arm":
+ optional: true
+ "@img/sharp-libvips-linux-arm64":
+ optional: true
+ "@img/sharp-libvips-linux-ppc64":
+ optional: true
+ "@img/sharp-libvips-linux-s390x":
+ optional: true
+ "@img/sharp-libvips-linux-x64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-libvips-linuxmusl-x64":
+ optional: true
+ "@img/sharp-linux-arm":
+ optional: true
+ "@img/sharp-linux-arm64":
+ optional: true
+ "@img/sharp-linux-ppc64":
+ optional: true
+ "@img/sharp-linux-s390x":
+ optional: true
+ "@img/sharp-linux-x64":
+ optional: true
+ "@img/sharp-linuxmusl-arm64":
+ optional: true
+ "@img/sharp-linuxmusl-x64":
+ optional: true
+ "@img/sharp-wasm32":
+ optional: true
+ "@img/sharp-win32-arm64":
+ optional: true
+ "@img/sharp-win32-ia32":
+ optional: true
+ "@img/sharp-win32-x64":
+ optional: true
+ checksum: 10c0/df9e6645e3db6ed298a0ac956ba74e468c367fc038b547936fbdddc6a29fce9af40413acbef73b3716291530760f311a20e45c8983f20ee5ea69dd2f21464a2b
+ languageName: node
+ linkType: hard
+
"shebang-command@npm:^1.2.0":
version: 1.2.0
resolution: "shebang-command@npm:1.2.0"
@@ -25280,6 +28215,53 @@ __metadata:
languageName: node
linkType: hard
+"socket.io-adapter@npm:~2.5.2":
+ version: 2.5.5
+ resolution: "socket.io-adapter@npm:2.5.5"
+ dependencies:
+ debug: "npm:~4.3.4"
+ ws: "npm:~8.17.1"
+ checksum: 10c0/04a5a2a9c4399d1b6597c2afc4492ab1e73430cc124ab02b09e948eabf341180b3866e2b61b5084cb899beb68a4db7c328c29bda5efb9207671b5cb0bc6de44e
+ languageName: node
+ linkType: hard
+
+"socket.io-client@npm:4.8.1":
+ version: 4.8.1
+ resolution: "socket.io-client@npm:4.8.1"
+ dependencies:
+ "@socket.io/component-emitter": "npm:~3.1.0"
+ debug: "npm:~4.3.2"
+ engine.io-client: "npm:~6.6.1"
+ socket.io-parser: "npm:~4.2.4"
+ checksum: 10c0/544c49cc8cc77118ef68b758a8a580c8e680a5909cae05c566d2cc07ec6cd6720a4f5b7e985489bf2a8391749177a5437ac30b8afbdf30b9da6402687ad51c86
+ languageName: node
+ linkType: hard
+
+"socket.io-parser@npm:~4.2.4":
+ version: 4.2.4
+ resolution: "socket.io-parser@npm:4.2.4"
+ dependencies:
+ "@socket.io/component-emitter": "npm:~3.1.0"
+ debug: "npm:~4.3.1"
+ checksum: 10c0/9383b30358fde4a801ea4ec5e6860915c0389a091321f1c1f41506618b5cf7cd685d0a31c587467a0c4ee99ef98c2b99fb87911f9dfb329716c43b587f29ca48
+ languageName: node
+ linkType: hard
+
+"socket.io@npm:^4.8.1":
+ version: 4.8.1
+ resolution: "socket.io@npm:4.8.1"
+ dependencies:
+ accepts: "npm:~1.3.4"
+ base64id: "npm:~2.0.0"
+ cors: "npm:~2.8.5"
+ debug: "npm:~4.3.2"
+ engine.io: "npm:~6.6.0"
+ socket.io-adapter: "npm:~2.5.2"
+ socket.io-parser: "npm:~4.2.4"
+ checksum: 10c0/acf931a2bb235be96433b71da3d8addc63eeeaa8acabd33dc8d64e12287390a45f1e9f389a73cf7dc336961cd491679741b7a016048325c596835abbcc017ca9
+ languageName: node
+ linkType: hard
+
"socks-proxy-agent@npm:^8.0.3":
version: 8.0.5
resolution: "socks-proxy-agent@npm:8.0.5"
@@ -25301,6 +28283,16 @@ __metadata:
languageName: node
linkType: hard
+"sonner@npm:2.0.3":
+ version: 2.0.3
+ resolution: "sonner@npm:2.0.3"
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ checksum: 10c0/59f84142f7a692dd1ec90e6df2003a70ff0b325eaef1d5dd17ad250e7f992b71053824f1a7ab3912f8c4caa5ce30523a096b5f5108b3e8ae13f906048691aca1
+ languageName: node
+ linkType: hard
+
"sonner@npm:^1.5.0, sonner@npm:^1.7.1":
version: 1.7.4
resolution: "sonner@npm:1.7.4"
@@ -25336,7 +28328,7 @@ __metadata:
languageName: node
linkType: hard
-"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1":
+"source-map-js@npm:1.2.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@@ -25353,7 +28345,7 @@ __metadata:
languageName: node
linkType: hard
-"source-map-support@npm:^0.5.21":
+"source-map-support@npm:^0.5.21, source-map-support@npm:~0.5.20":
version: 0.5.21
resolution: "source-map-support@npm:0.5.21"
dependencies:
@@ -25384,6 +28376,13 @@ __metadata:
languageName: node
linkType: hard
+"spamc@npm:0.0.5":
+ version: 0.0.5
+ resolution: "spamc@npm:0.0.5"
+ checksum: 10c0/ccaf6ecc05198380e1ca3e8108b96ee4205529d478c6e9a205c43e8af2bde62c1d1f5a6be4665a832e9540ffecdf03de500e9d1484c9653b344d04247f22c6e1
+ languageName: node
+ linkType: hard
+
"spawn-command@npm:^0.0.2-1":
version: 0.0.2
resolution: "spawn-command@npm:0.0.2"
@@ -25487,6 +28486,15 @@ __metadata:
languageName: node
linkType: hard
+"stacktrace-parser@npm:0.1.11":
+ version: 0.1.11
+ resolution: "stacktrace-parser@npm:0.1.11"
+ dependencies:
+ type-fest: "npm:^0.7.1"
+ checksum: 10c0/4633d9afe8cd2f6c7fb2cebdee3cc8de7fd5f6f9736645fd08c0f66872a303061ce9cc0ccf46f4216dc94a7941b56e331012398dc0024dc25e46b5eb5d4ff018
+ languageName: node
+ linkType: hard
+
"standard-as-callback@npm:^2.1.0":
version: 2.1.0
resolution: "standard-as-callback@npm:2.1.0"
@@ -25501,6 +28509,13 @@ __metadata:
languageName: node
linkType: hard
+"stdin-discarder@npm:^0.2.2":
+ version: 0.2.2
+ resolution: "stdin-discarder@npm:0.2.2"
+ checksum: 10c0/c78375e82e956d7a64be6e63c809c7f058f5303efcaf62ea48350af072bacdb99c06cba39209b45a071c1acbd49116af30df1df9abb448df78a6005b72f10537
+ languageName: node
+ linkType: hard
+
"stop-iteration-iterator@npm:^1.0.0":
version: 1.1.0
resolution: "stop-iteration-iterator@npm:1.1.0"
@@ -25590,8 +28605,8 @@ __metadata:
postcss: "npm:8.4.20"
prettier: "npm:2.7.1"
qs: "npm:^6.11.2"
- react: "npm:19.0.0"
- react-dom: "npm:19.0.0"
+ react: "npm:19.1.0"
+ react-dom: "npm:19.1.0"
react-drag-drop-files: "npm:2.3.10"
react-photo-album: "npm:^2.3.0"
react-player: "npm:^2.12.0"
@@ -25679,6 +28694,17 @@ __metadata:
languageName: node
linkType: hard
+"string-width@npm:^7.2.0":
+ version: 7.2.0
+ resolution: "string-width@npm:7.2.0"
+ dependencies:
+ emoji-regex: "npm:^10.3.0"
+ get-east-asian-width: "npm:^1.0.0"
+ strip-ansi: "npm:^7.1.0"
+ checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9
+ languageName: node
+ linkType: hard
+
"string.prototype.matchall@npm:^4.0.8":
version: 4.0.12
resolution: "string.prototype.matchall@npm:4.0.12"
@@ -25765,7 +28791,7 @@ __metadata:
languageName: node
linkType: hard
-"strip-ansi@npm:^7.0.1":
+"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0":
version: 7.1.0
resolution: "strip-ansi@npm:7.1.0"
dependencies:
@@ -25875,6 +28901,22 @@ __metadata:
languageName: node
linkType: hard
+"styled-jsx@npm:5.1.6":
+ version: 5.1.6
+ resolution: "styled-jsx@npm:5.1.6"
+ dependencies:
+ client-only: "npm:0.0.1"
+ peerDependencies:
+ react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ peerDependenciesMeta:
+ "@babel/core":
+ optional: true
+ babel-plugin-macros:
+ optional: true
+ checksum: 10c0/ace50e7ea5ae5ae6a3b65a50994c51fca6ae7df9c7ecfd0104c36be0b4b3a9c5c1a2374d16e2a11e256d0b20be6d47256d768ecb4f91ab390f60752a075780f5
+ languageName: node
+ linkType: hard
+
"stylis@npm:4.2.0":
version: 4.2.0
resolution: "stylis@npm:4.2.0"
@@ -25967,6 +29009,13 @@ __metadata:
languageName: node
linkType: hard
+"tailwind-merge@npm:3.2.0":
+ version: 3.2.0
+ resolution: "tailwind-merge@npm:3.2.0"
+ checksum: 10c0/294f6c2db0df74405bff126107107426c3126a70a1717d78e8d6811db65546c9bb3d61282bdb8d9fbded23f6bc8ec3e8e61031a4f53265f90b7f3dba558f88f4
+ languageName: node
+ linkType: hard
+
"tailwind-merge@npm:^1.14.0":
version: 1.14.0
resolution: "tailwind-merge@npm:1.14.0"
@@ -26023,6 +29072,39 @@ __metadata:
languageName: node
linkType: hard
+"tailwindcss@npm:3.4.0":
+ version: 3.4.0
+ resolution: "tailwindcss@npm:3.4.0"
+ dependencies:
+ "@alloc/quick-lru": "npm:^5.2.0"
+ arg: "npm:^5.0.2"
+ chokidar: "npm:^3.5.3"
+ didyoumean: "npm:^1.2.2"
+ dlv: "npm:^1.1.3"
+ fast-glob: "npm:^3.3.0"
+ glob-parent: "npm:^6.0.2"
+ is-glob: "npm:^4.0.3"
+ jiti: "npm:^1.19.1"
+ lilconfig: "npm:^2.1.0"
+ micromatch: "npm:^4.0.5"
+ normalize-path: "npm:^3.0.0"
+ object-hash: "npm:^3.0.0"
+ picocolors: "npm:^1.0.0"
+ postcss: "npm:^8.4.23"
+ postcss-import: "npm:^15.1.0"
+ postcss-js: "npm:^4.0.1"
+ postcss-load-config: "npm:^4.0.1"
+ postcss-nested: "npm:^6.0.1"
+ postcss-selector-parser: "npm:^6.0.11"
+ resolve: "npm:^1.22.2"
+ sucrase: "npm:^3.32.0"
+ bin:
+ tailwind: lib/cli.js
+ tailwindcss: lib/cli.js
+ checksum: 10c0/0a1cef7468e6d17c2857d0b3c4017af2cb37ed8ba27dfb14780c517b8a74f6786970227c400ac1325fc8bcfc09099d8e990fa7c60924bf945f3d0a912d63f546
+ languageName: node
+ linkType: hard
+
"tailwindcss@npm:^3.3.6":
version: 3.4.17
resolution: "tailwindcss@npm:3.4.17"
@@ -26056,6 +29138,13 @@ __metadata:
languageName: node
linkType: hard
+"tapable@npm:^2.1.1, tapable@npm:^2.2.0":
+ version: 2.2.2
+ resolution: "tapable@npm:2.2.2"
+ checksum: 10c0/8ad130aa705cab6486ad89e42233569a1fb1ff21af115f59cebe9f2b45e9e7995efceaa9cc5062510cdb4ec673b527924b2ab812e3579c55ad659ae92117011e
+ languageName: node
+ linkType: hard
+
"tar@npm:^7.4.3":
version: 7.4.3
resolution: "tar@npm:7.4.3"
@@ -26087,6 +29176,42 @@ __metadata:
languageName: node
linkType: hard
+"terser-webpack-plugin@npm:^5.3.11":
+ version: 5.3.14
+ resolution: "terser-webpack-plugin@npm:5.3.14"
+ dependencies:
+ "@jridgewell/trace-mapping": "npm:^0.3.25"
+ jest-worker: "npm:^27.4.5"
+ schema-utils: "npm:^4.3.0"
+ serialize-javascript: "npm:^6.0.2"
+ terser: "npm:^5.31.1"
+ peerDependencies:
+ webpack: ^5.1.0
+ peerDependenciesMeta:
+ "@swc/core":
+ optional: true
+ esbuild:
+ optional: true
+ uglify-js:
+ optional: true
+ checksum: 10c0/9b060947241af43bd6fd728456f60e646186aef492163672a35ad49be6fbc7f63b54a7356c3f6ff40a8f83f00a977edc26f044b8e106cc611c053c8c0eaf8569
+ languageName: node
+ linkType: hard
+
+"terser@npm:^5.31.1":
+ version: 5.43.1
+ resolution: "terser@npm:5.43.1"
+ dependencies:
+ "@jridgewell/source-map": "npm:^0.3.3"
+ acorn: "npm:^8.14.0"
+ commander: "npm:^2.20.0"
+ source-map-support: "npm:~0.5.20"
+ bin:
+ terser: bin/terser
+ checksum: 10c0/9cd3fa09ea6bcb79eb71995216b8bef0651b18c5c3877535fc699a77e1ab43b140a4da5811ac9baeb654fa9ec939b17324092f0f0bdb19c402e101e3db946986
+ languageName: node
+ linkType: hard
+
"test-exclude@npm:^6.0.0":
version: 6.0.0
resolution: "test-exclude@npm:6.0.0"
@@ -26151,6 +29276,13 @@ __metadata:
languageName: node
linkType: hard
+"tinyexec@npm:^0.3.2":
+ version: 0.3.2
+ resolution: "tinyexec@npm:0.3.2"
+ checksum: 10c0/3efbf791a911be0bf0821eab37a3445c2ba07acc1522b1fa84ae1e55f10425076f1290f680286345ed919549ad67527d07281f1c19d584df3b74326909eb1f90
+ languageName: node
+ linkType: hard
+
"tinyglobby@npm:^0.2.13":
version: 0.2.13
resolution: "tinyglobby@npm:0.2.13"
@@ -26513,6 +29645,13 @@ __metadata:
languageName: node
linkType: hard
+"turbo-stream@npm:2.4.1":
+ version: 2.4.1
+ resolution: "turbo-stream@npm:2.4.1"
+ checksum: 10c0/c93470c732787882b0085f23db1802a28c99182e4f3a39906409ab0221405b4cde3fc6e40b1f4f30a9e804e6843b81af58637e7cab3ae575781fb96875eb041f
+ languageName: node
+ linkType: hard
+
"turbo-windows-64@npm:2.4.2":
version: 2.4.2
resolution: "turbo-windows-64@npm:2.4.2"
@@ -26595,6 +29734,13 @@ __metadata:
languageName: node
linkType: hard
+"type-fest@npm:^0.7.1":
+ version: 0.7.1
+ resolution: "type-fest@npm:0.7.1"
+ checksum: 10c0/ce6b5ef806a76bf08d0daa78d65e61f24d9a0380bd1f1df36ffb61f84d14a0985c3a921923cf4b97831278cb6fa9bf1b89c751df09407e0510b14e8c081e4e0f
+ languageName: node
+ linkType: hard
+
"type-fest@npm:^2.19.0":
version: 2.19.0
resolution: "type-fest@npm:2.19.0"
@@ -26804,6 +29950,20 @@ __metadata:
languageName: node
linkType: hard
+"undici-types@npm:~6.21.0":
+ version: 6.21.0
+ resolution: "undici-types@npm:6.21.0"
+ checksum: 10c0/c01ed51829b10aa72fc3ce64b747f8e74ae9b60eafa19a7b46ef624403508a54c526ffab06a14a26b3120d055e1104d7abe7c9017e83ced038ea5cf52f8d5e04
+ languageName: node
+ linkType: hard
+
+"undici-types@npm:~7.8.0":
+ version: 7.8.0
+ resolution: "undici-types@npm:7.8.0"
+ checksum: 10c0/9d9d246d1dc32f318d46116efe3cfca5a72d4f16828febc1918d94e58f6ffcf39c158aa28bf5b4fc52f410446bc7858f35151367bd7a49f21746cab6497b709b
+ languageName: node
+ linkType: hard
+
"undici@npm:^6.19.2":
version: 6.21.2
resolution: "undici@npm:6.21.2"
@@ -26880,6 +30040,20 @@ __metadata:
languageName: node
linkType: hard
+"update-browserslist-db@npm:^1.1.3":
+ version: 1.1.3
+ resolution: "update-browserslist-db@npm:1.1.3"
+ dependencies:
+ escalade: "npm:^3.2.0"
+ picocolors: "npm:^1.1.1"
+ peerDependencies:
+ browserslist: ">= 4.21.0"
+ bin:
+ update-browserslist-db: cli.js
+ checksum: 10c0/682e8ecbf9de474a626f6462aa85927936cdd256fe584c6df2508b0df9f7362c44c957e9970df55dfe44d3623807d26316ea2c7d26b80bb76a16c56c37233c32
+ languageName: node
+ linkType: hard
+
"upper-case-first@npm:^2.0.2":
version: 2.0.2
resolution: "upper-case-first@npm:2.0.2"
@@ -26932,6 +30106,15 @@ __metadata:
languageName: node
linkType: hard
+"use-debounce@npm:10.0.4":
+ version: 10.0.4
+ resolution: "use-debounce@npm:10.0.4"
+ peerDependencies:
+ react: "*"
+ checksum: 10c0/73494fc44b2bd58a7ec799a528fc20077c45fe2e94fedff6dcd88d136f7a39f417d77f584d5613aac615ed32aeb2ea393797ae1f7d5b2645eab57cb497a6d0cb
+ languageName: node
+ linkType: hard
+
"use-debounce@npm:^8.0.1":
version: 8.0.4
resolution: "use-debounce@npm:8.0.4"
@@ -27291,6 +30474,16 @@ __metadata:
languageName: node
linkType: hard
+"watchpack@npm:^2.4.1":
+ version: 2.4.4
+ resolution: "watchpack@npm:2.4.4"
+ dependencies:
+ glob-to-regexp: "npm:^0.4.1"
+ graceful-fs: "npm:^4.1.2"
+ checksum: 10c0/6c0901f75ce245d33991225af915eea1c5ae4ba087f3aee2b70dd377d4cacb34bef02a48daf109da9d59b2d31ec6463d924a0d72f8618ae1643dd07b95de5275
+ languageName: node
+ linkType: hard
+
"wcwidth@npm:^1.0.1":
version: 1.0.1
resolution: "wcwidth@npm:1.0.1"
@@ -27314,6 +30507,51 @@ __metadata:
languageName: node
linkType: hard
+"webpack-sources@npm:^3.3.3":
+ version: 3.3.3
+ resolution: "webpack-sources@npm:3.3.3"
+ checksum: 10c0/ab732f6933b513ba4d505130418995ddef6df988421fccf3289e53583c6a39e205c4a0739cee98950964552d3006604912679c736031337fb4a9d78d8576ed40
+ languageName: node
+ linkType: hard
+
+"webpack@npm:^5":
+ version: 5.100.2
+ resolution: "webpack@npm:5.100.2"
+ dependencies:
+ "@types/eslint-scope": "npm:^3.7.7"
+ "@types/estree": "npm:^1.0.8"
+ "@types/json-schema": "npm:^7.0.15"
+ "@webassemblyjs/ast": "npm:^1.14.1"
+ "@webassemblyjs/wasm-edit": "npm:^1.14.1"
+ "@webassemblyjs/wasm-parser": "npm:^1.14.1"
+ acorn: "npm:^8.15.0"
+ acorn-import-phases: "npm:^1.0.3"
+ browserslist: "npm:^4.24.0"
+ chrome-trace-event: "npm:^1.0.2"
+ enhanced-resolve: "npm:^5.17.2"
+ es-module-lexer: "npm:^1.2.1"
+ eslint-scope: "npm:5.1.1"
+ events: "npm:^3.2.0"
+ glob-to-regexp: "npm:^0.4.1"
+ graceful-fs: "npm:^4.2.11"
+ json-parse-even-better-errors: "npm:^2.3.1"
+ loader-runner: "npm:^4.2.0"
+ mime-types: "npm:^2.1.27"
+ neo-async: "npm:^2.6.2"
+ schema-utils: "npm:^4.3.2"
+ tapable: "npm:^2.1.1"
+ terser-webpack-plugin: "npm:^5.3.11"
+ watchpack: "npm:^2.4.1"
+ webpack-sources: "npm:^3.3.3"
+ peerDependenciesMeta:
+ webpack-cli:
+ optional: true
+ bin:
+ webpack: bin/webpack.js
+ checksum: 10c0/0add75d44c482634c6879a3fc87fa2af6a6c7c8eacda5d5f60ed778a2ce13d33fd6178a2b4750368706a49e769af6d828934c28914b4faa2e21be790f92b4110
+ languageName: node
+ linkType: hard
+
"whatwg-encoding@npm:^2.0.0":
version: 2.0.0
resolution: "whatwg-encoding@npm:2.0.0"
@@ -27584,6 +30822,21 @@ __metadata:
languageName: node
linkType: hard
+"ws@npm:~8.17.1":
+ version: 8.17.1
+ resolution: "ws@npm:8.17.1"
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: ">=5.0.2"
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ checksum: 10c0/f4a49064afae4500be772abdc2211c8518f39e1c959640457dcee15d4488628620625c783902a52af2dd02f68558da2868fd06e6fd0e67ebcd09e6881b1b5bfe
+ languageName: node
+ linkType: hard
+
"xdg-basedir@npm:^4.0.0":
version: 4.0.0
resolution: "xdg-basedir@npm:4.0.0"
@@ -27612,6 +30865,13 @@ __metadata:
languageName: node
linkType: hard
+"xmlhttprequest-ssl@npm:~2.1.1":
+ version: 2.1.2
+ resolution: "xmlhttprequest-ssl@npm:2.1.2"
+ checksum: 10c0/70d60869323e823f473a238f78fd108437edbc3690ecd5859c39c83217080090a18899b272e515769c0d1f518cc64cbed6b6995b23fdd7ba13b297d530b6e631
+ languageName: node
+ linkType: hard
+
"xtend@npm:^4.0.0":
version: 4.0.2
resolution: "xtend@npm:4.0.2"
@@ -27799,6 +31059,13 @@ __metadata:
languageName: node
linkType: hard
+"yoctocolors@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "yoctocolors@npm:2.1.1"
+ checksum: 10c0/85903f7fa96f1c70badee94789fade709f9d83dab2ec92753d612d84fcea6d34c772337a9f8914c6bed2f5fc03a428ac5d893e76fab636da5f1236ab725486d0
+ languageName: node
+ linkType: hard
+
"yup@npm:^1.4.0":
version: 1.6.1
resolution: "yup@npm:1.6.1"
@@ -27818,6 +31085,13 @@ __metadata:
languageName: node
linkType: hard
+"zod@npm:3.24.3, zod@npm:^3.24.1":
+ version: 3.24.3
+ resolution: "zod@npm:3.24.3"
+ checksum: 10c0/ab0369810968d0329a1a141e9418e01e5c9c2a4905cbb7cb7f5a131d6e9487596e1400e21eeff24c4a8ee28dacfa5bd6103893765c055b7a98c2006a5a4fc68d
+ languageName: node
+ linkType: hard
+
"zod@npm:^3.22.4":
version: 3.24.2
resolution: "zod@npm:3.24.2"
@@ -27825,10 +31099,10 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.24.1":
- version: 3.24.3
- resolution: "zod@npm:3.24.3"
- checksum: 10c0/ab0369810968d0329a1a141e9418e01e5c9c2a4905cbb7cb7f5a131d6e9487596e1400e21eeff24c4a8ee28dacfa5bd6103893765c055b7a98c2006a5a4fc68d
+"zod@npm:^3.25.56":
+ version: 3.25.56
+ resolution: "zod@npm:3.25.56"
+ checksum: 10c0/3800f01d4b1df932b91354eb1e648f69cc7e5561549e6d2bf83827d930a5f33bbf92926099445f6fc1ebb64ca9c6513ef9ae5e5409cfef6325f354bcf6fc9a24
languageName: node
linkType: hard