diff --git a/packages/plugin-ecommerce/src/carts/beforeChange.ts b/packages/plugin-ecommerce/src/carts/beforeChange.ts index ef3cd6a0fc9..141603d32f9 100644 --- a/packages/plugin-ecommerce/src/carts/beforeChange.ts +++ b/packages/plugin-ecommerce/src/carts/beforeChange.ts @@ -1,18 +1,24 @@ import type { CollectionBeforeChangeHook } from 'payload' +import { getCouponDiscountValue } from '../utilities/getCouponDiscountValue.js' + type Props = { + couponsSlug: string productsSlug: string variantsSlug: string } export const beforeChangeCart: (args: Props) => CollectionBeforeChangeHook = - ({ productsSlug, variantsSlug }) => + ({ couponsSlug, productsSlug, variantsSlug }) => async ({ data, req }) => { - // Update subtotal based on items in the cart + const payload = req.payload + if (data.items && Array.isArray(data.items)) { const priceField = `priceIn${data.currency}` let subtotal = 0 + let totalItemsDiscount = 0 + let totalCartDiscount = 0 for (const item of data.items) { if (item.variant) { @@ -27,7 +33,23 @@ export const beforeChangeCart: (args: Props) => CollectionBeforeChangeHook = }, }) - subtotal += variant[priceField] * item.quantity + const itemAmount = variant[priceField] * item.quantity + subtotal += itemAmount + + if (Array.isArray(item.discount?.discountLines)) { + for (const discountLine of item.discount.discountLines) { + const coupon = await payload.findByID({ + id: discountLine.coupon, + collection: couponsSlug, + }) + + discountLine.amount = discountLine.amount ?? 0 + discountLine.amount += getCouponDiscountValue(itemAmount, coupon) + item.discount.amount += discountLine.amount + + totalItemsDiscount += item.discount.amount + } + } } else { const id = typeof item.product === 'object' ? item.product.id : item.product @@ -40,12 +62,49 @@ export const beforeChangeCart: (args: Props) => CollectionBeforeChangeHook = }, }) - subtotal += product[priceField] * item.quantity + const itemAmount = product[priceField] * item.quantity + subtotal += itemAmount + + if (Array.isArray(item.discount?.discountLines)) { + for (const discountLine of item.discount.discountLines) { + const coupon = await payload.findByID({ + id: discountLine.coupon, + collection: couponsSlug, + }) + + discountLine.amount = discountLine.amount ?? 0 + discountLine.amount += getCouponDiscountValue(itemAmount, coupon) + if (!item.discount.total) { + item.discount.total = 0 + } + item.discount.total += discountLine.amount + totalItemsDiscount += item.discount.total + } + } } } + for (const discountLine of data.discount?.discountLines ?? []) { + const coupon = await payload.findByID({ + id: discountLine.coupon, + collection: couponsSlug, + }) + + discountLine.amount = getCouponDiscountValue(subtotal, coupon) + totalCartDiscount += discountLine.amount + } + data.subtotal = subtotal + data.discount = data.discount ?? {} + data.discount.cartAmount = totalCartDiscount + data.discount.lineItemsAmount = totalItemsDiscount + data.discount.totalAmount = totalCartDiscount + totalItemsDiscount + data.total = subtotal + data.discount.totalAmount } else { data.subtotal = 0 + data.discount.cartAmount = 0 + data.discount.lineItemsAmount = 0 + data.discount.totalAmount = 0 + data.total = 0 } } diff --git a/packages/plugin-ecommerce/src/carts/cartsCollection.ts b/packages/plugin-ecommerce/src/carts/cartsCollection.ts index bab6394962b..21ac0ae804f 100644 --- a/packages/plugin-ecommerce/src/carts/cartsCollection.ts +++ b/packages/plugin-ecommerce/src/carts/cartsCollection.ts @@ -8,16 +8,25 @@ import { currencyField } from '../fields/currencyField.js' import { beforeChangeCart } from './beforeChange.js' type Props = { + /** + * Slug of the coupons collection, defaults to 'coupons'. + */ + couponsSlug?: string currenciesConfig?: CurrenciesConfig /** * Slug of the customers collection, defaults to 'users'. */ customersSlug?: string + enableCoupons?: boolean /** * Enables support for variants in the cart. * Defaults to false. */ enableVariants?: boolean + /** + * Enables support for coupons in the cart. + * Defaults to false. + */ overrides?: { fields?: FieldsOverride } & Partial> /** * Slug of the products collection, defaults to 'products'. @@ -31,8 +40,10 @@ type Props = { export const cartsCollection: (props?: Props) => CollectionConfig = (props) => { const { + couponsSlug = 'coupons', currenciesConfig, customersSlug = 'users', + enableCoupons = false, enableVariants = false, overrides, productsSlug = 'products', @@ -82,6 +93,67 @@ export const cartsCollection: (props?: Props) => CollectionConfig = (props) => { }, ], }, + + ...(enableCoupons && currenciesConfig + ? [ + { + name: 'discount', + type: 'group', + fields: [ + { + name: 'discountLines', + type: 'array', + fields: [ + { + name: 'coupon', + type: 'relationship', + hasMany: false, + label: 'Coupon', + relationTo: couponsSlug, + }, + + amountField({ + currenciesConfig, + overrides: { + name: 'amount', + label: ({ t }) => + // @ts-expect-error - translations are not typed in plugins yet + t('plugin-ecommerce:subtotal'), + }, + }), + ], + }, + + amountField({ + currenciesConfig, + overrides: { + name: 'totalAmount', + // @ts-expect-error - translations are not typed in plugins yet + label: ({ t }) => t('plugin-ecommerce:discount.amount'), + }, + }), + + amountField({ + currenciesConfig, + overrides: { + name: 'cartAmount', + // @ts-expect-error - translations are not typed in plugins yet + label: ({ t }) => t('plugin-ecommerce:discount.amount'), + }, + }), + + amountField({ + currenciesConfig, + overrides: { + name: 'lineItemsAmount', + // @ts-expect-error - translations are not typed in plugins yet + label: ({ t }) => t('plugin-ecommerce:discount.amount'), + }, + }), + ], + } as Field, + ] + : []), ...(currenciesConfig ? [ currencyField({ @@ -96,9 +168,20 @@ export const cartsCollection: (props?: Props) => CollectionConfig = (props) => { t('plugin-ecommerce:subtotal'), }, }), + amountField({ + currenciesConfig, + overrides: { + name: 'total', + label: ({ t }) => + // @ts-expect-error - translations are not typed in plugins yet + t('plugin-ecommerce:total'), + }, + }), ] : []), cartItemsField({ + currenciesConfig, + enableCoupons, enableVariants, overrides: { label: ({ t }) => @@ -135,7 +218,7 @@ export const cartsCollection: (props?: Props) => CollectionConfig = (props) => { hooks: { beforeChange: [ // This hook can be used to update the subtotal before saving the cart - beforeChangeCart({ productsSlug, variantsSlug }), + beforeChangeCart({ couponsSlug, productsSlug, variantsSlug }), ...(overrides?.hooks?.beforeChange || []), ], ...overrides?.hooks, diff --git a/packages/plugin-ecommerce/src/coupons/couponsCollection.ts b/packages/plugin-ecommerce/src/coupons/couponsCollection.ts new file mode 100644 index 00000000000..0d63a2e9091 --- /dev/null +++ b/packages/plugin-ecommerce/src/coupons/couponsCollection.ts @@ -0,0 +1,166 @@ +import type { CollectionConfig, Field } from 'payload' + +import type { CollectionOverride } from '../types.js' + +type Props = { + /** + * Slug of the customers collection, defaults to 'users'. + */ + customersSlug?: string + + overrides?: CollectionOverride + + /** + * Slug of the products collection, defaults to 'products'. + */ + productsSlug?: string +} + +export const couponsCollection: (props?: Props) => CollectionConfig = (props) => { + const { customersSlug = 'customers', productsSlug = 'products' } = props || {} + + const defaultFields: Field[] = [ + { + name: 'title', + type: 'text', + label: 'Title', + required: true, + }, + { + name: 'identifier', + type: 'text', + label: 'Unique identifier', + required: true, + unique: true, + }, + { + name: 'appliesTo', + type: 'select', + defaultValue: 'cart', + options: [ + { + label: 'Cart', + value: 'cart', + }, + { + label: 'Product', + value: 'product', + }, + ], + required: true, + }, + { + name: 'eligbleProducts', + type: 'relationship', + admin: { + condition: ({ appliesTo }) => appliesTo === 'product', + }, + hasMany: true, + label: 'Eligble products', + relationTo: productsSlug, + }, + { + type: 'row', + fields: [ + { + name: 'type', + type: 'select', + defaultValue: 'flat', + label: 'Discount type', + options: [ + { + label: 'Flat rate', + value: 'flat', + }, + { + label: 'Percentage', + value: 'percentage', + }, + ], + required: true, + }, + { + name: 'value', + type: 'number', + label: 'Discount value', + required: true, + validate: (value: null | number | undefined, { data }: { data: any }) => { + if (!value) { + return 'Please enter a value.' + } + + if (data.type === 'flat') { + if (value <= 0) { + return 'Value must be higer than 0.' + } + } else { + if (value < 1 || value > 100) { + return 'Value must be between 1 and 100.' + } + } + + return true + }, + }, + ], + }, + { + type: 'row', + fields: [ + { + name: 'maxClaims', + type: 'number', + label: 'Max amount of claims', + }, + { + name: 'numberOfClaims', + type: 'number', + admin: { + disabled: true, + }, + defaultValue: 0, + label: 'Number of claims', + required: true, + }, + { + name: 'eligbleCustomers', + type: 'relationship', + + hasMany: true, + label: 'Eligble customers', + relationTo: customersSlug, + }, + ], + }, + { + type: 'row', + fields: [ + { + name: 'validFrom', + type: 'date', + label: 'Valid from date', + }, + { + name: 'validTo', + type: 'date', + label: 'Valid until date', + }, + ], + }, + ] + + const fields = + typeof props?.overrides?.fields === 'function' + ? props.overrides.fields({ defaultFields }) + : defaultFields + + return { + slug: 'coupons', + ...props?.overrides, + admin: { + useAsTitle: 'title', + ...(props?.overrides?.admin ?? {}), + }, + fields, + } +} diff --git a/packages/plugin-ecommerce/src/endpoints/applyCoupon.ts b/packages/plugin-ecommerce/src/endpoints/applyCoupon.ts new file mode 100644 index 00000000000..22a67983ab5 --- /dev/null +++ b/packages/plugin-ecommerce/src/endpoints/applyCoupon.ts @@ -0,0 +1,236 @@ +import type { + BasePayload, + Endpoint, + JsonObject, + TypedCollection, + TypedUser, + TypeWithID, +} from 'payload' + +import { addDataAndFileToRequest } from 'payload' + +import type { Cart, CartItem, Coupon } from '../types.js' + +type ApplyCoupon = (props: Props) => Endpoint['handler'] + +type Props = { + cartsSlug?: string + couponsSlug?: string + transactionsSlug?: string +} + +function isProductEligible(coupon: Coupon, product: (JsonObject & TypeWithID) | number | string) { + return coupon.eligbleProducts.some((eligbleProduct) => { + const eligbleProductId = + typeof eligbleProduct === 'object' ? String(eligbleProduct.id) : String(eligbleProduct) + const productId = typeof product === 'object' ? String(product.id) : String(product) + + return eligbleProductId === productId + }) +} + +async function validateCoupon({ + cartsSlug, + coupon, + couponsSlug, + payload, + user, +}: { + cartsSlug: string + coupon: TypedCollection['coupons'] + couponsSlug: string + payload: BasePayload + user: TypedUser +}) { + const cartId = user?.cart?.docs?.[0]?.id + if (!cartId) { + return 'You do not have a cart to apply the coupon to.' + } + + const cart = (await payload.findByID({ + id: cartId, + collection: cartsSlug, + })) as Cart | undefined + + if (!cart) { + return 'Cart not found.' + } + + if (coupon.maxClaims && coupon.maxClaims > coupon.numberOfClaims) { + return 'Coupon has reached the maximum amount of claims.' + } + + if (coupon.validFrom && new Date(coupon.validFrom).getTime() > new Date().getTime()) { + return 'Coupon is not valid yet.' + } + + if (coupon.validTo && new Date(coupon.validTo).getTime() < new Date().getTime()) { + return 'Coupon has expired.' + } + + if (cart?.discount?.discountLines?.some((line) => line.coupon === coupon.id)) { + return 'Coupon has already been applied to the cart.' + } + + if ( + cart.items?.some((item: CartItem) => + item.discount?.discountLines?.some((line) => { + return line.coupon === coupon.id + }), + ) + ) { + return 'Coupon has already been applied to one of the items in the cart.' + } + + if (coupon.eligbleCustomers) { + const errorMessage = 'You are not eligble to claim this coupon.' + + if (!user) { + return errorMessage + } + + const couponMatch = await payload.find({ + collection: couponsSlug, + where: { + eligbleCustomers: { + in: user.id, + }, + }, + }) + + if (couponMatch.docs.length === 0) { + return errorMessage + } + } + + if (coupon.eligbleProducts) { + if (!cart.items.some((item: CartItem) => isProductEligible(coupon as Coupon, item.product))) { + return 'Coupon is not valid for the products in your cart.' + } + } + + return true +} + +/** + * Handles the endpoint for initiating payments. We will handle checking the amount and product and variant prices here before it is sent to the payment provider. + * This is the first step in the payment process. + */ +export const applyCouponHandler: ApplyCoupon = + ({ cartsSlug = 'carts', couponsSlug = 'coupons' }) => + async (req) => { + await addDataAndFileToRequest(req) + + const payload = req.payload + const data = req.data + const user = req.user + const cart = user?.cart?.docs?.[0] + const couponCode: string = data?.couponCode + + const couponsQuery = await payload.find({ + collection: 'coupons', + + where: { + identifier: { + equals: couponCode, + }, + }, + }) + + const foundCoupon = couponsQuery.docs?.[0] as Coupon | undefined + + if (!foundCoupon) { + return Response.json( + { + message: 'Coupon does not exist.', + }, + { + status: 400, + }, + ) + } + + const isCouponValid = await validateCoupon({ + cartsSlug, + coupon: foundCoupon, + couponsSlug, + payload, + user, + }) + + if (isCouponValid !== true) { + return Response.json( + { + message: isCouponValid, + }, + { + status: 400, + }, + ) + } + + let newData = { + ...cart, + } + + if (foundCoupon.appliesTo === 'cart') { + newData = { + ...newData, + discount: { + ...newData.discount, + discountLines: [ + ...(newData.discount?.discountLines ?? []), + { + amount: 0, // will be calculated in updateSubTotalHook + coupon: foundCoupon.id, + }, + ], + }, + } + } else { + newData = { ...cart } + newData.items = newData.items.map((item: CartItem) => { + // check if coupon applies to this product + + if (isProductEligible(foundCoupon, item.product)) { + return { + ...item, + discount: { + ...item.discount, + amount: 0, // will be calculated in updateSubTotalHook + discountLines: [ + ...(item.discount?.discountLines ?? []), + { + amount: 0, // will be calculated in updateSubTotalHook + coupon: foundCoupon.id, + }, + ], + }, + } + } + + return item + }) + } + + try { + const result = await payload.update({ + id: cart.id, + collection: cartsSlug, + data: newData, + }) + console.dir({ newData, result }, { depth: null }) + return Response.json(result) + } catch (error) { + payload.logger.error(error, 'Error applying coupon.') + + return Response.json( + { + message: `An error occurred while applying the coupon.`, + }, + { + status: 500, + }, + ) + } + } diff --git a/packages/plugin-ecommerce/src/endpoints/removeCoupon.ts b/packages/plugin-ecommerce/src/endpoints/removeCoupon.ts new file mode 100644 index 00000000000..0daf5d67d3d --- /dev/null +++ b/packages/plugin-ecommerce/src/endpoints/removeCoupon.ts @@ -0,0 +1,108 @@ +import type { Endpoint } from 'payload' + +import { addDataAndFileToRequest } from 'payload' + +import type { Cart, CartItem } from '../types.js' + +type RemoveCoupon = (props: Props) => Endpoint['handler'] + +type Props = { + cartsSlug?: string + couponsSlug?: string +} + +/** + * Handles the endpoint for removing a coupon from a cart. + */ +export const removeCouponHandler: RemoveCoupon = + ({ cartsSlug = 'carts', couponsSlug = 'coupons' }) => + async (req) => { + await addDataAndFileToRequest(req) + + const payload = req.payload + const data = req.data + const user = req.user + const cart: Cart = user?.cart?.docs?.[0] + const couponCode: string = data?.couponCode + + if (!couponCode) { + return Response.json( + { + message: 'No coupon code provided.', + }, + { + status: 400, + }, + ) + } + + const couponsQuery = await payload.find({ + collection: couponsSlug, + where: { + identifier: { + equals: couponCode, + }, + }, + }) + + const coupon = couponsQuery.docs[0] + + if (!coupon) { + return Response.json( + { + message: 'Coupon code is invalid.', + }, + { + status: 400, + }, + ) + } + + const newCart: Cart = { ...cart } + + // Remove cart-level coupon + if (newCart.discount?.discountLines && newCart.discount.discountLines.length > 0) { + newCart.discount.discountLines = newCart.discount.discountLines.filter( + (discountLine) => discountLine.coupon !== coupon.id, + ) + } + + // Remove item-level coupons + if (newCart.items && newCart.items.length > 0) { + newCart.items = newCart.items.map((item: CartItem) => { + if (item.discount?.discountLines && item.discount.discountLines.length > 0) { + return { + ...item, + discount: { + ...item.discount, + discountLines: item.discount.discountLines.filter( + (discountLine) => discountLine.coupon !== coupon.id, + ), + }, + } + } + + return item + }) + } + + try { + const result = await payload.update({ + id: cart.id, + collection: cartsSlug, + data: newCart, + }) + return Response.json(result) + } catch (error) { + payload.logger.error(error, 'Error removing coupon.') + + return Response.json( + { + message: `An error occurred while removing the coupon.`, + }, + { + status: 500, + }, + ) + } + } diff --git a/packages/plugin-ecommerce/src/fields/cartItemsField.ts b/packages/plugin-ecommerce/src/fields/cartItemsField.ts index 453f4add445..6b7ed045c4d 100644 --- a/packages/plugin-ecommerce/src/fields/cartItemsField.ts +++ b/packages/plugin-ecommerce/src/fields/cartItemsField.ts @@ -6,10 +6,19 @@ import { amountField } from './amountField.js' import { currencyField } from './currencyField.js' type Props = { + /** + * Slug of the coupons collection, defaults to 'coupons'. + */ + couponsSlug?: string /** * Include this in order to enable support for currencies per item in the cart. */ currenciesConfig?: CurrenciesConfig + /** + * Enables coupons for specific products / cart items. + * Defaults to false. + */ + enableCoupons?: boolean enableVariants?: boolean /** * Enables individual prices for each item in the cart. @@ -17,6 +26,7 @@ type Props = { */ individualPrices?: boolean overrides?: Partial + /** * Slug of the products collection, defaults to 'products'. */ @@ -29,7 +39,9 @@ type Props = { export const cartItemsField: (props?: Props) => ArrayField = (props) => { const { + couponsSlug = 'coupons', currenciesConfig, + enableCoupons = true, enableVariants = false, individualPrices, overrides, @@ -61,6 +73,47 @@ export const cartItemsField: (props?: Props) => ArrayField = (props) => { } as Field, ] : []), + ...(enableCoupons && currenciesConfig + ? [ + { + name: 'discount', + type: 'group', + fields: [ + amountField({ + currenciesConfig, + overrides: { + name: 'amount', + // @ts-expect-error - translations are not typed in plugins yet + label: ({ t }) => t('plugin-ecommerce:discount.amount'), + }, + }), + { + name: 'discountLines', + type: 'array' as const, + fields: [ + { + name: 'coupon', + type: 'relationship', + hasMany: false, + label: 'Coupons', + relationTo: couponsSlug, + }, + + amountField({ + currenciesConfig, + overrides: { + name: 'amount', + label: ({ t }) => + // @ts-expect-error - translations are not typed in plugins yet + t('plugin-ecommerce:subtotal'), + }, + }), + ], + }, + ], + } as Field, + ] + : []), { name: 'quantity', type: 'number', @@ -71,6 +124,7 @@ export const cartItemsField: (props?: Props) => ArrayField = (props) => { min: 1, required: true, }, + ...(currenciesConfig && individualPrices ? [amountField({ currenciesConfig })] : []), ...(currenciesConfig ? [currencyField({ currenciesConfig })] : []), ], diff --git a/packages/plugin-ecommerce/src/index.ts b/packages/plugin-ecommerce/src/index.ts index 0fe02831b3e..fa3c79dacae 100644 --- a/packages/plugin-ecommerce/src/index.ts +++ b/packages/plugin-ecommerce/src/index.ts @@ -6,8 +6,11 @@ import type { EcommercePluginConfig, SanitizedEcommercePluginConfig } from './ty import { addressesCollection } from './addresses/addressesCollection.js' import { cartsCollection } from './carts/cartsCollection.js' +import { couponsCollection } from './coupons/couponsCollection.js' +import { applyCouponHandler } from './endpoints/applyCoupon.js' import { confirmOrderHandler } from './endpoints/confirmOrder.js' import { initiatePaymentHandler } from './endpoints/initiatePayment.js' +import { removeCouponHandler } from './endpoints/removeCoupon.js' import { ordersCollection } from './orders/ordersCollection.js' import { productsCollection } from './products/productsCollection.js' import { transactionsCollection } from './transactions/transactionsCollection.js' @@ -116,6 +119,7 @@ export const ecommercePlugin = const carts = cartsCollection({ currenciesConfig, customersSlug: collectionSlugMap.customers, + enableCoupons: sanitizedPluginConfig.coupons, enableVariants: Boolean(productsConfig.variants), overrides: sanitizedPluginConfig.carts === true @@ -238,6 +242,35 @@ export const ecommercePlugin = incomingConfig.i18n.translations = {} } + if (sanitizedPluginConfig.coupons) { + if (!Array.isArray(incomingConfig.endpoints)) { + incomingConfig.endpoints = [] + } + + const coupons = couponsCollection({ + customersSlug: collectionSlugMap.customers, + overrides: + typeof sanitizedPluginConfig.coupons === 'boolean' + ? undefined + : sanitizedPluginConfig.coupons, + productsSlug: collectionSlugMap.products, + }) + + incomingConfig.collections.push(coupons) + + incomingConfig.endpoints.push({ + handler: applyCouponHandler({ cartsSlug: collectionSlugMap.carts }), + method: 'post', + path: `/apply-coupon`, + }) + + incomingConfig.endpoints.push({ + handler: removeCouponHandler({ cartsSlug: collectionSlugMap.carts }), + method: 'post', + path: `/remove-coupon`, + }) + } + incomingConfig.i18n.translations = deepMergeSimple( translations, incomingConfig.i18n?.translations, diff --git a/packages/plugin-ecommerce/src/react/provider/index.tsx b/packages/plugin-ecommerce/src/react/provider/index.tsx index 4ef41c32194..f731e0feada 100644 --- a/packages/plugin-ecommerce/src/react/provider/index.tsx +++ b/packages/plugin-ecommerce/src/react/provider/index.tsx @@ -9,6 +9,7 @@ import type { ContextProps, EcommerceContext as EcommerceContextType } from './t const defaultContext: EcommerceContextType = { addItem: async () => {}, + applyCoupon: async () => {}, clearCart: async () => {}, confirmOrder: async () => {}, createAddress: async () => {}, @@ -33,6 +34,7 @@ const defaultContext: EcommerceContextType = { incrementItem: async () => {}, initiatePayment: async () => {}, paymentMethods: [], + removeCoupon: async () => {}, removeItem: async () => {}, setCurrency: () => {}, updateAddress: async () => {}, @@ -105,6 +107,9 @@ export const EcommerceProvider: React.FC = ({ const baseQuery = { depth: 0, populate: { + items: { + discount: true, + }, products: { [priceField]: true, }, @@ -114,8 +119,10 @@ export const EcommerceProvider: React.FC = ({ }, }, select: { + discount: true, items: true, subtotal: true, + total: true, }, } @@ -180,6 +187,60 @@ export const EcommerceProvider: React.FC = ({ [baseAPIURL, cartQuery, cartsSlug], ) + const applyCoupon = useCallback( + async (couponCode: string, cartID: DefaultDocumentIDType) => { + const response = await fetch(`/api/apply-coupon`, { + body: JSON.stringify({ + couponCode, + }), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + if (!response.ok) { + const errorText = await response.json() + throw new Error(errorText.message || 'Failed to apply coupon') + } + const data = await response.json() + if (data.error) { + throw new Error(data.error) + } + + const cart = await getCart(cartID) + setCart(cart) + }, + [getCart, setCart], + ) + + const removeCoupon = useCallback( + async (couponCode: string, cartID: DefaultDocumentIDType) => { + const response = await fetch(`/api/remove-coupon`, { + body: JSON.stringify({ + couponCode, + }), + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to remove coupon: ${errorText}`) + } + const data = await response.json() + if (data.error) { + throw new Error(`Remove coupon error: ${data.error}`) + } + + const cart = await getCart(cartID) + setCart(cart) + }, + [getCart, setCart], + ) + const updateCart = useCallback( async (cartID: DefaultDocumentIDType, data: Partial) => { const query = qs.stringify(cartQuery) @@ -785,6 +846,7 @@ export const EcommerceProvider: React.FC = ({ value={{ addItem, addresses, + applyCoupon, cart, clearCart, confirmOrder, @@ -795,6 +857,7 @@ export const EcommerceProvider: React.FC = ({ incrementItem, initiatePayment, paymentMethods, + removeCoupon, removeItem, selectedPaymentMethod, setCurrency, @@ -857,13 +920,31 @@ export const useCurrency = () => { } export const useCart = () => { - const { addItem, cart, clearCart, decrementItem, incrementItem, removeItem } = useEcommerce() + const { + addItem, + applyCoupon, + cart, + clearCart, + decrementItem, + incrementItem, + removeCoupon, + removeItem, + } = useEcommerce() if (!addItem) { throw new Error('useCart must be used within an EcommerceProvider') } - return { addItem, cart, clearCart, decrementItem, incrementItem, removeItem } + return { + addItem, + applyCoupon, + cart, + clearCart, + decrementItem, + incrementItem, + removeCoupon, + removeItem, + } } export const usePayments = () => { diff --git a/packages/plugin-ecommerce/src/react/provider/types.ts b/packages/plugin-ecommerce/src/react/provider/types.ts index 6639dd30701..91738701e00 100644 --- a/packages/plugin-ecommerce/src/react/provider/types.ts +++ b/packages/plugin-ecommerce/src/react/provider/types.ts @@ -104,6 +104,12 @@ export type EcommerceContext = { * This is used to manage shipping and billing addresses. */ addresses?: TypedCollection['addresses'][] + + /** + * Apply coupon to cart or cartItem, depending on the appliesTo of that coupon. + * Takes a unique coupon code as body. + */ + applyCoupon: (couponCode: string, cartID: DefaultDocumentIDType) => Promise /** * The current data of the cart. */ @@ -156,6 +162,11 @@ export type EcommerceContext = { options?: { additionalData: Record }, ) => Promise paymentMethods: PaymentAdapterClient[] + /** + * removes coupon from cart or cartItem, depending on the appliesTo of that coupon. + * Takes a unique coupon code as body. + */ + removeCoupon: (couponCode: string, cartID: DefaultDocumentIDType) => Promise /** * Remove an item from the cart by its index ID. */ diff --git a/packages/plugin-ecommerce/src/types.ts b/packages/plugin-ecommerce/src/types.ts index ce849d4ba74..8e29f5b3612 100644 --- a/packages/plugin-ecommerce/src/types.ts +++ b/packages/plugin-ecommerce/src/types.ts @@ -16,6 +16,13 @@ export type CollectionOverride = { fields?: FieldsOverride } & Partial< > export type CartItem = { + discount?: { + amount?: number + discountLines?: { + coupon?: Coupon | DefaultDocumentIDType + id?: string + }[] + } id: DefaultDocumentIDType product: DefaultDocumentIDType | TypedCollection['products'] quantity: number @@ -25,6 +32,13 @@ export type CartItem = { type DefaultCartType = { currency?: string customer?: DefaultDocumentIDType | TypedCollection['customers'] + discount?: { + amount?: number + discountLines?: { + coupon?: Coupon | DefaultDocumentIDType + id?: string + }[] + } id: DefaultDocumentIDType items: CartItem[] subtotal?: number @@ -32,6 +46,21 @@ type DefaultCartType = { export type Cart = DefaultCartType +export type Coupon = { + appliesTo: 'cart' | 'product' + eligbleCustomers: Array + eligbleProducts: Array + id: DefaultDocumentIDType + identifier: string + maxClaims?: number + numberOfClaims: number + title: string + type: 'flat' | 'percentage' + validFrom?: string + validTo?: string + value: number +} + type InitiatePaymentReturnType = { [key: string]: any // Allows for additional data to be returned, such as payment method specific data message: string @@ -354,6 +383,12 @@ export type EcommercePluginConfig = { * Defaults to true. */ carts?: boolean | CartsConfig + /** + * Enable/disable coupons feature. + * + * Defaults to true. + */ + coupons?: boolean /** * Configure supported currencies and default settings. * diff --git a/packages/plugin-ecommerce/src/utilities/getCouponDiscountValue.ts b/packages/plugin-ecommerce/src/utilities/getCouponDiscountValue.ts new file mode 100644 index 00000000000..902246ebdad --- /dev/null +++ b/packages/plugin-ecommerce/src/utilities/getCouponDiscountValue.ts @@ -0,0 +1,11 @@ +export function getCouponDiscountValue(baseAmount: number, coupon: any) { + if (coupon.type === 'flat') { + if (coupon.value > baseAmount) { + return 0 + } + + return coupon.value * -1 + } + + return baseAmount * (coupon.value / 100) * -1 +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cce836a53af..d04fbef8c30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 1.50.0 '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)) + version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.99.9(@swc/core@1.11.29)) '@sentry/node': specifier: ^8.33.1 version: 8.55.0 @@ -101,7 +101,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.16.3) escape-html: specifier: ^1.0.3 version: 1.0.3 @@ -323,7 +323,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.16.3) pg: specifier: 8.16.3 version: 8.16.3 @@ -369,7 +369,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.16.3) prompts: specifier: 2.4.2 version: 2.4.2 @@ -412,7 +412,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.16.3) pg: specifier: 8.16.3 version: 8.16.3 @@ -455,7 +455,7 @@ importers: version: 2.0.3 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.16.3) prompts: specifier: 2.4.2 version: 2.4.2 @@ -1017,6 +1017,9 @@ importers: '@payloadcms/ui': specifier: workspace:* version: link:../ui + qs-esm: + specifier: 7.0.2 + version: 7.0.2 react: specifier: 19.1.0 version: 19.1.0 @@ -1112,7 +1115,7 @@ importers: dependencies: next: specifier: ^15.2.3 - version: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + version: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) devDependencies: '@payloadcms/eslint-config': specifier: workspace:* @@ -1177,7 +1180,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)) + version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.99.9(@swc/core@1.11.29)) '@sentry/types': specifier: ^8.33.1 version: 8.55.0 @@ -1735,7 +1738,7 @@ importers: version: 16.11.0 next: specifier: 15.3.2 - version: 15.3.2(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + version: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) payload: specifier: workspace:* version: link:../../packages/payload @@ -1763,7 +1766,7 @@ importers: version: 19.1.2(@types/react@19.1.0) '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) + version: 4.5.2(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) eslint: specifier: ^9.16.0 version: 9.22.0(jiti@2.4.2) @@ -1787,10 +1790,224 @@ importers: version: 5.7.3 vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) + version: 5.1.4(typescript@5.7.3)(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + vitest: + specifier: 3.2.3 + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + + templates/ecommerce: + dependencies: + '@payloadcms/admin-bar': + specifier: workspace:* + version: link:../../packages/admin-bar + '@payloadcms/db-mongodb': + specifier: workspace:* + version: link:../../packages/db-mongodb + '@payloadcms/email-nodemailer': + specifier: workspace:* + version: link:../../packages/email-nodemailer + '@payloadcms/live-preview-react': + specifier: workspace:* + version: link:../../packages/live-preview-react + '@payloadcms/next': + specifier: workspace:* + version: link:../../packages/next + '@payloadcms/payload-cloud': + specifier: workspace:* + version: link:../../packages/payload-cloud + '@payloadcms/plugin-ecommerce': + specifier: workspace:* + version: link:../../packages/plugin-ecommerce + '@payloadcms/plugin-form-builder': + specifier: workspace:* + version: link:../../packages/plugin-form-builder + '@payloadcms/plugin-nested-docs': + specifier: workspace:* + version: link:../../packages/plugin-nested-docs + '@payloadcms/plugin-redirects': + specifier: workspace:* + version: link:../../packages/plugin-redirects + '@payloadcms/plugin-search': + specifier: workspace:* + version: link:../../packages/plugin-search + '@payloadcms/plugin-seo': + specifier: workspace:* + version: link:../../packages/plugin-seo + '@payloadcms/plugin-stripe': + specifier: workspace:* + version: link:../../packages/plugin-stripe + '@payloadcms/richtext-lexical': + specifier: workspace:* + version: link:../../packages/richtext-lexical + '@payloadcms/translations': + specifier: workspace:* + version: link:../../packages/translations + '@payloadcms/ui': + specifier: workspace:* + version: link:../../packages/ui + '@radix-ui/react-accordion': + specifier: 1.2.11 + version: 1.2.11(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-checkbox': + specifier: ^1.1.4 + version: 1.3.2(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dialog': + specifier: ^1.1.6 + version: 1.1.14(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-label': + specifier: ^2.1.2 + version: 2.1.7(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-select': + specifier: ^2.1.6 + version: 2.2.5(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.1.2 + version: 1.2.3(@types/react@19.0.1)(react@19.1.0) + '@stripe/react-stripe-js': + specifier: ^3 + version: 3.7.0(@stripe/stripe-js@4.10.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@stripe/stripe-js': + specifier: ^4.0.0 + version: 4.10.0 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.1 + clsx: + specifier: ^2.1.0 + version: 2.1.1 + cross-env: + specifier: 7.0.3 + version: 7.0.3 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + dotenv: + specifier: 16.4.7 + version: 16.4.7 + embla-carousel-auto-scroll: + specifier: ^8.1.5 + version: 8.6.0(embla-carousel@8.6.0) + embla-carousel-react: + specifier: ^8.5.2 + version: 8.6.0(react@19.1.0) + geist: + specifier: ^1.3.0 + version: 1.4.2(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)) + jsonwebtoken: + specifier: 9.0.1 + version: 9.0.1 + lucide-react: + specifier: ^0.477.0 + version: 0.477.0(react@19.1.0) + next: + specifier: ^15.1.0 + version: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + payload: + specifier: workspace:* + version: link:../../packages/payload + prism-react-renderer: + specifier: ^2.3.1 + version: 2.4.1(react@19.1.0) + qs-esm: + specifier: ^7 + version: 7.0.2 + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + react-hook-form: + specifier: 7.54.1 + version: 7.54.1(react@19.1.0) + sharp: + specifier: 0.32.6 + version: 0.32.6 + stripe: + specifier: ^10.2.0 + version: 10.17.0 + tailwind-merge: + specifier: ^2.3.0 + version: 2.6.0 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.11) + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.1 + '@playwright/test': + specifier: 1.50.0 + version: 1.50.0 + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@4.1.11) + '@tailwindcss/postcss': + specifier: 4.0.12 + version: 4.0.12 + '@tailwindcss/typography': + specifier: ^0.5.12 + version: 0.5.16(tailwindcss@4.1.11) + '@testing-library/react': + specifier: 16.3.0 + version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.10 + '@types/node': + specifier: 22.5.4 + version: 22.5.4 + '@types/react': + specifier: 19.0.1 + version: 19.0.1 + '@types/react-dom': + specifier: 19.0.1 + version: 19.0.1 + '@vercel/git-hooks': + specifier: ^1.0.0 + version: 1.0.0 + '@vitejs/plugin-react': + specifier: 4.5.2 + version: 4.5.2(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + eslint: + specifier: ^9.16.0 + version: 9.22.0(jiti@2.4.2) + eslint-config-next: + specifier: 15.1.0 + version: 15.1.0(eslint-plugin-import-x@4.6.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.22.0(jiti@2.4.2))(typescript@5.7.3) + jsdom: + specifier: 26.1.0 + version: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + lint-staged: + specifier: ^15.2.2 + version: 15.2.7 + playwright: + specifier: 1.50.0 + version: 1.50.0 + playwright-core: + specifier: 1.50.0 + version: 1.50.0 + postcss: + specifier: ^8.4.38 + version: 8.5.6 + prettier: + specifier: ^3.4.2 + version: 3.5.3 + prettier-plugin-tailwindcss: + specifier: ^0.6.11 + version: 0.6.13(prettier@3.5.3) + tailwindcss: + specifier: ^4.0.12 + version: 4.1.11 + typescript: + specifier: 5.7.3 + version: 5.7.3 + vite-tsconfig-paths: + specifier: 5.1.4 + version: 5.1.4(typescript@5.7.3)(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) vitest: specifier: 3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@1.21.6)(jsdom@26.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) templates/website: dependencies: @@ -1865,7 +2082,7 @@ importers: version: 0.378.0(react@19.1.0) next: specifier: 15.3.3 - version: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + version: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) next-sitemap: specifier: ^4.2.3 version: 4.2.3(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)) @@ -1920,7 +2137,7 @@ importers: version: 19.1.2(@types/react@19.1.0) '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) + version: 4.5.2(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) autoprefixer: specifier: ^10.4.19 version: 10.4.21(postcss@8.5.6) @@ -1956,10 +2173,10 @@ importers: version: 5.7.3 vite-tsconfig-paths: specifier: 5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) + version: 5.1.4(typescript@5.7.3)(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) vitest: specifier: 3.2.3 - version: 3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.6)(jsdom@26.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + version: 3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) test: devDependencies: @@ -2082,7 +2299,7 @@ importers: version: link:../packages/ui '@sentry/nextjs': specifier: ^8.33.1 - version: 8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29)) + version: 8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.99.9(@swc/core@1.11.29)) '@sentry/react': specifier: ^7.77.0 version: 7.120.3(react@19.1.0) @@ -2124,7 +2341,7 @@ importers: version: 0.31.4 drizzle-orm: specifier: 0.44.2 - version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3) + version: 0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.16.3) escape-html: specifier: 1.0.3 version: 1.0.3 @@ -3923,29 +4140,12 @@ packages: cpu: [arm64] os: [darwin] - '@img/sharp-darwin-arm64@0.34.2': - resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.33.5': - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - '@img/sharp-darwin-x64@0.34.2': resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.0.4': - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.1.0': resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} cpu: [arm64] @@ -3997,64 +4197,28 @@ packages: cpu: [arm64] os: [linux] - '@img/sharp-linux-arm64@0.34.2': - resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.33.5': - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - '@img/sharp-linux-arm@0.34.2': resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-s390x@0.33.5': - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - '@img/sharp-linux-s390x@0.34.2': resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.33.5': - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - '@img/sharp-linux-x64@0.34.2': resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.33.5': - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.2': resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] - os: [win32] - - '@img/sharp-linuxmusl-x64@0.33.5': - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] os: [linux] '@img/sharp-linuxmusl-x64@0.34.2': @@ -4063,11 +4227,6 @@ packages: cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.33.5': - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - '@img/sharp-wasm32@0.34.2': resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -4079,30 +4238,26 @@ packages: cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.33.5': - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - '@img/sharp-win32-ia32@0.34.2': resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.33.5': - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@img/sharp-win32-x64@0.34.2': resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -4893,9 +5048,6 @@ packages: cpu: [x64] os: [win32] - '@petamoriken/float16@3.9.2': - resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4917,6 +5069,19 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-accordion@1.2.11': + resolution: {integrity: sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.1.0 + react-dom: 19.1.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-arrow@1.1.7': resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} peerDependencies: @@ -4943,6 +5108,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.11': + resolution: {integrity: sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: 19.1.0 + react-dom: 19.1.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -6884,11 +7062,8 @@ packages: bare-events@2.5.4: resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} - bare-events@2.5.4: - resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==} - - bare-fs@4.1.6: - resolution: {integrity: sha512-25RsLF33BqooOEFNdMcEhMpJy8EoR88zSMrnOQOaM3USnOK2VmaJ1uaQEwPA6AQjrv1lXChScosN6CzbwbO9OQ==} + bare-fs@4.1.5: + resolution: {integrity: sha512-1zccWBMypln0jEE05LzZt+V/8y8AQsQQqxtklqaIyg5nu6OAYFhZxPXinJTSG+kU5qyNmeLgcn9AW7eHiCHVLA==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -7495,10 +7670,6 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -7716,10 +7887,6 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -8383,11 +8550,6 @@ packages: peerDependencies: next: '>=13.2.0' - gel@2.0.1: - resolution: {integrity: sha512-gfem3IGvqKqXwEq7XseBogyaRwGsQGuE7Cw/yQsjLGdgiyqX92G1xENPCE0ltunPGcsJIa6XBOTx/PK169mOqw==} - engines: {node: '>= 18.0.0'} - hasBin: true - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -8971,10 +9133,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} @@ -10181,9 +10339,6 @@ packages: pg-protocol@1.10.3: resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} - pg-protocol@1.7.0: - resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} - pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} @@ -10985,11 +11140,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -11012,10 +11162,6 @@ packages: resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} engines: {node: '>=14.15.0'} - sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - sharp@0.34.2: resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -11028,10 +11174,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} - engines: {node: '>= 0.4'} - shelljs@0.8.5: resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} engines: {node: '>=4'} @@ -11250,9 +11392,6 @@ packages: streamx@2.22.1: resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} - streamx@2.22.1: - resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -11454,8 +11593,8 @@ packages: tar-fs@2.1.3: resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} - tar-fs@3.1.0: - resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + tar-fs@3.0.10: + resolution: {integrity: sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} @@ -12117,11 +12256,6 @@ packages: engines: {node: '>= 8'} hasBin: true - which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} @@ -14435,21 +14569,11 @@ snapshots: optionalDependencies: cjs-module-lexer: 1.4.3 - '@img/sharp-darwin-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - optional: true - '@img/sharp-darwin-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.1.0 optional: true - '@img/sharp-darwin-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - optional: true - '@img/sharp-darwin-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.1.0 @@ -14482,61 +14606,31 @@ snapshots: '@img/sharp-libvips-linuxmusl-x64@1.1.0': optional: true - '@img/sharp-linux-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - optional: true - '@img/sharp-linux-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.1.0 optional: true - '@img/sharp-linux-arm@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - optional: true - '@img/sharp-linux-arm@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.1.0 optional: true - '@img/sharp-linux-s390x@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 - optional: true - '@img/sharp-linux-s390x@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.1.0 optional: true - '@img/sharp-linux-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - optional: true - '@img/sharp-linux-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.1.0 optional: true - '@img/sharp-linuxmusl-arm64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 optional: true - '@img/sharp-linuxmusl-x64@0.33.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - optional: true - '@img/sharp-linuxmusl-x64@0.34.2': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.1.0 @@ -14547,26 +14641,21 @@ snapshots: '@emnapi/runtime': 1.4.3 optional: true - '@img/sharp-wasm32@0.34.2': - dependencies: - '@emnapi/runtime': 1.4.3 - optional: true - '@img/sharp-win32-arm64@0.34.2': optional: true - '@img/sharp-win32-ia32@0.33.5': - optional: true - '@img/sharp-win32-ia32@0.34.2': optional: true - '@img/sharp-win32-x64@0.33.5': - optional: true - '@img/sharp-win32-x64@0.34.2': optional: true + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -15511,9 +15600,6 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@5.3.0': optional: true - '@petamoriken/float16@3.9.2': - optional: true - '@pkgjs/parseargs@0.11.0': optional: true @@ -15535,6 +15621,23 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-accordion@1.2.11(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collapsible': 1.1.11(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.1 + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -15585,6 +15688,22 @@ snapshots: '@types/react': 19.1.0 '@types/react-dom': 19.1.2(@types/react@19.1.0) + '@radix-ui/react-collapsible@1.1.11(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.1)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.1)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.1 + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.1)(react@19.1.0) @@ -16267,21 +16386,18 @@ snapshots: '@sentry/utils': 7.120.3 localforage: 1.10.0 - '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))': + '@sentry/nextjs@8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.99.9(@swc/core@1.11.29))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.27.0 - '@rollup/plugin-commonjs': 26.0.1(rollup@3.29.5) - '@sentry-internal/browser-utils': 8.37.1 - '@sentry/core': 8.37.1 - '@sentry/node': 8.37.1 - '@sentry/opentelemetry': 8.37.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0) - '@sentry/react': 8.37.1(react@19.1.0) - '@sentry/types': 8.37.1 - '@sentry/utils': 8.37.1 - '@sentry/vercel-edge': 8.37.1 - '@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29)) + '@opentelemetry/semantic-conventions': 1.34.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@3.29.5) + '@sentry-internal/browser-utils': 8.55.0 + '@sentry/core': 8.55.0 + '@sentry/node': 8.55.0 + '@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/react': 8.55.0(react@19.1.0) + '@sentry/vercel-edge': 8.55.0 + '@sentry/webpack-plugin': 2.22.7(webpack@5.99.9(@swc/core@1.11.29)) chalk: 3.0.0 next: 15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) resolve: 1.22.8 @@ -16297,23 +16413,20 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@8.37.1(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.96.1(@swc/core@1.11.29))': + '@sentry/nextjs@8.55.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4))(react@19.1.0)(webpack@5.99.9(@swc/core@1.11.29))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation-http': 0.53.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.27.0 - '@rollup/plugin-commonjs': 26.0.1(rollup@3.29.5) - '@sentry-internal/browser-utils': 8.37.1 - '@sentry/core': 8.37.1 - '@sentry/node': 8.37.1 - '@sentry/opentelemetry': 8.37.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.27.0) - '@sentry/react': 8.37.1(react@19.1.0) - '@sentry/types': 8.37.1 - '@sentry/utils': 8.37.1 - '@sentry/vercel-edge': 8.37.1 - '@sentry/webpack-plugin': 2.22.6(webpack@5.96.1(@swc/core@1.11.29)) + '@opentelemetry/semantic-conventions': 1.34.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@3.29.5) + '@sentry-internal/browser-utils': 8.55.0 + '@sentry/core': 8.55.0 + '@sentry/node': 8.55.0 + '@sentry/opentelemetry': 8.55.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.34.0) + '@sentry/react': 8.55.0(react@19.1.0) + '@sentry/vercel-edge': 8.55.0 + '@sentry/webpack-plugin': 2.22.7(webpack@5.99.9(@swc/core@1.11.29)) chalk: 3.0.0 - next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) resolve: 1.22.8 rollup: 3.29.5 stacktrace-parser: 0.1.11 @@ -16415,12 +16528,12 @@ snapshots: '@opentelemetry/api': 1.9.0 '@sentry/core': 8.55.0 - '@sentry/webpack-plugin@2.22.6(webpack@5.96.1(@swc/core@1.11.29))': + '@sentry/webpack-plugin@2.22.7(webpack@5.99.9(@swc/core@1.11.29))': dependencies: '@sentry/bundler-plugin-core': 2.22.7 unplugin: 1.0.1 uuid: 9.0.0 - webpack: 5.96.1(@swc/core@1.11.29) + webpack: 5.99.9(@swc/core@1.11.29) transitivePeerDependencies: - encoding - supports-color @@ -17004,6 +17117,16 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.1)(@types/react@19.0.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.27.6 + '@testing-library/dom': 10.4.0 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.0.1 + '@types/react-dom': 19.0.1 + '@testing-library/react@16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.1.2(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 @@ -17551,7 +17674,7 @@ snapshots: utf-8-validate: 6.0.5 ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0))': + '@vitejs/plugin-react@4.5.2(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.7 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.7) @@ -17559,11 +17682,11 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0))': + '@vitejs/plugin-react@4.5.2(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@babel/core': 7.27.7 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.7) @@ -17571,7 +17694,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -17583,21 +17706,21 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0))': + '@vitest/mocker@3.2.3(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) - '@vitest/mocker@3.2.3(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0))': + '@vitest/mocker@3.2.3(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) '@vitest/pretty-format@3.2.3': dependencies: @@ -18165,10 +18288,7 @@ snapshots: bare-events@2.5.4: optional: true - bare-events@2.5.4: - optional: true - - bare-fs@4.1.6: + bare-fs@4.1.5: dependencies: bare-events: 2.5.4 bare-path: 3.0.0 @@ -18738,9 +18858,6 @@ snapshots: detect-libc@2.0.2: {} - detect-libc@2.0.3: - optional: true - detect-libc@2.0.4: {} detect-newline@3.1.0: {} @@ -18796,22 +18913,20 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3): + drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.10.2)(@vercel/postgres@0.9.0)(pg@8.16.3): optionalDependencies: '@libsql/client': 0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@opentelemetry/api': 1.9.0 '@types/pg': 8.10.2 '@vercel/postgres': 0.9.0 - gel: 2.0.1 pg: 8.16.3 - drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(gel@2.0.1)(pg@8.16.3): + drizzle-orm@0.44.2(@libsql/client@0.14.0(bufferutil@4.0.9))(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@vercel/postgres@0.9.0)(pg@8.16.3): optionalDependencies: '@libsql/client': 0.14.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) '@opentelemetry/api': 1.9.0 '@types/pg': 8.11.6 '@vercel/postgres': 0.9.0 - gel: 2.0.1 pg: 8.16.3 dunder-proto@1.0.1: @@ -18878,9 +18993,6 @@ snapshots: entities@6.0.1: {} - env-paths@3.0.0: - optional: true - environment@1.1.0: {} error-ex@1.3.2: @@ -19849,19 +19961,7 @@ snapshots: geist@1.4.2(next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4)): dependencies: - next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) - - gel@2.0.1: - dependencies: - '@petamoriken/float16': 3.9.2 - debug: 4.4.1 - env-paths: 3.0.0 - semver: 7.7.2 - shell-quote: 1.8.3 - which: 4.0.0 - transitivePeerDependencies: - - supports-color - optional: true + next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) gensync@1.0.0-beta.2: {} @@ -20427,9 +20527,6 @@ snapshots: isexe@2.0.0: {} - isexe@3.1.1: - optional: true - isomorphic-unfetch@3.1.0: dependencies: node-fetch: 2.7.0 @@ -21617,9 +21714,9 @@ snapshots: '@next/env': 13.5.11 fast-glob: 3.3.3 minimist: 1.2.8 - next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) - next@15.3.2(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): + next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): dependencies: '@next/env': 15.3.2 '@swc/counter': 0.1.3 @@ -21641,15 +21738,16 @@ snapshots: '@next/swc-win32-x64-msvc': 15.3.2 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.50.0 + babel-plugin-react-compiler: 19.1.0-rc.2 sass: 1.77.4 sharp: 0.34.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - next@15.3.2(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): + next@15.3.3(@babel/core@7.27.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): dependencies: - '@next/env': 15.3.2 + '@next/env': 15.3.3 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -21657,16 +21755,16 @@ snapshots: postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(@babel/core@7.27.7)(babel-plugin-macros@3.1.0)(react@19.1.0) + styled-jsx: 5.1.6(@babel/core@7.27.3)(babel-plugin-macros@3.1.0)(react@19.1.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.3.2 - '@next/swc-darwin-x64': 15.3.2 - '@next/swc-linux-arm64-gnu': 15.3.2 - '@next/swc-linux-arm64-musl': 15.3.2 - '@next/swc-linux-x64-gnu': 15.3.2 - '@next/swc-linux-x64-musl': 15.3.2 - '@next/swc-win32-arm64-msvc': 15.3.2 - '@next/swc-win32-x64-msvc': 15.3.2 + '@next/swc-darwin-arm64': 15.3.3 + '@next/swc-darwin-x64': 15.3.3 + '@next/swc-linux-arm64-gnu': 15.3.3 + '@next/swc-linux-arm64-musl': 15.3.3 + '@next/swc-linux-x64-gnu': 15.3.3 + '@next/swc-linux-x64-musl': 15.3.3 + '@next/swc-win32-arm64-msvc': 15.3.3 + '@next/swc-win32-x64-msvc': 15.3.3 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.50.0 babel-plugin-react-compiler: 19.1.0-rc.2 @@ -21676,7 +21774,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): + next@15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4): dependencies: '@next/env': 15.3.3 '@swc/counter': 0.1.3 @@ -22009,8 +22107,6 @@ snapshots: pg-protocol@1.10.3: {} - pg-protocol@1.10.2: {} - pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -22819,8 +22915,6 @@ snapshots: semver@7.7.2: {} - semver@7.7.2: {} - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -22854,41 +22948,14 @@ snapshots: color: 4.2.3 detect-libc: 2.0.4 node-addon-api: 6.1.0 - prebuild-install: 7.1.2 + prebuild-install: 7.1.3 semver: 7.7.2 simple-get: 4.0.1 - tar-fs: 3.1.0 + tar-fs: 3.0.10 tunnel-agent: 0.6.0 transitivePeerDependencies: - bare-buffer - sharp@0.34.2: - dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - optional: true - sharp@0.34.2: dependencies: color: 4.2.3 @@ -22923,9 +22990,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: - optional: true - shelljs@0.8.5: dependencies: glob: 7.2.3 @@ -23168,14 +23232,6 @@ snapshots: optionalDependencies: bare-events: 2.5.4 - streamx@2.22.1: - dependencies: - fast-fifo: 1.3.2 - text-decoder: 1.2.1 - optionalDependencies: - bare-events: 2.5.4 - optional: true - string-argv@0.3.2: {} string-length@4.0.2: @@ -23416,6 +23472,10 @@ snapshots: tailwindcss@4.0.12: {} + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + tar-fs@2.1.3: dependencies: chownr: 1.1.4 @@ -23423,12 +23483,12 @@ snapshots: pump: 3.0.3 tar-stream: 2.2.0 - tar-fs@3.1.0: + tar-fs@3.0.10: dependencies: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.1.6 + bare-fs: 4.1.5 bare-path: 3.0.0 transitivePeerDependencies: - bare-buffer @@ -23491,14 +23551,14 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.10(@swc/core@1.11.29)(webpack@5.96.1(@swc/core@1.11.29)): + terser-webpack-plugin@5.3.14(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.36.0 - webpack: 5.96.1(@swc/core@1.11.29) + terser: 5.43.1 + webpack: 5.99.9(@swc/core@1.11.29) optionalDependencies: '@swc/core': 1.11.29 @@ -23851,7 +23911,7 @@ snapshots: '@uploadthing/shared': 7.1.1 effect: 3.10.3 optionalDependencies: - next: 15.3.3(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) + next: 15.3.3(@babel/core@7.27.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.50.0)(babel-plugin-macros@3.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.77.4) tailwindcss: 4.1.11 uri-js@4.4.1: @@ -23935,13 +23995,13 @@ snapshots: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vite-node@3.2.3(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vite-node@3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -23956,13 +24016,13 @@ snapshots: - tsx - yaml - vite-node@3.2.3(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vite-node@3.2.3(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -23977,29 +24037,29 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.7.3) optionalDependencies: - vite: 6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.7.3) optionalDependencies: - vite: 6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) @@ -24013,12 +24073,12 @@ snapshots: jiti: 2.4.2 lightningcss: 1.30.1 sass: 1.77.4 - sass-embedded: 1.80.6 - terser: 5.36.0 + sass-embedded: 1.89.2 + terser: 5.43.1 tsx: 4.20.3 - yaml: 2.6.0 + yaml: 2.8.0 - vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: esbuild: 0.25.5 fdir: 6.4.6(picomatch@4.0.2) @@ -24032,17 +24092,17 @@ snapshots: jiti: 2.4.2 lightningcss: 1.30.1 sass: 1.77.4 - sass-embedded: 1.80.6 - terser: 5.36.0 + sass-embedded: 1.89.2 + terser: 5.43.1 tsx: 4.20.3 - yaml: 2.6.0 + yaml: 2.8.0 - vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@1.21.6)(jsdom@26.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.15.30)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) - '@vitest/pretty-format': 3.2.3 + '@vitest/mocker': 3.2.3(vite@7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 '@vitest/spy': 3.2.3 @@ -24059,8 +24119,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) - vite-node: 3.2.3(@types/node@22.15.30)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@22.15.30)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24080,12 +24140,12 @@ snapshots: - tsx - yaml - vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@1.21.6)(jsdom@26.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0): + vitest@3.2.3(@types/debug@4.1.12)(@types/node@22.5.4)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(vite@6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0)) - '@vitest/pretty-format': 3.2.3 + '@vitest/mocker': 3.2.3(vite@7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 '@vitest/spy': 3.2.3 @@ -24102,8 +24162,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) - vite-node: 3.2.3(@types/node@22.5.4)(jiti@1.21.6)(sass-embedded@1.80.6)(sass@1.77.4)(terser@5.36.0)(tsx@4.20.3)(yaml@2.6.0) + vite: 7.0.0(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.3(@types/node@22.5.4)(jiti@2.4.2)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.77.4)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24175,7 +24235,7 @@ snapshots: webpack-virtual-modules@0.5.0: {} - webpack@5.96.1(@swc/core@1.11.29): + webpack@5.99.9(@swc/core@1.11.29): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -24196,11 +24256,11 @@ snapshots: loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.11.29)(webpack@5.96.1(@swc/core@1.11.29)) - watchpack: 2.4.2 - webpack-sources: 3.2.3 + schema-utils: 4.3.2 + tapable: 2.2.2 + terser-webpack-plugin: 5.3.14(@swc/core@1.11.29)(webpack@5.99.9(@swc/core@1.11.29)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 transitivePeerDependencies: - '@swc/core' - esbuild @@ -24271,11 +24331,6 @@ snapshots: dependencies: isexe: 2.0.0 - which@4.0.0: - dependencies: - isexe: 3.1.1 - optional: true - why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 diff --git a/templates/ecommerce/src/components/Cart/CartModal.tsx b/templates/ecommerce/src/components/Cart/CartModal.tsx index 321e1cfa3ed..20dff8635d0 100644 --- a/templates/ecommerce/src/components/Cart/CartModal.tsx +++ b/templates/ecommerce/src/components/Cart/CartModal.tsx @@ -1,6 +1,7 @@ 'use client' import { Price } from '@/components/Price' +import { XIcon } from '@payloadcms/ui' import { Sheet, SheetContent, @@ -15,18 +16,40 @@ import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' import { DeleteItemButton } from './DeleteItemButton' import { EditItemQuantityButton } from './EditItemQuantityButton' import { OpenCartButton } from './OpenCart' import { Button } from '@/components/ui/button' import { Product } from '@/payload-types' +import { Input } from '../ui/input' +import { FormError } from '../forms/FormError' + +interface CouponFormData { + couponCode: string +} export function CartModal() { - const { cart } = useCart() + const { cart, applyCoupon, removeItem, removeCoupon } = useCart() const [isOpen, setIsOpen] = useState(false) + const discountLines = useMemo(() => { + const lineDiscount = cart?.items?.flatMap((item) => { + return item.discount?.discountLines || [] + }) + + return [...(lineDiscount ?? []), ...(cart?.discount?.discountLines ?? [])] + }, []) + const pathname = usePathname() + const { + formState: { errors, isLoading }, + handleSubmit, + setError, + reset, + register, + } = useForm() // useEffect(() => { // // Open cart modal when quantity changes. @@ -46,6 +69,25 @@ export function CartModal() { setIsOpen(false) }, [pathname]) + const onSubmit = async ({ couponCode }: { couponCode: string }) => { + if (!cart) { + console.error('Cart is not available') + return + } + + try { + await applyCoupon(couponCode, cart.id) + reset() + } catch (error) { + console.error('Error applying coupon:', error) + console.log({ message: error.message }) + setError('couponCode', { + message: error.message || 'An error occurred while applying the coupon', + }) + return + } + } + const totalQuantity = useMemo(() => { if (!cart || !cart.items || !cart.items.length) return undefined return cart.items.reduce((quantity, item) => (item.quantity || 0) + quantity, 0) @@ -158,11 +200,28 @@ export function CartModal() { })} + {!!cart && ( +
+
+ + +
+ {!!errors.couponCode && ( + + )} +
+ )}
{typeof cart?.subtotal === 'number' && (
-

Total

+

Subtotal

)} + {!!cart?.discount?.totalAmount && ( +
+
+

Discount

+ +
+ +
+

Total

+ +
+ +

Applied coupons

+ +
+ {discountLines?.map((item) => { + return ( +
+

{item.coupon.title}

+
+ + + +
+
+ ) + })} +
+
+ )} +