diff --git a/.bugster/config.yaml b/.bugster/config.yaml new file mode 100644 index 0000000..3786588 --- /dev/null +++ b/.bugster/config.yaml @@ -0,0 +1,35 @@ +# Bugster Configuration File +# This file contains your project configuration and test execution preferences. + +# Project Information +project_name: bugster-nextjs-example +project_id: CNBx3CugMe1wRrG2ESHk +base_url: "http://localhost:3000" + +# Project Authentication +credentials: + - id: admin + username: admin + password: admin + +# Vercel Configuration +# You can create the Vercel Protection Bypass Secret for Automation in this link: +# https://vercel.com/d?to=/[team]/[project]/settings/deployment-protection&title=Deployment+Protection+settings +# x-vercel-protection-bypass: your-bypass-secret + + +# Test Execution Preferences +# Uncomment and modify the options below to customize test execution behavior. +# CLI options will override these settings when specified. +# preferences: +# tests: +# always_run: +# - .bugster/tests/test1.yaml +# - .bugster/tests/test2.yaml +# limit: 5 # Maximum number of tests to run +# headless: false # Run tests in headless mode +# silent: false # Run tests in silent mode +# verbose: false # Enable verbose output +# only_affected: false # Only run tests for affected files +# parallel: 5 # Maximum number of concurrent tests +# output: bugster_output.json # Save test results to JSON file diff --git a/README.md b/README.md index 0566393..fb3ef3e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 🐛 Bugster - Automated Testing for Next.js **Generate and run comprehensive tests for your Next.js applications with AI-powered automation.** - + This is a demo shirt shop built with Next.js to showcase how Bugster can automatically generate and run tests for your web applications. Follow this step-by-step guide to try it from scratch! ## 🚀 Phase 1: Try Bugster Locally diff --git a/app/cart/order-summary.tsx b/app/cart/order-summary.tsx index edafe2b..9caca03 100644 --- a/app/cart/order-summary.tsx +++ b/app/cart/order-summary.tsx @@ -1,21 +1,80 @@ import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'; import { ProceedToCheckout } from './proceed-to-checkout'; +import { PromoCodeSection } from '@/components/shopping-cart/promo-code-section'; +import { ShippingOptionsSection } from '@/components/shopping-cart/shipping-options-section'; export function OrderSummary({ showSummerBanner, freeDelivery, + subtotal, + itemCount, }: { showSummerBanner: boolean; freeDelivery: boolean; + subtotal: number; + itemCount: number; }) { - // Using default value: proceedToCheckoutColor = 'blue' - const proceedToCheckoutColor = 'blue'; + // Dynamic color based on cart value + const proceedToCheckoutColor = subtotal >= 100 ? 'green' : 'blue'; + + // Calculate estimated delivery date + const deliveryDate = new Date(); + deliveryDate.setDate(deliveryDate.getDate() + (freeDelivery ? 3 : 5)); + const formattedDeliveryDate = deliveryDate.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + }); return ( - } - /> +
+
+

Order summary

+ {itemCount > 0 && ( + + {itemCount} {itemCount === 1 ? 'item' : 'items'} + + )} +
+ + {/* Estimated Delivery */} + {itemCount > 0 && ( +
+
+ + + +
+

+ Estimated delivery: {formattedDeliveryDate} +

+
+
+
+ )} + + {/* Promo Code Section */} +
+ +
+ + {/* Shipping Options */} +
+ +
+ + {/* Proceed to Checkout Button */} +
+ +
+ + {/* Order Summary Details */} + +
); } \ No newline at end of file diff --git a/app/cart/page.tsx b/app/cart/page.tsx index 9886ef2..85048de 100644 --- a/app/cart/page.tsx +++ b/app/cart/page.tsx @@ -1,21 +1,111 @@ import { OrderSummary } from '@/app/cart/order-summary'; import { Main } from '@/components/main'; import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'; +import { getCart } from '@/lib/actions'; +import { Suspense } from 'react'; -export default function CartPage() { - // Using default values: showSummerBanner = false, freeDelivery = false - const showSummerBanner = false; - const freeDelivery = false; +async function CartPageContent() { + const { items } = await getCart(); + + // Calculate total to determine free delivery eligibility + const subtotal = items.reduce((total, item) => { + // Handle different product types with their respective prices + const productPrices: Record = { + 'shirt': 20.00, + 'sticker-circle': 12.99, + 'sticker-star': 15.99, + 'sticker-emoji': 10.99, + 'sticker-nature': 14.99, + 'sticker-geometric': 13.99, + 'sticker-holographic': 18.99, + }; + + const price = productPrices[item.id] || 20.00; // Default to shirt price + return total + (price * item.quantity); + }, 0); + + // Dynamic logic for banners and free delivery + const showSummerBanner = new Date().getMonth() >= 5 && new Date().getMonth() <= 7; // June-August + const freeDelivery = subtotal >= 50; // Free delivery for orders over $50 + const itemCount = items.reduce((count, item) => count + item.quantity, 0); + + return ( +
+
+
+ {/* Cart Header */} +
+

+ Shopping Cart +

+ {itemCount > 0 && ( +

+ {itemCount} {itemCount === 1 ? 'item' : 'items'} in your cart +

+ )} +
+ + {/* Free Delivery Banner */} + {!freeDelivery && subtotal > 0 && ( +
+
+
+ + + +
+
+

+ Add ${(50 - subtotal).toFixed(2)} more to qualify for free delivery! +

+
+
+
+ )} + + {/* Main Cart Grid */} +
+ + +
+
+
+
+ ); +} +function CartPageFallback() { return (
-
- - +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
); +} + +export default function CartPage() { + return ( + }> + + + ); } \ No newline at end of file diff --git a/app/stickers/page.tsx b/app/stickers/page.tsx new file mode 100644 index 0000000..f263286 --- /dev/null +++ b/app/stickers/page.tsx @@ -0,0 +1,78 @@ +import { Main } from '@/components/main'; +import { StickerProductCard } from '@/components/stickers/sticker-product-card'; + +const stickerProducts = [ + { + id: 'sticker-circle', + name: 'Circle Sticker Pack', + price: 12.99, + image: '/images/stickers/circle-sticker.png', + description: 'A pack of colorful circular stickers perfect for decoration', + colors: ['Red', 'Blue', 'Green', 'Yellow'] + }, + { + id: 'sticker-star', + name: 'Star Sticker Collection', + price: 15.99, + image: '/images/stickers/star-sticker.png', + description: 'Shiny star stickers that glow in the dark', + colors: ['Gold', 'Silver', 'Blue', 'Purple'] + }, + { + id: 'sticker-emoji', + name: 'Emoji Sticker Set', + price: 10.99, + image: '/images/stickers/emoji-sticker.png', + description: 'Express yourself with these fun emoji stickers', + colors: ['Multi-color'] + }, + { + id: 'sticker-nature', + name: 'Nature Sticker Pack', + price: 14.99, + image: '/images/stickers/nature-sticker.png', + description: 'Beautiful nature-themed stickers with plants and animals', + colors: ['Green', 'Brown', 'Multi-color'] + }, + { + id: 'sticker-geometric', + name: 'Geometric Sticker Set', + price: 13.99, + image: '/images/stickers/geometric-sticker.png', + description: 'Modern geometric patterns for a contemporary look', + colors: ['Black', 'White', 'Gold', 'Silver'] + }, + { + id: 'sticker-holographic', + name: 'Holographic Sticker Pack', + price: 18.99, + image: '/images/stickers/holographic-sticker.png', + description: 'Shimmering holographic stickers that change colors', + colors: ['Rainbow', 'Silver', 'Gold'] + } +]; + +export default function StickersPage() { + return ( +
+
+
+
+

+ Stickers Collection +

+

+ Discover our amazing collection of high-quality stickers +

+
+ +
+ {stickerProducts.map((product) => ( + + ))} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/navigation.tsx b/components/navigation.tsx index 2566cfc..cde5dc5 100644 --- a/components/navigation.tsx +++ b/components/navigation.tsx @@ -56,7 +56,13 @@ export function Navigation() { key={page} className="flex items-center text-sm font-medium text-gray-700 hover:text-gray-800" > - {page === 'Home' ? Home : page} + {page === 'Home' ? ( + Home + ) : page === 'Stickers' ? ( + Stickers + ) : ( + page + )} ); })} diff --git a/components/shopping-cart/order-summary-section.tsx b/components/shopping-cart/order-summary-section.tsx index 77147cd..34facc0 100644 --- a/components/shopping-cart/order-summary-section.tsx +++ b/components/shopping-cart/order-summary-section.tsx @@ -31,20 +31,23 @@ function OrderSummaryFallback({ ); } -async function OrderSummaryContent({ +function OrderSummaryContent({ showSummerBanner, freeDelivery, + subtotal, + itemCount, }: { showSummerBanner: boolean; freeDelivery: boolean; + subtotal: number; + itemCount: number; }) { - const { items } = await getCart(); - const subtotal = items.length * 20; // Assuming $20 per shirt + // Calculate discounts and costs const summerDiscount = showSummerBanner ? subtotal * (20 / 100) * -1 : 0; // 20% discount - const qualifyingForFreeDelivery = freeDelivery && subtotal > 30; - const shippingCost = 5; - const shipping = qualifyingForFreeDelivery ? 0 : shippingCost; - const total = subtotal + shipping + summerDiscount; + const shippingCost = 5.99; + const shipping = freeDelivery ? 0 : shippingCost; + const tax = subtotal * 0.08; // 8% tax + const total = subtotal + shipping + summerDiscount + tax; return (
@@ -64,10 +67,10 @@ async function OrderSummaryContent({ ) : null}

Shipping estimate

- {qualifyingForFreeDelivery ? ( + {freeDelivery ? (

- {shipping.toFixed(2)} USD + {shippingCost.toFixed(2)} USD {' '} FREE

@@ -77,6 +80,12 @@ async function OrderSummaryContent({

)}
+
+

Tax

+

+ {tax.toFixed(2)} USD +

+

Order total

@@ -90,27 +99,23 @@ async function OrderSummaryContent({ export function OrderSummarySection({ showSummerBanner, freeDelivery, - proceedToCheckout, + subtotal, + itemCount, }: { showSummerBanner: boolean; freeDelivery: boolean; - proceedToCheckout: React.ReactNode; + subtotal: number; + itemCount: number; }) { return ( -

-

Order summary

- -
{proceedToCheckout}
- - } - > - - - +
+ +

or{' '} @@ -122,6 +127,6 @@ export function OrderSummarySection({

-
+
); } diff --git a/components/shopping-cart/promo-code-section.tsx b/components/shopping-cart/promo-code-section.tsx new file mode 100644 index 0000000..b384fc4 --- /dev/null +++ b/components/shopping-cart/promo-code-section.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { useState } from 'react'; +import { toast } from 'sonner'; + +interface PromoCodeSectionProps { + subtotal: number; +} + +const validPromoCodes = { + 'SAVE10': { discount: 0.10, description: '10% off' }, + 'WELCOME20': { discount: 0.20, description: '20% off for new customers' }, + 'FREESHIP': { discount: 0, description: 'Free shipping', freeShipping: true }, + 'SUMMER25': { discount: 0.25, description: '25% off summer sale' }, +}; + +export function PromoCodeSection({ subtotal }: PromoCodeSectionProps) { + const [promoCode, setPromoCode] = useState(''); + const [appliedCode, setAppliedCode] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleApplyPromoCode = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!promoCode.trim()) { + toast.error('Please enter a promo code'); + return; + } + + setIsLoading(true); + + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + const code = promoCode.trim().toUpperCase(); + + if (validPromoCodes[code as keyof typeof validPromoCodes]) { + setAppliedCode(code); + setPromoCode(''); + toast.success(`Promo code applied: ${validPromoCodes[code as keyof typeof validPromoCodes].description}`); + } else { + toast.error('Invalid promo code'); + } + + setIsLoading(false); + }; + + const handleRemovePromoCode = () => { + setAppliedCode(null); + toast.success('Promo code removed'); + }; + + return ( +
+

Promo Code

+ + {appliedCode ? ( +
+
+ + + +
+

+ {appliedCode}: {validPromoCodes[appliedCode as keyof typeof validPromoCodes].description} +

+
+
+ +
+ ) : ( +
+ setPromoCode(e.target.value)} + placeholder="Enter promo code" + className="flex-1 min-w-0 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> + +
+ )} + + {/* Promo suggestions */} + {!appliedCode && ( +
+

+ Try: SAVE10, WELCOME20, FREESHIP, SUMMER25 +

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/components/shopping-cart/shipping-options-section.tsx b/components/shopping-cart/shipping-options-section.tsx new file mode 100644 index 0000000..06495c0 --- /dev/null +++ b/components/shopping-cart/shipping-options-section.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useState } from 'react'; +import { Radio, RadioGroup } from '@headlessui/react'; +import clsx from 'clsx'; + +interface ShippingOption { + id: string; + name: string; + price: number; + description: string; + estimatedDays: number; + icon: React.ReactNode; +} + +interface ShippingOptionsSectionProps { + freeDelivery: boolean; +} + +const shippingOptions: ShippingOption[] = [ + { + id: 'standard', + name: 'Standard Shipping', + price: 5.99, + description: 'Delivered in 5-7 business days', + estimatedDays: 7, + icon: ( + + + + ), + }, + { + id: 'express', + name: 'Express Shipping', + price: 12.99, + description: 'Delivered in 2-3 business days', + estimatedDays: 3, + icon: ( + + + + ), + }, + { + id: 'overnight', + name: 'Overnight Shipping', + price: 24.99, + description: 'Delivered next business day', + estimatedDays: 1, + icon: ( + + + + ), + }, +]; + +export function ShippingOptionsSection({ freeDelivery }: ShippingOptionsSectionProps) { + const [selectedOption, setSelectedOption] = useState(shippingOptions[0]); + + const getShippingPrice = (option: ShippingOption) => { + if (freeDelivery && option.id === 'standard') { + return 0; + } + return option.price; + }; + + const getEstimatedDelivery = (option: ShippingOption) => { + const deliveryDate = new Date(); + deliveryDate.setDate(deliveryDate.getDate() + option.estimatedDays); + return deliveryDate.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + }); + }; + + return ( +
+

Shipping Options

+ + +
+ {shippingOptions.map((option) => ( + + {({ checked }) => ( +
+
+
+
+ {checked && ( +
+ )} +
+
+
+
+ {option.icon} +
+
+

+ {option.name} +

+

+ {option.description} +

+
+
+
+
+

+ {getShippingPrice(option) === 0 ? ( + Free + ) : ( + `$${getShippingPrice(option).toFixed(2)}` + )} +

+

+ by {getEstimatedDelivery(option)} +

+
+
+ )} + + ))} +
+ + + {freeDelivery && ( +
+

+ 🎉 You qualify for free standard shipping! +

+
+ )} +
+ ); +} \ No newline at end of file diff --git a/components/stickers/sticker-product-card.tsx b/components/stickers/sticker-product-card.tsx new file mode 100644 index 0000000..57fb6b2 --- /dev/null +++ b/components/stickers/sticker-product-card.tsx @@ -0,0 +1,112 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { track } from '@vercel/analytics'; +import { addToCart } from '@/lib/actions'; +import { toast } from 'sonner'; +import Image from 'next/image'; + +interface StickerProduct { + id: string; + name: string; + price: number; + image: string; + description: string; + colors: string[]; +} + +interface StickerProductCardProps { + product: StickerProduct; +} + +export function StickerProductCard({ product }: StickerProductCardProps) { + const [isLoading, setIsLoading] = useState(false); + const [selectedColor, setSelectedColor] = useState(product.colors[0]); + const router = useRouter(); + + const handleAddToCart = async () => { + setIsLoading(true); + track('add_to_cart:clicked', { product_id: product.id }); + + try { + await addToCart({ + id: product.id, + color: selectedColor, + size: 'One Size', + quantity: 1 + }); + + toast.success('Added to cart!', { + description: `${product.name} has been added to your cart.`, + }); + + // Optionally redirect to cart + // router.push('/cart'); + } catch (error) { + toast.error('Failed to add to cart', { + description: 'Please try again later.', + }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ {/* Placeholder colored rectangle since we don't have actual sticker images */} +
+
+
🎨
+
{product.name}
+
+
+
+ +
+
+

{product.name}

+

${product.price}

+
+ +

{product.description}

+ + {/* Color Selection */} +
+ +
+ {product.colors.map((color) => ( + + ))} +
+
+ + {/* Add to Cart Button */} + +
+
+ ); +} \ No newline at end of file