-
Notifications
You must be signed in to change notification settings - Fork 3
Chore update categories #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Chore update categories #57
Conversation
feat: created the navbar and its components
fix: added flex to the div of the navbar
feat: created footer and its components
Feature: Build About Us Page and Remodel Typography Component
Feat newsletter
Feat/contactus
Built Shop Page
…w their orders are being sent
…hing Feat added redis caching
chore: made some ui fixes
chore: made a minor fix
chore: made a fix
…ck of revenue and customers
chore: improving seo
Merge branch 'main' of https://github.com/himspired1/himspired into feat-implement-flexible-logistics-pricing
…ble-logistics-pricing Feat implement flexible logistics pricing
- Changed shop navigation categories from THRIFT/LUXURY/SENATORS to POLOS/T-SHIRTS/JEANS/SHIRTS - This focuses the shop on thrift products with product-type-based filtering 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Replaced brand-based product arrays with product-type arrays - Added polosProducts, tshirtsProducts, jeansProducts, shirtsProducts - Updated product data with appropriate thrift-focused items and pricing - Maintained legacy exports for backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Changed SECTIONS array from brand categories to product types - Updated imports to use new product arrays - Modified section IDs and labels to match new categorization - Carousel now displays POLOS/T-SHIRTS/JEANS/SHIRTS instead of THRIFTS/LUXURY/VINTAGE/MODERN 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
👷 Deploy request for aquamarine-meringue-a3a1e5 pending review.Visit the deploys page to approve it
|
WalkthroughThis update introduces a comprehensive suite of new features, configuration files, documentation, and major backend and frontend components for a fashion e-commerce platform. It adds detailed admin dashboards, analytics, user and order management, newsletter and contact APIs, delivery fee management, testing infrastructure, and extensive documentation. Numerous React components and API endpoints are implemented, along with supporting scripts, configuration, and utility enhancements. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant HomePage as HomePage Components
participant API
participant AdminPanel
participant Database
participant Cache
participant EmailService
User->>HomePage: Visit homepage
HomePage->>API: Fetch products, newsletter, etc.
API->>Database: Query product data / stock
Database-->>API: Return product data
API-->>HomePage: Return products, stock, etc.
User->>HomePage: Submit newsletter form
HomePage->>API: POST /api/newsletter
API->>Cache: Check rate limits
API->>EmailService: Send welcome email
API-->>HomePage: Success or error response
User->>AdminPanel: Login as admin
AdminPanel->>API: POST /api/admin/auth
API->>Database: Verify credentials
Database-->>API: Auth result
API-->>AdminPanel: Set auth cookie, respond
AdminPanel->>API: GET /api/admin/analytics/*
API->>Cache: Check analytics cache
API->>Database: Aggregate analytics data
Database-->>API: Analytics results
API->>Cache: Store analytics cache
API-->>AdminPanel: Return analytics data
AdminPanel->>API: PATCH /api/delivery-fees/[state]
API->>Database: Update delivery fee
Database-->>API: Update result
API-->>AdminPanel: Success or error response
User->>HomePage: Add product to cart, checkout
HomePage->>API: POST /api/orders
API->>Database: Create order, update stock
API->>EmailService: Send order confirmation
API-->>HomePage: Order confirmation
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 55
🧹 Nitpick comments (41)
src/app/about/about-page.tsx (1)
11-11: Consider extracting magic numbers to constants.The hardcoded margin values
lg:mt-[13em] mt-[7em]andpb-[120.93px]could be extracted to named constants for better maintainability.const LAYOUT_MARGINS = { topDesktop: 'lg:mt-[13em]', topMobile: 'mt-[7em]', bottom: 'pb-[120.93px]' } as const; // Then use: <div className={`w-full min-h-screen ${LAYOUT_MARGINS.topDesktop} ${LAYOUT_MARGINS.topMobile} ${LAYOUT_MARGINS.bottom}`}>src/app/api/newsletter/unsubscribe/route.ts (1)
4-52: Consider adding rate limiting for security.While the implementation is solid with proper validation and error handling, consider adding rate limiting similar to the subscription endpoint to prevent abuse of the unsubscribe functionality.
The subscription route includes IP-based rate limiting. Consider implementing similar protection:
// Add rate limiting logic similar to subscription route const ip = getClientIp(req); // ... rate limiting implementationsrc/app/api/admin/verify/route.ts (1)
32-32: Consider making the role configurable.The hard-coded role "admin" may limit flexibility if you need different admin role types in the future.
Consider making the role configurable:
- role: "admin", + role: user.role || "admin",src/app/api/admin/force-cleanup/route.ts (1)
6-12: Consider more robust IP extractionThe IP extraction could handle additional headers for better compatibility with various proxy configurations.
Consider adding support for additional headers:
function getClientIp(req: NextRequest): string { return ( req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || + req.headers.get("cf-connecting-ip") || // Cloudflare + req.headers.get("x-client-ip") || "unknown" ); }src/app/_Home/components/ImageStack.tsx (1)
13-44: Extract repeated CSS classesConsider extracting common CSS classes to reduce repetition and improve maintainability.
+const imageBaseClasses = "w-full object-cover"; +const constrainedImageClasses = `${imageBaseClasses} max-h-[421px] md:max-h-[1024px] md:h-full object-top`; +const fullImageClasses = `${imageBaseClasses} h-full`; // Then use in components: - className="w-full h-full object-cover" + className={fullImageClasses}src/app/_Home/components/Products.tsx (3)
142-150: Simplify loading state conditionThe loading check is redundant. If
loadingis true, show the skeleton regardless of the array length.- if (loading && clothesByCategory.length === 0) { + if (loading) {
57-57: Remove commented codeRemove unused commented code for better code cleanliness.
- // const { clothes, loading, error, refetch } = useClothes();
60-63: Remove empty useEffectThis empty useEffect with just a comment serves no purpose.
- // Calculate items to show based on screen width - useEffect(() => { - // Removed itemsToShow logic; not needed with ProductCard - }, []);src/app/admin/users/page.tsx (1)
6-11: Consider using shared type definitionThe
AdminUserinterface duplicates the structure fromsrc/models/admin.ts. Consider importing and using a shared type to maintain consistency.You could create a shared client-side type that extends or picks from the backend model, excluding server-only fields like
passwordHash.src/app/admin/login/page.tsx (1)
33-36: Improve error handling in authentication checkThe catch block only logs to console. Consider handling network errors more gracefully.
} catch { // Not authenticated, stay on login page - console.log("Not authenticated, staying on login page"); + // Silently handle - user is not authenticated }If network errors need to be distinguished from authentication failures, consider checking the error type.
jest.config.js (1)
35-35: Consider using a more maintainable approach for transformIgnorePatternsThe hardcoded list of packages in
transformIgnorePatternsmay become difficult to maintain as dependencies evolve.Consider extracting this to a constant or configuration:
+const esmPackages = ["jose", "@jose", "@panva", "@peculiar"]; + const customJestConfig = { // ... other config - transformIgnorePatterns: ["node_modules/(?!(jose|@jose|@panva|@peculiar)/)"], + transformIgnorePatterns: [`node_modules/(?!(${esmPackages.join("|")})/)`],src/app/admin/orders/[orderId]/page.tsx (1)
4-6: Move export statement to follow React conventionsThe
export const dynamic = "force-dynamic"is placed between the main imports and should be moved to maintain conventional import organization."use client"; import { useState, useEffect, useCallback } from "react"; - -// Force dynamic rendering to avoid build issues -export const dynamic = "force-dynamic"; import { useParams, useRouter } from "next/navigation"; import { motion } from "framer-motion"; + +// Force dynamic rendering to avoid build issues +export const dynamic = "force-dynamic";src/app/_Home/components/Fashion.tsx (2)
30-37: Remove unused viewport tracking logicThe
onUpdatecallback calculates and stores viewport segments but this value is never used. Consider removing it to simplify the code.scrub: 0.6, pin: true, - onUpdate: (self) => { - const scrollPosition = Math.round(self.progress * 300); // 300vw is the total width - const currentViewport = Math.floor(scrollPosition / 100); // Determine the current 100vw scroll step - - if (currentViewport !== lastLoggedViewport.current) { - lastLoggedViewport.current = currentViewport; - } - }, },If this tracking is intended for future use, add a comment explaining its purpose.
55-56: Improve text responsivenessThe text sizing uses fixed breakpoint classes but the word spacing is hardcoded. Consider making it responsive as well.
<h1 - className="text-7xl md:text-40 lg:text-50 xl:text-62.5 font-bold whitespace-nowrap font-moon space-x-20 ml-[3em] md:ml-0" - style={{ wordSpacing: "10rem" }} + className="text-7xl md:text-40 lg:text-50 xl:text-62.5 font-bold whitespace-nowrap font-moon ml-[3em] md:ml-0" + style={{ wordSpacing: "clamp(2rem, 10vw, 10rem)" }} >src/app/admin/orders/page.tsx (2)
4-6: Move export statement to follow React conventionsSame issue as in the order details page - the export statement should be moved for better import organization.
"use client"; import { useState, useEffect, useCallback } from "react"; - -// Force dynamic rendering to avoid build issues -export const dynamic = "force-dynamic"; import { useRouter } from "next/navigation"; + +// Force dynamic rendering to avoid build issues +export const dynamic = "force-dynamic";
121-129: Optimize useCallback dependenciesThe
fetchOrderscallback includes setter functions in its dependency array which are stable and don't need to be included.}, [ router, - setOrders, - setPagination, calculateStats, filterOrdersByStatus, filter, - setLoading, ]src/app/_Home/components/Newsletter.tsx (1)
19-19: Redundant submission check.The
if (isSubmitting) return;check is redundant since the form elements are disabled whenisSubmittingis true.Remove the redundant check:
- if (isSubmitting) return; -src/app/api/checkout/session/route.ts (1)
77-79: Session management architecture concern.The comments indicate session management is client-side only, which raises security concerns for checkout processes.
Consider implementing server-side session storage for checkout processes to prevent tampering with cart contents and ensure data integrity during the checkout flow.
src/app/api/health/route.ts (2)
12-12: Potential information disclosure.Exposing the NODE_ENV environment variable in health check responses could provide attackers with information about the deployment environment.
Consider removing or conditionally including environment information:
- environment: process.env.NODE_ENV, + environment: process.env.NODE_ENV === "production" ? "production" : "non-production",
46-53: Memory usage information could aid attacks.Exposing detailed memory usage information could help attackers plan memory exhaustion attacks or understand system capacity.
Consider limiting memory information in production:
// Check memory usage const memUsage = process.memoryUsage(); + const isProduction = process.env.NODE_ENV === "production"; healthChecks.checks.memory = { - rss: Math.round(memUsage.rss / 1024 / 1024), // MB - heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), // MB - heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), // MB - external: Math.round(memUsage.external / 1024 / 1024), // MB + rss: isProduction ? "hidden" : Math.round(memUsage.rss / 1024 / 1024), // MB + heapUsed: isProduction ? "hidden" : Math.round(memUsage.heapUsed / 1024 / 1024), // MB + heapTotal: isProduction ? "hidden" : Math.round(memUsage.heapTotal / 1024 / 1024), // MB + external: isProduction ? "hidden" : Math.round(memUsage.external / 1024 / 1024), // MB };src/app/admin/delivery-fees/page.tsx (1)
131-136: Consider using a modal for delete confirmationThe native
confirm()dialog works but doesn't match the modern UI design. Consider using a modal component for better UX consistency.You could integrate a confirmation modal library or create a custom modal component for a more polished user experience.
src/app/api/admin/auth/route.ts (2)
7-9: In-memory rate limiting has scalability limitationsThe in-memory Map won't work across multiple server instances and loses data on restart. Consider using Redis (as mentioned in other routes) for consistent rate limiting.
For production deployments with multiple instances, implement Redis-based rate limiting or use a dedicated rate limiting service to ensure consistent protection across all instances.
108-114: Consider dynamic role assignmentThe role is hardcoded as "admin". If the system supports multiple roles, retrieve this from the token payload or user data.
return NextResponse.json({ authenticated: true, user: { username: user.username, - role: "admin", + role: user.role || "admin", }, });src/app/admin/page.tsx (1)
94-94: Add missing dependency to useCallbackThe
fetchMessagescallback in line 94 unnecessarily includes state setters in its dependency array. State setters fromuseStateare stable and don't need to be included.- [router, setMessages, setStats, setPagination, setLoading] + [router]src/app/api/delivery-fees/[state]/route.ts (1)
120-130: Consider using a constant for max delivery feeThe hard-coded value of 10000 for maximum delivery fee should be extracted to a configuration constant for better maintainability.
Add a constant at the top of the file:
const MAX_DELIVERY_FEE = 10000;Then update the validation:
- body.deliveryFee > 10000) + body.deliveryFee > MAX_DELIVERY_FEE) ) { return NextResponse.json( - { error: "Delivery fee must be a number between 0 and 10,000." }, + { error: `Delivery fee must be a number between 0 and ${MAX_DELIVERY_FEE.toLocaleString()}.` },docs/ENVIRONMENT_VARIABLES.md (2)
101-104: Add language specifier to fenced code blockThe error message code block should specify a language for better syntax highlighting.
-``` +```text Error: SITE_URL environment variable is required in production. Please set NEXT_PUBLIC_URL, BASE_URL, NEXT_PUBLIC_BASE_URL, SITE_URL, or VERCEL_URL.--- `119-119`: **Security consideration for JWT_SECRET documentation** Consider adding a note about the importance of JWT secret strength and uniqueness. ```diff JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters +# Note: Use a cryptographically secure random string in productionsrc/app/api/contact/[messageId]/route.ts (1)
144-144: Type assertion could hide type errorsThe
as objecttype assertion bypasses TypeScript's type checking and could hide potential type mismatches.Consider defining a proper type for the update operation or using a more specific assertion:
- $push: { replies: newReply } as object, + $push: { replies: newReply },src/app/admin/messages/page.tsx (3)
94-94: Remove unnecessary dependencies from useCallbackState setters are stable and don't need to be in the dependency array.
- [router, setMessages, setStats, setPagination, setLoading] + [router]
206-206: Use a better confirmation dialogThe native
confirm()blocks the main thread and provides poor UX. Consider using a custom modal dialog.Would you like me to implement a custom confirmation modal component that matches your design system?
486-486: Improve close button accessibilityThe modal close button uses a multiplication sign character which may not be accessible to screen readers.
- × + <span aria-label="Close">×</span>src/app/api/admin/analytics/customers/route.ts (1)
39-42: Remove redundant variable assignmentThe
daysvariable on line 41 is redundant sincerangeis already available and used directly on line 42.const end = new Date(); const start = new Date(); -const days = range; -start.setDate(start.getDate() - days); +start.setDate(start.getDate() - range);src/app/api/newsletter/route.ts (2)
6-10: Consider distributed rate limiting for productionThe in-memory Map-based rate limiting won't work correctly in serverless or multi-instance deployments. Consider using Redis or a similar distributed cache for production environments.
108-108: Validate NEXT_PUBLIC_URL environment variableThe URL construction should validate the environment variable to ensure it's a valid URL format.
Consider validating the URL:
const baseUrl = process.env.NEXT_PUBLIC_URL || "https://himspired.com"; // Ensure URL doesn't have trailing slash for consistent concatenation const shopUrl = baseUrl.replace(/\/$/, '') + '/shop';src/app/api/orders/[orderId]/route.ts (1)
172-230: Consider using database transactions for atomicityThe stock updates and order status changes should ideally be wrapped in a database transaction to ensure atomicity. If any stock update fails, the entire operation should roll back.
src/app/api/orders/route.ts (2)
102-109: Implement virus scanning for uploaded filesThe virus scanning placeholder should be implemented before production deployment to prevent malicious file uploads.
Would you like me to help implement virus scanning using ClamAV or integrate with a cloud-based virus scanning API?
158-166: Extract email validation to a utility functionThe email validation function should be extracted to a shared utility for reuse across the codebase.
Consider moving this to a shared utilities file:
// In @/lib/validation.ts export const isValidEmail = (email: string): boolean => { if (!email || typeof email !== "string" || email.trim() === "") { return false; } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email.trim()); };src/app/_Home/components/products/components/ProductSection.tsx (4)
47-60: Simplify grid items creation using array methods.The manual loop can be replaced with a more declarative approach using
slice()andmap().- // Create grid items based on itemsToShow - const gridItems = []; - for (let i = 0; i < itemsToShow; i++) { - if (i < products.length) { - const product = products[i]; - gridItems.push( - <ProductItem - key={product._id} - product={product} - itemsToShow={itemsToShow} - variants={itemVariants} - /> - ); - } - } + // Create grid items based on itemsToShow + const gridItems = products + .slice(0, itemsToShow) + .map((product) => ( + <ProductItem + key={product._id} + product={product} + itemsToShow={itemsToShow} + variants={itemVariants} + /> + ));
335-346: Remove console.error from production code.Console errors should be handled through proper logging mechanisms in production.
- console.error("Reservation error:", error); + // Use proper error logging service in production + if (process.env.NODE_ENV === 'development') { + console.error("Reservation error:", error); + }
396-397: Extract inline style calculation for better maintainability.The complex width calculation should be extracted to improve readability.
+ const itemWidth = useMemo(() => ({ + width: `calc(${100 / itemsToShow}% - ${itemsToShow > 1 ? "2rem" : "0rem"})` + }), [itemsToShow]); + return ( <motion.div ref={cardRef} onClick={handleClick} className="flex flex-col gap-y-2 items-center cursor-pointer" variants={variants} - style={{ - width: `calc(${100 / itemsToShow}% - ${itemsToShow > 1 ? "2rem" : "0rem"})`, - }} + style={itemWidth} >
424-425: Extract complex conditional styling for better readability.The stock message color logic should be extracted to a utility function.
+ const getStockMessageColor = (stock: number) => { + if (stock <= 0) return "text-red-600"; + if (stock <= 3) return "text-orange-600"; + return "text-green-600"; + }; + {stockMessage && ( <span - className={`text-xs font-medium ${currentStock <= 0 ? "text-red-600" : currentStock <= 3 ? "text-orange-600" : "text-green-600"}`} + className={`text-xs font-medium ${getStockMessageColor(currentStock)}`} > {stockMessage} </span> )}
| globals: { | ||
| "ts-jest": { | ||
| useESM: true, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update deprecated ts-jest configuration syntax
The globals configuration for ts-jest is deprecated. The useESM option should be configured through the preset or transform options instead.
Consider updating to the modern configuration:
- extensionsToTreatAsEsm: [".ts", ".tsx"],
- globals: {
- "ts-jest": {
- useESM: true,
- },
- },
+ extensionsToTreatAsEsm: [".ts", ".tsx"],
+ preset: "ts-jest/presets/default-esm",
+ transform: {
+ "^.+\\.tsx?$": ["ts-jest", {
+ useESM: true,
+ }],
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| globals: { | |
| "ts-jest": { | |
| useESM: true, | |
| }, | |
| }, | |
| extensionsToTreatAsEsm: [".ts", ".tsx"], | |
| preset: "ts-jest/presets/default-esm", | |
| transform: { | |
| "^.+\\.tsx?$": ["ts-jest", { | |
| useESM: true, | |
| }], | |
| }, |
🤖 Prompt for AI Agents
In jest.config.js around lines 37 to 41, the ts-jest configuration uses the
deprecated globals syntax to set useESM. Remove the useESM option from globals
and instead configure it properly via the preset or transform options according
to the latest ts-jest documentation, ensuring the ESM support is enabled in the
recommended way.
| images: { | ||
| domains: ["cdn.sanity.io", "res.cloudinary.com"], | ||
| formats: ["image/webp", "image/avif"], | ||
| minimumCacheTTL: 60, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Update deprecated image configuration
The domains configuration for images is deprecated in Next.js 12.3.0+. Use remotePatterns for better security and flexibility.
images: {
- domains: ["cdn.sanity.io", "res.cloudinary.com"],
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'cdn.sanity.io',
+ pathname: '/images/**',
+ },
+ {
+ protocol: 'https',
+ hostname: 'res.cloudinary.com',
+ pathname: '/**',
+ },
+ ],
formats: ["image/webp", "image/avif"],
minimumCacheTTL: 60,
},🌐 Web query:
What is the latest Next.js image configuration syntax for remote patterns?
💡 Result:
The latest (mid-2025) Next.js image configuration syntax for remote patterns uses the remotePatterns property in your next.config.js file, allowing you to specify exactly which remote images can be optimized by the <Image /> component. Wildcard support is included for both hostnames and pathnames, offering fine-grained control.
Example: Remote Patterns Syntax (as of Next.js 15.x+)
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
port: '', // Optional: leave empty for default ports
pathname: '/account123/**', // Supports * or ** wildcards
search: '', // Optional: restricts query string
},
],
},
}- The above will allow images from
https://example.com/account123/* pathnameandhostnamesupport wildcards:
Wildcard Examples:
-
Match all subdomains:
hostname: '**.example.com'Matches
img1.example.com,foo.bar.example.com, etc.[4][8] -
Match all image paths under a prefix:
pathname: '/images/**'Matches
/images/cat.png,/images/profile/xyz.jpg, etc.1
Updated Syntax Note:
As of Next.js v15.3.0 and later, you can also use a URL object for remotePatterns, such as:
module.exports = {
images: {
remotePatterns: [new URL('https://example.com/account123/**')],
},
}This is an alternative to the object-syntax above and is newer1.
Security Consideration:
- Be as specific as possible with
remotePatternsto prevent malicious usage. Overly broad patterns (e.g., hostname: '**') may expose your optimization API to abuse110. - The older
domainsarray is deprecated in favor of the more powerfulremotePatterns3.
Summary Table:
| Property | Description | Supports Wildcards | Example Value |
|---|---|---|---|
| protocol | URL protocol, typically 'https' | No | 'https' |
| hostname | Remote host; supports wildcards | Yes | 's3.amazonaws.com', '**.example.com' |
| port | Port, default empty for standard | No | '', '443' |
| pathname | Path on the host; supports wildcards | Yes | '/bucket/**', '/images/*' |
| search | Query string; restricts by ?key=value | No | '?v=1' |
Reference: For official and always up-to-date examples, see the Next.js Image docs4.
Citations:
- 1: https://nextjs.org/docs/pages/api-reference/components/image
- 2: https://www.youtube.com/watch?v=gpJKj45AikY
- 3: https://prismic.io/blog/nextjs-image-component-optimization
- 4: https://nextjs.org/docs/pages/api-reference/components/image-legacy
- 5: https://refine.dev/blog/using-next-image/
- 6: https://sentry.io/answers/how-to-allow-domains-for-images-in-next-js/
- 7: https://nextjs.org/docs/app/api-reference/config/next-config-js/images
- 8: How to use remotePatterns? vercel/next.js#61071
- 9: https://www.youtube.com/watch?v=IU_qq_c_lKA
- 10: Allowing all remotePatterns when using next/image - is it safe? vercel/next.js#71843
Update deprecated image configuration
The domains array is deprecated in Next.js 12.3+. Replace it with remotePatterns to explicitly whitelist remote sources:
// next.config.ts
images: {
- domains: ["cdn.sanity.io", "res.cloudinary.com"],
+ remotePatterns: [
+ {
+ protocol: 'https',
+ hostname: 'cdn.sanity.io',
+ port: '', // optional, default ports
+ pathname: '/images/**',
+ search: '', // optional, restrict query strings
+ },
+ {
+ protocol: 'https',
+ hostname: 'res.cloudinary.com',
+ port: '',
+ pathname: '/**',
+ search: '',
+ },
+ ],
formats: ["image/webp", "image/avif"],
minimumCacheTTL: 60,
},• Adjust pathname (and optionally port/search) to match only the paths you need.
• This change improves security and unlocking wildcard support for hostnames and paths.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| images: { | |
| domains: ["cdn.sanity.io", "res.cloudinary.com"], | |
| formats: ["image/webp", "image/avif"], | |
| minimumCacheTTL: 60, | |
| }, | |
| images: { | |
| remotePatterns: [ | |
| { | |
| protocol: 'https', | |
| hostname: 'cdn.sanity.io', | |
| port: '', // optional, default ports | |
| pathname: '/images/**', | |
| search: '', // optional, restrict query strings | |
| }, | |
| { | |
| protocol: 'https', | |
| hostname: 'res.cloudinary.com', | |
| port: '', | |
| pathname: '/**', | |
| search: '', | |
| }, | |
| ], | |
| formats: ["image/webp", "image/avif"], | |
| minimumCacheTTL: 60, | |
| }, |
🤖 Prompt for AI Agents
In next.config.ts around lines 4 to 8, the images configuration uses the
deprecated 'domains' array. Replace 'domains' with 'remotePatterns' by
specifying an array of objects that include 'protocol', 'hostname', and
'pathname' to whitelist remote image sources explicitly. Adjust 'pathname' to
match only necessary paths and optionally include 'port' and 'search' fields for
finer control, improving security and enabling wildcard support.
| experimental: { | ||
| optimizeCss: true, | ||
| optimizePackageImports: ["lucide-react", "framer-motion"], | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove invalid experimental configuration
The optimizeCss option is not a valid experimental feature in Next.js. CSS optimization is handled automatically by Next.js.
experimental: {
- optimizeCss: true,
optimizePackageImports: ["lucide-react", "framer-motion"],
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| experimental: { | |
| optimizeCss: true, | |
| optimizePackageImports: ["lucide-react", "framer-motion"], | |
| }, | |
| experimental: { | |
| optimizePackageImports: ["lucide-react", "framer-motion"], | |
| }, |
🤖 Prompt for AI Agents
In next.config.ts around lines 10 to 13, remove the invalid experimental option
`optimizeCss` from the experimental configuration object, as Next.js handles CSS
optimization automatically and this option is not recognized.
|
|
||
| - Filter by category (All, Thrift, Luxury, Senators) | ||
| - View product details | ||
| - Add products to cart |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outdated category references conflict with PR objectives
Line 116 still references the old brand-based categories (Thrift, Luxury, Senators) which should be updated to the new product-type categories (Polos, T-Shirts, Jeans, Shirts) according to the PR objectives.
Apply this diff to align with the stated category changes:
- Filter by category (All, Thrift, Luxury, Senators)
+ Filter by category (All, Polos, T-Shirts, Jeans, Shirts)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - Filter by category (All, Thrift, Luxury, Senators) | |
| - View product details | |
| - Add products to cart | |
| - Filter by category (All, Polos, T-Shirts, Jeans, Shirts) | |
| - View product details | |
| - Add products to cart |
🤖 Prompt for AI Agents
In README.md around lines 115 to 118, update the category references from the
old brand-based categories (Thrift, Luxury, Senators) to the new product-type
categories (Polos, T-Shirts, Jeans, Shirts) to align with the PR objectives.
Replace the outdated category list with the new one in the relevant bullet
point.
| ttf_size=$(stat -f%z "$ttf_file") | ||
| woff2_size=$(stat -f%z "$woff2_file") | ||
| savings=$((ttf_size - woff2_size)) | ||
| savings_percent=$((savings * 100 / ttf_size)) | ||
|
|
||
| echo "$(basename "$woff2_file"): ${woff2_size} bytes (${savings_percent}% smaller than TTF)" | ||
| elif [ -f "$otf_file" ]; then | ||
| otf_size=$(stat -f%z "$otf_file") | ||
| woff2_size=$(stat -f%z "$woff2_file") | ||
| savings=$((otf_size - woff2_size)) | ||
| savings_percent=$((savings * 100 / otf_size)) | ||
|
|
||
| echo "$(basename "$woff2_file"): ${woff2_size} bytes (${savings_percent}% smaller than OTF)" | ||
| fi | ||
| done |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Improve portability across Unix systems.
The script uses macOS-specific stat -f%z command, which won't work on Linux systems.
Consider making the script more portable:
- ttf_size=$(stat -f%z "$ttf_file")
- woff2_size=$(stat -f%z "$woff2_file")
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ ttf_size=$(stat -f%z "$ttf_file")
+ woff2_size=$(stat -f%z "$woff2_file")
+ else
+ ttf_size=$(stat -c%s "$ttf_file")
+ woff2_size=$(stat -c%s "$woff2_file")
+ fi📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ttf_size=$(stat -f%z "$ttf_file") | |
| woff2_size=$(stat -f%z "$woff2_file") | |
| savings=$((ttf_size - woff2_size)) | |
| savings_percent=$((savings * 100 / ttf_size)) | |
| echo "$(basename "$woff2_file"): ${woff2_size} bytes (${savings_percent}% smaller than TTF)" | |
| elif [ -f "$otf_file" ]; then | |
| otf_size=$(stat -f%z "$otf_file") | |
| woff2_size=$(stat -f%z "$woff2_file") | |
| savings=$((otf_size - woff2_size)) | |
| savings_percent=$((savings * 100 / otf_size)) | |
| echo "$(basename "$woff2_file"): ${woff2_size} bytes (${savings_percent}% smaller than OTF)" | |
| fi | |
| done | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| ttf_size=$(stat -f%z "$ttf_file") | |
| woff2_size=$(stat -f%z "$woff2_file") | |
| else | |
| ttf_size=$(stat -c%s "$ttf_file") | |
| woff2_size=$(stat -c%s "$woff2_file") | |
| fi | |
| savings=$((ttf_size - woff2_size)) | |
| savings_percent=$((savings * 100 / ttf_size)) | |
| echo "$(basename "$woff2_file"): ${woff2_size} bytes (${savings_percent}% smaller than TTF)" |
🤖 Prompt for AI Agents
In scripts/optimize-fonts.sh around lines 52 to 66, the script uses the
macOS-specific stat command with the -f%z option to get file sizes, which is not
portable to Linux. Modify the script to detect the operating system and use the
appropriate stat command: on Linux, use stat -c%s, and on macOS, keep using stat
-f%z. Implement a conditional check for the OS type at the start of the script
and assign the correct stat command to a variable, then use that variable to get
file sizes throughout the script.
| function getClientIp(req: NextRequest) { | ||
| return ( | ||
| req.headers.get("x-forwarded-for") || | ||
| req.headers.get("x-real-ip") || | ||
| "unknown" | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Handle multiple IPs in x-forwarded-for header
The x-forwarded-for header can contain multiple comma-separated IPs when requests pass through multiple proxies. Consider extracting only the first IP.
function getClientIp(req: NextRequest) {
- return (
- req.headers.get("x-forwarded-for") ||
- req.headers.get("x-real-ip") ||
- "unknown"
- );
+ const forwarded = req.headers.get("x-forwarded-for");
+ if (forwarded) {
+ // Take the first IP if multiple are present
+ return forwarded.split(',')[0].trim();
+ }
+ return req.headers.get("x-real-ip") || "unknown";
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getClientIp(req: NextRequest) { | |
| return ( | |
| req.headers.get("x-forwarded-for") || | |
| req.headers.get("x-real-ip") || | |
| "unknown" | |
| ); | |
| } | |
| function getClientIp(req: NextRequest) { | |
| const forwarded = req.headers.get("x-forwarded-for"); | |
| if (forwarded) { | |
| // Take the first IP if multiple are present | |
| return forwarded.split(',')[0].trim(); | |
| } | |
| return req.headers.get("x-real-ip") || "unknown"; | |
| } |
🤖 Prompt for AI Agents
In src/app/api/delivery-fees/route.ts between lines 8 and 14, the getClientIp
function currently returns the entire x-forwarded-for header value, which may
contain multiple comma-separated IPs. Modify the function to split the
x-forwarded-for header by commas and return only the first IP address from the
list to correctly identify the client IP.
| if (!body.state || !states.includes(body.state)) { | ||
| return NextResponse.json( | ||
| { error: "Invalid state. Please provide a valid Nigerian state." }, | ||
| { status: 400 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider case-insensitive state validation
The current validation may reject valid states due to case sensitivity or whitespace issues.
// Validation
- if (!body.state || !states.includes(body.state)) {
+ const normalizedState = body.state?.trim();
+ const validState = states.find(s => s.toLowerCase() === normalizedState?.toLowerCase());
+ if (!normalizedState || !validState) {
return NextResponse.json(
{ error: "Invalid state. Please provide a valid Nigerian state." },
{ status: 400 }
);
}
+ // Use the correctly cased state name
+ body.state = validState;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (!body.state || !states.includes(body.state)) { | |
| return NextResponse.json( | |
| { error: "Invalid state. Please provide a valid Nigerian state." }, | |
| { status: 400 } | |
| ); | |
| } | |
| // Validation | |
| const normalizedState = body.state?.trim(); | |
| const validState = states.find(s => s.toLowerCase() === normalizedState?.toLowerCase()); | |
| if (!normalizedState || !validState) { | |
| return NextResponse.json( | |
| { error: "Invalid state. Please provide a valid Nigerian state." }, | |
| { status: 400 } | |
| ); | |
| } | |
| // Use the correctly cased state name | |
| body.state = validState; |
🤖 Prompt for AI Agents
In src/app/api/delivery-fees/route.ts around lines 88 to 93, the state
validation is case-sensitive and does not handle extra whitespace, which may
incorrectly reject valid states. To fix this, normalize the input state by
trimming whitespace and converting it to a consistent case (e.g., lowercase)
before checking if it exists in the states list, which should also be normalized
to the same case for comparison.
| const itemsList = items | ||
| .map( | ||
| (item) => | ||
| `<li style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 4px;"> | ||
| <strong>${item.title}</strong><br> | ||
| <span style="color: #666; font-size: 14px;">Size: ${item.size || "N/A"} | Qty: ${item.quantity} | ₦${item.price.toLocaleString()}</span> | ||
| </li>` | ||
| ) | ||
| .join(""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential XSS vulnerability in email template.
The email template directly interpolates user data without sanitization, which could lead to XSS if the data contains malicious content.
Sanitize the data before inserting into HTML:
+import DOMPurify from 'isomorphic-dompurify';
const itemsList = items
.map(
(item) =>
`<li style="margin: 10px 0; padding: 10px; background: #f9f9f9; border-radius: 4px;">
- <strong>${item.title}</strong><br>
+ <strong>${DOMPurify.sanitize(item.title)}</strong><br>
- <span style="color: #666; font-size: 14px;">Size: ${item.size || "N/A"} | Qty: ${item.quantity} | ₦${item.price.toLocaleString()}</span>
+ <span style="color: #666; font-size: 14px;">Size: ${DOMPurify.sanitize(item.size) || "N/A"} | Qty: ${item.quantity} | ₦${item.price.toLocaleString()}</span>
</li>`
)
.join("");🤖 Prompt for AI Agents
In src/app/api/orders/send-confirmation/route.ts around lines 30 to 38, the
email template directly inserts user data into HTML without sanitization,
risking XSS attacks. To fix this, sanitize all user-provided fields like
item.title, item.size, and item.quantity before embedding them in the HTML
string. Use a proper HTML escaping function to replace special characters with
safe entities to prevent malicious code execution.
| <div style="background: #f8f8f8; padding: 25px; border-radius: 8px;"> | ||
| <h2 style="color: #68191E; margin-top: 0;">Order Received Successfully!</h2> | ||
| <p style="color: #333; line-height: 1.6;">Dear ${customerInfo.name},</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sanitize customer name in email template.
The customer name is directly interpolated into the HTML email without sanitization.
Apply sanitization:
- <p style="color: #333; line-height: 1.6;">Dear ${customerInfo.name},</p>
+ <p style="color: #333; line-height: 1.6;">Dear ${DOMPurify.sanitize(customerInfo.name)},</p>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <p style="color: #333; line-height: 1.6;">Dear ${customerInfo.name},</p> | |
| <p style="color: #333; line-height: 1.6;">Dear ${DOMPurify.sanitize(customerInfo.name)},</p> |
🤖 Prompt for AI Agents
In src/app/api/orders/send-confirmation/route.ts at line 53, the customer name
is directly inserted into the HTML email template without sanitization, which
can lead to XSS vulnerabilities. Fix this by sanitizing the customerInfo.name
value before interpolation, using a suitable HTML escaping or sanitization
function to ensure any special characters are safely encoded.
| const { orderId, email, message, updateStatus } = await req.json(); | ||
| if (!orderId || !email || !message) { | ||
| return NextResponse.json( | ||
| { error: "Missing required fields" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Optionally update order status | ||
| if (updateStatus && isValidOrderStatus(updateStatus)) { | ||
| await orderService.updateOrderStatus( | ||
| orderId, | ||
| updateStatus as OrderStatus | ||
| ); | ||
| } | ||
|
|
||
| // Send the custom email | ||
| await sendCustomOrderEmail(email, "Order Update from Himspired", message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation and rate limiting
The endpoint lacks important security measures:
- Email format validation is missing
- Message content should be sanitized to prevent injection attacks
- Rate limiting should be applied to prevent abuse
Consider adding these validations:
const { orderId, email, message, updateStatus } = await req.json();
+
+// Validate email format
+const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+if (!emailRegex.test(email)) {
+ return NextResponse.json(
+ { error: "Invalid email format" },
+ { status: 400 }
+ );
+}
+
+// Sanitize message content (basic HTML escape)
+const sanitizedMessage = message.replace(/[<>]/g, '');
+
if (!orderId || !email || !message) {Also consider implementing rate limiting using a middleware or service like @/lib/rate-limiter.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/api/orders/send-custom-email/route.ts around lines 10 to 27, add
validation to check that the email field contains a valid email format and
sanitize the message content to prevent injection attacks before processing.
Additionally, implement rate limiting by integrating a middleware or service
such as '@/lib/rate-limiter' to restrict the number of requests to this endpoint
and prevent abuse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| const ProductItem = ({ | ||
| product, | ||
| itemsToShow, | ||
| variants, | ||
| }: { | ||
| product: Product; | ||
| itemsToShow: number; | ||
| variants: Variants | undefined; | ||
| }) => { | ||
| const [showSizes, setShowSizes] = useState(false); | ||
| const [addedToCart, setAddedToCart] = useState<string | null>(null); | ||
| const [currentStock, setCurrentStock] = useState(product.stock || 0); | ||
| const [stockMessage, setStockMessage] = useState(""); | ||
| const [isAvailable, setIsAvailable] = useState(true); | ||
| const [lastKnownStock, setLastKnownStock] = useState(product.stock || 0); | ||
| const [isReserving, setIsReserving] = useState(false); | ||
| const cardRef = useRef<HTMLDivElement>(null); | ||
| const router = useRouter(); | ||
| const dispatch = useAppDispatch(); | ||
| const fetchStockRef = useRef<(() => Promise<void>) | undefined>(undefined); | ||
|
|
||
| // Get cart quantities for this product | ||
| const cartQuantity = useAppSelector((state) => | ||
| selectCartItemQuantity(state, product._id, product.size?.[0] || "") | ||
| ); | ||
|
|
||
| // Get all cart items for this product | ||
| const cartItems = useAppSelector((state) => | ||
| state.persistedReducer.cart.items.filter( | ||
| (item) => item.originalProductId === product._id | ||
| ) | ||
| ); | ||
|
|
||
| // Memoized size-specific quantities to prevent unnecessary rerenders | ||
| const sizeQuantities = useMemo(() => { | ||
| const quantities: Record<string, number> = {}; | ||
| cartItems.forEach((item: CartItem) => { | ||
| if (item.size) { | ||
| quantities[item.size] = | ||
| (quantities[item.size] || 0) + (item.quantity || 1); | ||
| } | ||
| }); | ||
| return quantities; | ||
| }, [cartItems]); | ||
|
|
||
| // Check product availability | ||
| const checkAvailability = useCallback(async () => { | ||
| try { | ||
| const sessionId = SessionManager.getSessionId(); | ||
| const res = await fetch( | ||
| `/api/products/availability/${product._id}?sessionId=${sessionId}` | ||
| ); | ||
| if (!res.ok) return; | ||
| const data = await res.json(); | ||
| setIsAvailable(data.isAvailable); | ||
| } catch (error) { | ||
| console.error("Error checking availability:", error); | ||
| } | ||
| }, [product._id]); | ||
|
|
||
| // Real-time polling for stock and reservation status | ||
| useEffect(() => { | ||
| SessionManager.refreshSessionIfNeeded(); | ||
| let isMounted = true; | ||
| const sessionId = SessionManager.getSessionId(); | ||
| const fetchStock = async () => { | ||
| try { | ||
| const res = await fetch( | ||
| `/api/products/stock/${product._id}?sessionId=${sessionId}` | ||
| ); | ||
| if (!res.ok) return; | ||
| const data = await res.json(); | ||
| if (isMounted) { | ||
| const newStock = data.stock || 0; | ||
| const availableStock = data.availableStock || 0; | ||
| const reservedByCurrentUser = data.reservedByCurrentUser || 0; | ||
| const reservedByOthers = data.reservedByOthers || 0; | ||
|
|
||
| // Check if stock changed significantly (admin intervention) | ||
| if (Math.abs(newStock - lastKnownStock) > 1) { | ||
| console.warn( | ||
| `Significant stock change detected for ${product.title}: ${lastKnownStock} -> ${newStock}` | ||
| ); | ||
| toast.info(`Stock updated for ${product.title}`); | ||
| } | ||
|
|
||
| setCurrentStock(newStock); | ||
| setLastKnownStock(newStock); | ||
|
|
||
| // Use the enhanced stock message from API | ||
| setStockMessage(data.stockMessage || ""); | ||
|
|
||
| // Update availability based on available stock | ||
| if (availableStock <= 0) { | ||
| setIsAvailable(false); | ||
| } else { | ||
| setIsAvailable(true); | ||
| } | ||
|
|
||
| // Log reservation info for debugging | ||
| if (reservedByCurrentUser > 0 || reservedByOthers > 0) { | ||
| console.log( | ||
| `${product.title}: You reserved ${reservedByCurrentUser}, Others reserved ${reservedByOthers}, Available: ${availableStock}` | ||
| ); | ||
| } | ||
| } | ||
| } catch { | ||
| // Ignore errors for polling | ||
| } | ||
| }; | ||
|
|
||
| // Initial fetch | ||
| fetchStock(); | ||
|
|
||
| // Also fetch after a short delay to catch any recent changes | ||
| const immediateRefresh = setTimeout(fetchStock, 2000); | ||
|
|
||
| // Poll every 5 seconds for faster responsiveness | ||
| const interval = setInterval(fetchStock, 5000); | ||
|
|
||
| // Listen for cart changes from other tabs | ||
| const handleStorageChange = (event: StorageEvent) => { | ||
| if ( | ||
| event.key === CACHE_KEYS.CART_SYNC || | ||
| event.key === CACHE_KEYS.STOCK_UPDATE | ||
| ) { | ||
| // Refetch stock when cart changes or stock updates | ||
| fetchStock(); | ||
| } | ||
| }; | ||
|
|
||
| // Listen for visibility changes to refresh stock when tab becomes active | ||
| const handleVisibilityChange = () => { | ||
| if (!document.hidden) { | ||
| fetchStock(); | ||
| } | ||
| }; | ||
|
|
||
| // Listen for window focus to refresh stock when user switches back to tab | ||
| const handleWindowFocus = () => { | ||
| fetchStock(); | ||
| }; | ||
|
|
||
| window.addEventListener("storage", handleStorageChange); | ||
| document.addEventListener("visibilitychange", handleVisibilityChange); | ||
| window.addEventListener("focus", handleWindowFocus); | ||
|
|
||
| // Store fetchStock in a ref for instant update after reservation | ||
| fetchStockRef.current = fetchStock; | ||
|
|
||
| return () => { | ||
| isMounted = false; | ||
| clearInterval(interval); | ||
| clearTimeout(immediateRefresh); | ||
| window.removeEventListener("storage", handleStorageChange); | ||
| document.removeEventListener("visibilitychange", handleVisibilityChange); | ||
| window.removeEventListener("focus", handleWindowFocus); | ||
| fetchStockRef.current = undefined; | ||
| }; | ||
| }, [product._id, lastKnownStock, product.title]); | ||
|
|
||
| // Initialize session and check availability | ||
| useEffect(() => { | ||
| SessionManager.refreshSessionIfNeeded(); | ||
| checkAvailability(); | ||
|
|
||
| // Also fetch stock immediately when component mounts | ||
| if (fetchStockRef.current) { | ||
| fetchStockRef.current(); | ||
| } | ||
| }, [product._id, checkAvailability]); | ||
|
|
||
| const handleClick = () => { | ||
| router.push(`/shop/${product._id}/${product.slug?.current}`); | ||
| }; | ||
|
|
||
| const handleAddToCart = async ( | ||
| selectedSize?: string, | ||
| e?: React.MouseEvent | ||
| ) => { | ||
| if (e) e.stopPropagation(); | ||
|
|
||
| // Check if product is available | ||
| if (!isAvailable) { | ||
| toast.error(stockMessage || "Product is not available"); | ||
| return; | ||
| } | ||
|
|
||
| setIsReserving(true); | ||
| SessionManager.refreshSessionIfNeeded(); | ||
| const sessionId = SessionManager.getSessionId(); | ||
|
|
||
| // Get current cart quantity for this specific product and size | ||
| const currentSize = selectedSize || product.size?.[0] || ""; | ||
| const currentCartQuantity = sizeQuantities[currentSize] || 0; | ||
|
|
||
| // Calculate new total quantity (current + 1) | ||
| const newTotalQuantity = currentCartQuantity + 1; | ||
|
|
||
| let reservationResult: { | ||
| success: boolean; | ||
| reservationId?: string; | ||
| error?: string; | ||
| } | null = null; | ||
|
|
||
| // Reservation logic | ||
| try { | ||
| const controller = new AbortController(); | ||
| const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout | ||
|
|
||
| const reservationData = { | ||
| sessionId, | ||
| quantity: newTotalQuantity, // Reserve the new total quantity | ||
| size: selectedSize || product.size?.[0] || "", | ||
| isUpdate: currentCartQuantity > 0, // Mark as update if item already in cart | ||
| }; | ||
|
|
||
| const response = await fetch(`/api/products/reserve/${product._id}`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(reservationData), | ||
| signal: controller.signal, | ||
| }); | ||
|
|
||
| clearTimeout(timeoutId); | ||
| reservationResult = await response.json(); | ||
|
|
||
| if (!reservationResult?.success) { | ||
| // Check if it's a reservation conflict | ||
| if ( | ||
| reservationResult?.error && | ||
| reservationResult.error.includes("already reserved") | ||
| ) { | ||
| toast.error( | ||
| "This product is currently being purchased by another user" | ||
| ); | ||
| } else if ( | ||
| reservationResult?.error && | ||
| (reservationResult.error.includes("timeout") || | ||
| reservationResult.error.includes("connection")) | ||
| ) { | ||
| toast.error( | ||
| "This product is currently being purchased by another user" | ||
| ); | ||
| } else { | ||
| toast.error(reservationResult?.error || "Failed to reserve product"); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| toast.success("Product reserved and added to cart"); | ||
|
|
||
| // Instantly update stock/reservation status after reservation | ||
| if (fetchStockRef.current) { | ||
| await fetchStockRef.current(); | ||
| } | ||
|
|
||
| // Broadcast stock update to other tabs | ||
| localStorage.setItem(CACHE_KEYS.STOCK_UPDATE, Date.now().toString()); | ||
| } catch (error) { | ||
| console.error("Reservation error:", error); | ||
|
|
||
| if (error instanceof Error) { | ||
| if (error.name === "AbortError") { | ||
| toast.error("Reservation timed out. Please try again."); | ||
| } else { | ||
| toast.error("Failed to reserve product"); | ||
| } | ||
| } else { | ||
| toast.error("Failed to reserve product"); | ||
| } | ||
| return; | ||
| } finally { | ||
| setIsReserving(false); | ||
| } | ||
|
|
||
| // Fixed: Updated to match new CartItem interface | ||
| const data: Omit< | ||
| CartItem, | ||
| "quantity" | "originalPrice" | "originalProductId" | ||
| > = { | ||
| _id: product._id, | ||
| title: product.title, | ||
| category: product.category, | ||
| mainImage: product.mainImage, | ||
| price: product.price, | ||
| size: selectedSize || product.size?.[0] || "", | ||
| stock: product.stock || 0, // Include stock information | ||
| reservationId: reservationResult?.reservationId, // Include reservation ID | ||
| }; | ||
|
|
||
| dispatch(addItem(data)); | ||
|
|
||
| // Show feedback animation | ||
| setAddedToCart(selectedSize || "default"); | ||
| setTimeout(() => setAddedToCart(null), 1500); | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| const handleClickOutside = (event: globalThis.MouseEvent) => { | ||
| if (cardRef.current && !cardRef.current.contains(event.target as Node)) { | ||
| setShowSizes(false); | ||
| } | ||
| }; | ||
|
|
||
| if (showSizes) { | ||
| document.addEventListener("mousedown", handleClickOutside); | ||
| } | ||
|
|
||
| return () => { | ||
| document.removeEventListener("mousedown", handleClickOutside); | ||
| }; | ||
| }, [showSizes]); | ||
|
|
||
| return ( | ||
| <motion.div | ||
| ref={cardRef} | ||
| onClick={handleClick} | ||
| className="flex flex-col gap-y-2 items-center cursor-pointer" | ||
| variants={variants} | ||
| style={{ | ||
| width: `calc(${100 / itemsToShow}% - ${itemsToShow > 1 ? "2rem" : "0rem"})`, | ||
| }} | ||
| > | ||
| <motion.div | ||
| whileHover={{ scale: 1.05 }} | ||
| transition={{ duration: 0.3 }} | ||
| className="relative group cursor-pointer" | ||
| > | ||
| <SanityImageComponent | ||
| image={product.mainImage || "/placeholder.svg"} | ||
| alt={product.title} | ||
| width={0} | ||
| height={0} | ||
| className="w-auto h-auto md:px-7 py-3 md:py-4.5" | ||
| /> | ||
| <div className="absolute inset-0 bg-black/0 transition-all duration-300 rounded-lg"></div> | ||
| </motion.div> | ||
|
|
||
| <div className="flex flex-col gap-y-2.5"> | ||
| <p className="text-gray-850/50 text-xs">{product.category}</p> | ||
| <h3 className="text-gray-850 text-sm md:text-base whitespace-nowrap overflow-hidden text-ellipsis w-40"> | ||
| {product.title} | ||
| </h3> | ||
| <p className="text-gray-850 text-xs md:text-base"> | ||
| NGN {product.price.toLocaleString()} | ||
| </p> | ||
| {stockMessage && ( | ||
| <span | ||
| className={`text-xs font-medium ${currentStock <= 0 ? "text-red-600" : currentStock <= 3 ? "text-orange-600" : "text-green-600"}`} | ||
| > | ||
| {stockMessage} | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {!showSizes && ( | ||
| <motion.button | ||
| className={`mt-1.5 p-3 md:p-4 w-fit rounded-full transition-all duration-300 ${ | ||
| addedToCart === "default" | ||
| ? "bg-green-100 border-2 border-green-500" | ||
| : !isAvailable || isReserving | ||
| ? "bg-gray-300 cursor-not-allowed" | ||
| : "bg-white-200 hover:bg-gray-200" | ||
| }`} | ||
| whileHover={isAvailable && !isReserving ? { scale: 1.1 } : undefined} | ||
| whileTap={isAvailable && !isReserving ? { scale: 0.95 } : undefined} | ||
| onClick={(e) => { | ||
| if (!isAvailable || isReserving) return; | ||
| if (!product.size?.length) { | ||
| handleAddToCart(undefined, e); | ||
| } else { | ||
| e.stopPropagation(); | ||
| setShowSizes(true); | ||
| } | ||
| }} | ||
| disabled={!isAvailable || isReserving} | ||
| > | ||
| {addedToCart === "default" ? <Check color="#22c55e" /> : <Plus />} | ||
| </motion.button> | ||
| )} | ||
|
|
||
| {!product.size?.length && cartQuantity > 0 && ( | ||
| <p className="text-xs text-[#68191E] font-medium"> | ||
| In cart: {cartQuantity} | ||
| </p> | ||
| )} | ||
|
|
||
| <AnimatePresence> | ||
| {showSizes && ( | ||
| <motion.div | ||
| className="flex gap-4 flex-wrap justify-center" | ||
| initial={{ opacity: 0, y: 20 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0, y: 20 }} | ||
| transition={{ duration: 0.4 }} | ||
| > | ||
| {product.size?.map((sizeOption) => { | ||
| const sizeCartQuantity = sizeQuantities[sizeOption] || 0; | ||
|
|
||
| return ( | ||
| <motion.div | ||
| key={sizeOption} | ||
| onClick={(e) => { | ||
| if (isReserving) return; | ||
| e.stopPropagation(); | ||
| handleAddToCart(sizeOption, e); | ||
| setShowSizes(false); | ||
| }} | ||
| className={`px-4 py-2 font-medium rounded-full flex flex-col items-center justify-center text-sm font-activo uppercase cursor-pointer transition-all duration-300 ${ | ||
| addedToCart === sizeOption | ||
| ? "bg-green-100 border-2 border-green-500 text-green-700" | ||
| : isReserving | ||
| ? "bg-gray-300 cursor-not-allowed text-gray-500" | ||
| : "bg-[#F4F4F4] hover:bg-[#DADADA] text-[#1E1E1E]" | ||
| }`} | ||
| whileHover={!isReserving ? { scale: 1.1 } : undefined} | ||
| whileTap={!isReserving ? { scale: 0.95 } : undefined} | ||
| > | ||
| <span>{sizeOption}</span> | ||
| {sizeCartQuantity > 0 && ( | ||
| <span className="text-xs text-[#68191E] font-medium"> | ||
| ({sizeCartQuantity}) | ||
| </span> | ||
| )} | ||
| </motion.div> | ||
| ); | ||
| })} | ||
| </motion.div> | ||
| )} | ||
| </AnimatePresence> | ||
| </motion.div> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider decomposing the ProductItem component for better maintainability.
The ProductItem component is quite large (400+ lines) and handles multiple responsibilities. Consider extracting logic into custom hooks and smaller components.
Suggested refactoring:
- Extract stock polling logic into a
useProductStockhook - Extract reservation logic into a
useProductReservationhook - Extract size selector into a separate
ProductSizeSelectorcomponent - Extract cart management into a
useProductCarthook
Example structure:
// Custom hooks
const useProductStock = (productId: string) => {
// Stock polling and management logic
};
const useProductReservation = (product: Product) => {
// Reservation logic
};
// Smaller component
const ProductSizeSelector = ({ sizes, onSelect, cartQuantities }) => {
// Size selection UI
};This would improve:
- Code organization and readability
- Unit testing capabilities
- Reusability of logic
- Component maintainability
🤖 Prompt for AI Agents
In src/app/_Home/components/products/components/ProductSection.tsx around lines
75 to 508, the ProductItem component is too large and handles multiple
responsibilities, making it hard to maintain. Refactor by extracting the stock
polling logic into a custom hook named useProductStock, the reservation logic
into useProductReservation, the size selector UI into a separate
ProductSizeSelector component, and cart management logic into useProductCart
hook. This modularization will improve code organization, readability, and
testability.
| if (Math.abs(newStock - lastKnownStock) > 1) { | ||
| console.warn( | ||
| `Significant stock change detected for ${product.title}: ${lastKnownStock} -> ${newStock}` | ||
| ); | ||
| toast.info(`Stock updated for ${product.title}`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove console statements and improve stock change detection.
Console statements should be removed from production code. Also, the stock comparison might have floating-point precision issues.
- // Check if stock changed significantly (admin intervention)
- if (Math.abs(newStock - lastKnownStock) > 1) {
- console.warn(
- `Significant stock change detected for ${product.title}: ${lastKnownStock} -> ${newStock}`
- );
+ // Check if stock changed significantly (admin intervention)
+ const stockDifference = Math.abs(newStock - lastKnownStock);
+ if (stockDifference > 1 && Number.isInteger(stockDifference)) {
toast.info(`Stock updated for ${product.title}`);
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/_Home/components/products/components/ProductSection.tsx around lines
154 to 159, remove the console.warn statement to avoid logging in production.
Replace the stock change detection logic to handle floating-point precision
issues by using a method like comparing the absolute difference against a small
epsilon value instead of a fixed threshold of 1. Keep the toast.info
notification as is.
| if (reservedByCurrentUser > 0 || reservedByOthers > 0) { | ||
| console.log( | ||
| `${product.title}: You reserved ${reservedByCurrentUser}, Others reserved ${reservedByOthers}, Available: ${availableStock}` | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove debug console.log statement.
Debug logging should not be present in production code.
- // Log reservation info for debugging
- if (reservedByCurrentUser > 0 || reservedByOthers > 0) {
- console.log(
- `${product.title}: You reserved ${reservedByCurrentUser}, Others reserved ${reservedByOthers}, Available: ${availableStock}`
- );
- }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/_Home/components/products/components/ProductSection.tsx between lines
175 and 179, remove the console.log statement used for debugging that outputs
reservation details. This debug logging should not be present in production
code, so simply delete the entire console.log call to clean up the code.
| // Initial fetch | ||
| fetchStock(); | ||
|
|
||
| // Also fetch after a short delay to catch any recent changes | ||
| const immediateRefresh = setTimeout(fetchStock, 2000); | ||
|
|
||
| // Poll every 5 seconds for faster responsiveness | ||
| const interval = setInterval(fetchStock, 5000); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider optimizing the polling strategy to reduce API calls.
Multiple polling mechanisms (initial fetch, 2s delay, 5s interval) combined with event-based refetches could lead to excessive API calls, especially with multiple product cards.
Consider implementing:
- A shared polling context to coordinate fetches across multiple ProductItem instances
- Debouncing for event-based refetches
- Exponential backoff for failed requests
- Rate limiting to prevent API overload
Example debounced fetch:
const debouncedFetchStock = useMemo(
() => debounce(fetchStock, 1000),
[fetchStock]
);🤖 Prompt for AI Agents
In src/app/_Home/components/products/components/ProductSection.tsx around lines
186 to 194, the current polling strategy uses multiple timers causing excessive
API calls when multiple product cards are rendered. To fix this, refactor to
implement a shared polling context that coordinates fetchStock calls across all
ProductItem instances, add debouncing to event-based fetches to limit rapid
repeated calls, introduce exponential backoff logic for handling failed fetch
attempts, and apply rate limiting to prevent API overload. This will optimize
API usage and improve performance.
| const loadAnalyticsData = useCallback(async () => { | ||
| try { | ||
| setLoading(true); | ||
| const params = new URLSearchParams(); | ||
| params.append("range", dateRange); | ||
| if (dateRange === "custom" && customRange.start && customRange.end) { | ||
| params.append("start", customRange.start); | ||
| params.append("end", customRange.end); | ||
| } | ||
|
|
||
| const [revenueRes, ordersRes, deliveryRes, customersRes] = | ||
| await Promise.all([ | ||
| fetch(`/api/admin/analytics/revenue?${params}`), | ||
| fetch(`/api/admin/analytics/orders?${params}`), | ||
| fetch(`/api/admin/analytics/delivery?${params}`), | ||
| fetch(`/api/admin/analytics/customers?${params}`), | ||
| ]); | ||
|
|
||
| // Check each response individually and handle errors gracefully | ||
| const responses = [ | ||
| { name: "revenue", response: revenueRes }, | ||
| { name: "orders", response: ordersRes }, | ||
| { name: "delivery", response: deliveryRes }, | ||
| { name: "customers", response: customersRes }, | ||
| ]; | ||
|
|
||
| const errors = []; | ||
| for (const { name, response } of responses) { | ||
| if (!response.ok) { | ||
| errors.push(`${name}: ${response.status} ${response.statusText}`); | ||
| } | ||
| } | ||
|
|
||
| if (errors.length > 0) { | ||
| console.error("Analytics API errors:", errors); | ||
| toast.error(`Failed to load some analytics data: ${errors.join(", ")}`); | ||
| // Continue with partial data if some endpoints work | ||
| } | ||
|
|
||
| // Parse responses with error handling | ||
| const parseResponse = async (response: Response, name: string) => { | ||
| try { | ||
| if (response.ok) { | ||
| return await response.json(); | ||
| } else { | ||
| console.error( | ||
| `${name} API failed:`, | ||
| response.status, | ||
| response.statusText | ||
| ); | ||
| return null; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Failed to parse ${name} response:`, error); | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const [revenueData, ordersData, deliveryData, customersData] = | ||
| await Promise.all([ | ||
| parseResponse(revenueRes, "revenue"), | ||
| parseResponse(ordersRes, "orders"), | ||
| parseResponse(deliveryRes, "delivery"), | ||
| parseResponse(customersRes, "customers"), | ||
| ]); | ||
|
|
||
| // Set data with fallbacks for failed requests | ||
| setData({ | ||
| revenue: revenueData, | ||
| orders: ordersData, | ||
| delivery: deliveryData, | ||
| customers: customersData, | ||
| }); | ||
| } catch (error) { | ||
| console.error("Failed to load analytics:", error); | ||
| toast.error("Failed to load analytics data"); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }, [dateRange, customRange]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add request cancellation to prevent memory leaks
The parallel fetch requests should be cancellable to prevent state updates after component unmount.
const loadAnalyticsData = useCallback(async () => {
+ const abortController = new AbortController();
+
try {
setLoading(true);
const params = new URLSearchParams();
params.append("range", dateRange);
if (dateRange === "custom" && customRange.start && customRange.end) {
params.append("start", customRange.start);
params.append("end", customRange.end);
}
const [revenueRes, ordersRes, deliveryRes, customersRes] =
await Promise.all([
- fetch(`/api/admin/analytics/revenue?${params}`),
- fetch(`/api/admin/analytics/orders?${params}`),
- fetch(`/api/admin/analytics/delivery?${params}`),
- fetch(`/api/admin/analytics/customers?${params}`),
+ fetch(`/api/admin/analytics/revenue?${params}`, { signal: abortController.signal }),
+ fetch(`/api/admin/analytics/orders?${params}`, { signal: abortController.signal }),
+ fetch(`/api/admin/analytics/delivery?${params}`, { signal: abortController.signal }),
+ fetch(`/api/admin/analytics/customers?${params}`, { signal: abortController.signal }),
]);Also return the abort controller so it can be called on cleanup.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const loadAnalyticsData = useCallback(async () => { | |
| try { | |
| setLoading(true); | |
| const params = new URLSearchParams(); | |
| params.append("range", dateRange); | |
| if (dateRange === "custom" && customRange.start && customRange.end) { | |
| params.append("start", customRange.start); | |
| params.append("end", customRange.end); | |
| } | |
| const [revenueRes, ordersRes, deliveryRes, customersRes] = | |
| await Promise.all([ | |
| fetch(`/api/admin/analytics/revenue?${params}`), | |
| fetch(`/api/admin/analytics/orders?${params}`), | |
| fetch(`/api/admin/analytics/delivery?${params}`), | |
| fetch(`/api/admin/analytics/customers?${params}`), | |
| ]); | |
| // Check each response individually and handle errors gracefully | |
| const responses = [ | |
| { name: "revenue", response: revenueRes }, | |
| { name: "orders", response: ordersRes }, | |
| { name: "delivery", response: deliveryRes }, | |
| { name: "customers", response: customersRes }, | |
| ]; | |
| const errors = []; | |
| for (const { name, response } of responses) { | |
| if (!response.ok) { | |
| errors.push(`${name}: ${response.status} ${response.statusText}`); | |
| } | |
| } | |
| if (errors.length > 0) { | |
| console.error("Analytics API errors:", errors); | |
| toast.error(`Failed to load some analytics data: ${errors.join(", ")}`); | |
| // Continue with partial data if some endpoints work | |
| } | |
| // Parse responses with error handling | |
| const parseResponse = async (response: Response, name: string) => { | |
| try { | |
| if (response.ok) { | |
| return await response.json(); | |
| } else { | |
| console.error( | |
| `${name} API failed:`, | |
| response.status, | |
| response.statusText | |
| ); | |
| return null; | |
| } | |
| } catch (error) { | |
| console.error(`Failed to parse ${name} response:`, error); | |
| return null; | |
| } | |
| }; | |
| const [revenueData, ordersData, deliveryData, customersData] = | |
| await Promise.all([ | |
| parseResponse(revenueRes, "revenue"), | |
| parseResponse(ordersRes, "orders"), | |
| parseResponse(deliveryRes, "delivery"), | |
| parseResponse(customersRes, "customers"), | |
| ]); | |
| // Set data with fallbacks for failed requests | |
| setData({ | |
| revenue: revenueData, | |
| orders: ordersData, | |
| delivery: deliveryData, | |
| customers: customersData, | |
| }); | |
| } catch (error) { | |
| console.error("Failed to load analytics:", error); | |
| toast.error("Failed to load analytics data"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [dateRange, customRange]); | |
| const loadAnalyticsData = useCallback(async () => { | |
| const abortController = new AbortController(); | |
| try { | |
| setLoading(true); | |
| const params = new URLSearchParams(); | |
| params.append("range", dateRange); | |
| if (dateRange === "custom" && customRange.start && customRange.end) { | |
| params.append("start", customRange.start); | |
| params.append("end", customRange.end); | |
| } | |
| const [revenueRes, ordersRes, deliveryRes, customersRes] = | |
| await Promise.all([ | |
| fetch(`/api/admin/analytics/revenue?${params}`, { signal: abortController.signal }), | |
| fetch(`/api/admin/analytics/orders?${params}`, { signal: abortController.signal }), | |
| fetch(`/api/admin/analytics/delivery?${params}`, { signal: abortController.signal }), | |
| fetch(`/api/admin/analytics/customers?${params}`, { signal: abortController.signal }), | |
| ]); | |
| // Check each response individually and handle errors gracefully | |
| const responses = [ | |
| { name: "revenue", response: revenueRes }, | |
| { name: "orders", response: ordersRes }, | |
| { name: "delivery", response: deliveryRes }, | |
| { name: "customers", response: customersRes }, | |
| ]; | |
| const errors = []; | |
| for (const { name, response } of responses) { | |
| if (!response.ok) { | |
| errors.push(`${name}: ${response.status} ${response.statusText}`); | |
| } | |
| } | |
| if (errors.length > 0) { | |
| console.error("Analytics API errors:", errors); | |
| toast.error(`Failed to load some analytics data: ${errors.join(", ")}`); | |
| // Continue with partial data if some endpoints work | |
| } | |
| // Parse responses with error handling | |
| const parseResponse = async (response: Response, name: string) => { | |
| try { | |
| if (response.ok) { | |
| return await response.json(); | |
| } else { | |
| console.error( | |
| `${name} API failed:`, | |
| response.status, | |
| response.statusText | |
| ); | |
| return null; | |
| } | |
| } catch (error) { | |
| console.error(`Failed to parse ${name} response:`, error); | |
| return null; | |
| } | |
| }; | |
| const [revenueData, ordersData, deliveryData, customersData] = | |
| await Promise.all([ | |
| parseResponse(revenueRes, "revenue"), | |
| parseResponse(ordersRes, "orders"), | |
| parseResponse(deliveryRes, "delivery"), | |
| parseResponse(customersRes, "customers"), | |
| ]); | |
| // Set data with fallbacks for failed requests | |
| setData({ | |
| revenue: revenueData, | |
| orders: ordersData, | |
| delivery: deliveryData, | |
| customers: customersData, | |
| }); | |
| } catch (error) { | |
| console.error("Failed to load analytics:", error); | |
| toast.error("Failed to load analytics data"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }, [dateRange, customRange]); |
🤖 Prompt for AI Agents
In src/app/admin/analytics/page.tsx around lines 71 to 150, the fetch requests
in loadAnalyticsData lack cancellation support, risking memory leaks if the
component unmounts before completion. To fix this, create an AbortController
instance before the fetch calls, pass its signal to each fetch request, and
return a cleanup function from useCallback that calls abort() on the controller.
This ensures requests are cancelled on unmount, preventing state updates after
unmount.
| const transporter = nodemailer.createTransport({ | ||
| service: "gmail", | ||
| auth: { | ||
| user: process.env.EMAIL_USER, | ||
| pass: process.env.EMAIL_PASS, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add environment variable validation
The transporter creation should validate that required environment variables exist to fail fast with clear error messages.
+if (!process.env.EMAIL_USER || !process.env.EMAIL_PASS) {
+ throw new Error('Email configuration missing: EMAIL_USER and EMAIL_PASS environment variables are required');
+}
+
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const transporter = nodemailer.createTransport({ | |
| service: "gmail", | |
| auth: { | |
| user: process.env.EMAIL_USER, | |
| pass: process.env.EMAIL_PASS, | |
| }, | |
| }); | |
| if (!process.env.EMAIL_USER || !process.env.EMAIL_PASS) { | |
| throw new Error( | |
| 'Email configuration missing: EMAIL_USER and EMAIL_PASS environment variables are required' | |
| ); | |
| } | |
| const transporter = nodemailer.createTransport({ | |
| service: "gmail", | |
| auth: { | |
| user: process.env.EMAIL_USER, | |
| pass: process.env.EMAIL_PASS, | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In src/app/api/newsletter/route.ts around lines 11 to 17, the code creates a
nodemailer transporter using environment variables without validating their
presence. Add checks before transporter creation to verify that
process.env.EMAIL_USER and process.env.EMAIL_PASS are defined. If either is
missing, throw an error with a clear message indicating which environment
variable is not set, so the application fails fast and provides helpful
feedback.
| export async function POST(req: NextRequest) { | ||
| // Rate limiting logic | ||
| const ip = getClientIp(req); | ||
| const now = Date.now(); | ||
| let entry = newsletterAttempts.get(ip); | ||
| if (!entry || now - entry.firstAttempt > WINDOW_MS) { | ||
| entry = { count: 0, firstAttempt: now }; | ||
| } | ||
| entry.count++; | ||
| newsletterAttempts.set(ip, entry); | ||
| if (entry.count > MAX_SUBSCRIPTIONS) { | ||
| return NextResponse.json( | ||
| { error: "Too many newsletter subscriptions. Please try again later." }, | ||
| { status: 429 } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memory leak: Rate limit entries are never cleaned up
The newsletterAttempts Map will grow indefinitely as entries are never removed, causing a memory leak. Old entries should be periodically cleaned up.
Consider implementing a cleanup mechanism:
+// Periodically clean up old entries
+setInterval(() => {
+ const now = Date.now();
+ for (const [key, entry] of newsletterAttempts.entries()) {
+ if (now - entry.firstAttempt > WINDOW_MS) {
+ newsletterAttempts.delete(key);
+ }
+ }
+}, WINDOW_MS); // Run cleanup every window period
+
export async function POST(req: NextRequest) {
// Rate limiting logic
const ip = getClientIp(req);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function POST(req: NextRequest) { | |
| // Rate limiting logic | |
| const ip = getClientIp(req); | |
| const now = Date.now(); | |
| let entry = newsletterAttempts.get(ip); | |
| if (!entry || now - entry.firstAttempt > WINDOW_MS) { | |
| entry = { count: 0, firstAttempt: now }; | |
| } | |
| entry.count++; | |
| newsletterAttempts.set(ip, entry); | |
| if (entry.count > MAX_SUBSCRIPTIONS) { | |
| return NextResponse.json( | |
| { error: "Too many newsletter subscriptions. Please try again later." }, | |
| { status: 429 } | |
| ); | |
| } | |
| // Periodically clean up old entries | |
| setInterval(() => { | |
| const now = Date.now(); | |
| for (const [key, entry] of newsletterAttempts.entries()) { | |
| if (now - entry.firstAttempt > WINDOW_MS) { | |
| newsletterAttempts.delete(key); | |
| } | |
| } | |
| }, WINDOW_MS); // Run cleanup every window period | |
| export async function POST(req: NextRequest) { | |
| // Rate limiting logic | |
| const ip = getClientIp(req); | |
| const now = Date.now(); | |
| let entry = newsletterAttempts.get(ip); | |
| if (!entry || now - entry.firstAttempt > WINDOW_MS) { | |
| entry = { count: 0, firstAttempt: now }; | |
| } | |
| entry.count++; | |
| newsletterAttempts.set(ip, entry); | |
| if (entry.count > MAX_SUBSCRIPTIONS) { | |
| return NextResponse.json( | |
| { error: "Too many newsletter subscriptions. Please try again later." }, | |
| { status: 429 } | |
| ); | |
| } | |
| // …rest of handler… | |
| } |
🤖 Prompt for AI Agents
In src/app/api/newsletter/route.ts between lines 27 and 42, the
newsletterAttempts Map grows indefinitely because old rate limit entries are
never removed, causing a memory leak. To fix this, implement a cleanup mechanism
that removes entries from newsletterAttempts when their firstAttempt timestamp
is older than the WINDOW_MS duration before adding or updating entries. This can
be done by iterating over the Map and deleting stale entries to keep memory
usage bounded.
| // In-memory rate limiting per session or IP | ||
| const orderAttempts = new Map(); // key: sessionId or IP, value: { count, firstAttempt } | ||
| const MAX_ORDERS_PER_SESSION = 3; | ||
| const MAX_ORDERS_PER_IP = 100; | ||
| const WINDOW_MS = 30 * 60 * 1000; // 30 minutes | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memory leak: Rate limit entries are never cleaned up
Similar to the newsletter route, the orderAttempts Map will grow indefinitely. Implement periodic cleanup of expired entries.
Add a cleanup mechanism:
setInterval(() => {
const now = Date.now();
for (const [key, entry] of orderAttempts.entries()) {
if (now - entry.firstAttempt > WINDOW_MS) {
orderAttempts.delete(key);
}
}
}, WINDOW_MS);🤖 Prompt for AI Agents
In src/app/api/orders/route.ts around lines 9 to 14, the orderAttempts Map used
for rate limiting grows indefinitely, causing a memory leak. Fix this by adding
a setInterval cleanup function that runs every WINDOW_MS milliseconds, iterating
over orderAttempts entries and deleting those whose firstAttempt timestamp is
older than WINDOW_MS to remove expired entries and prevent unbounded memory
growth.
| const address = formData.get("address") as string; | ||
| const state = formData.get("state") as string; | ||
| const message = formData.get("message") as string; | ||
| const items = JSON.parse(formData.get("items") as string); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for JSON parsing
The JSON.parse call could throw an error if the items field contains invalid JSON.
- const items = JSON.parse(formData.get("items") as string);
+ let items;
+ try {
+ items = JSON.parse(formData.get("items") as string);
+ } catch (error) {
+ return NextResponse.json(
+ { error: "Invalid items format. Expected valid JSON." },
+ { status: 400 }
+ );
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const items = JSON.parse(formData.get("items") as string); | |
| // … earlier validation logic … | |
| let items; | |
| try { | |
| items = JSON.parse(formData.get("items") as string); | |
| } catch (error) { | |
| return NextResponse.json( | |
| { error: "Invalid items format. Expected valid JSON." }, | |
| { status: 400 } | |
| ); | |
| } | |
| // … continue processing items … |
🤖 Prompt for AI Agents
In src/app/api/orders/route.ts at line 71, the JSON.parse call on
formData.get("items") can throw an error if the input is invalid JSON. Wrap the
JSON.parse call in a try-catch block to catch any parsing errors. In the catch
block, handle the error appropriately by returning a response indicating invalid
input or throwing a controlled error to prevent the server from crashing.
| return [ | ||
| "payment_pending", | ||
| "payment_confirmed", | ||
| "shipped", | ||
| "complete", | ||
| ].includes(status); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing "canceled" status in validation
The status validation is missing "canceled" which is handled in the order update endpoint. This inconsistency could cause issues.
return [
"payment_pending",
"payment_confirmed",
"shipped",
"complete",
+ "canceled",
].includes(status);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return [ | |
| "payment_pending", | |
| "payment_confirmed", | |
| "shipped", | |
| "complete", | |
| ].includes(status); | |
| return [ | |
| "payment_pending", | |
| "payment_confirmed", | |
| "shipped", | |
| "complete", | |
| "canceled", | |
| ].includes(status); |
🤖 Prompt for AI Agents
In src/app/api/orders/route.ts around lines 214 to 219, the status validation
array is missing the "canceled" status, which is handled elsewhere in the order
update endpoint. To fix this, add "canceled" to the list of valid statuses in
the includes check to ensure consistency and proper validation of all handled
statuses.
|
hi @CijeTheCreator can you resolve the conflicts? |
|
I'll look into it ASAP |
|
My phone full system app changed .how . Please help me |
|
???
…On Wed, Sep 10, 2025 at 6:31 PM bbdr51646-lab ***@***.***> wrote:
*bbdr51646-lab* left a comment (himspired1/himspired#57)
<#57 (comment)>
My phone full system app changed .how . Please help me
—
Reply to this email directly, view it on GitHub
<#57 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/A25EILU4DSK55B3ATQNRQAT3SBOAHAVCNFSM6AAAAACDLXLZ4CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTENZVHA4DAMZRGI>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Update Shop Categories from Brand-Based to Product-Type-Based
Summary
This PR transforms the shop categorization system from brand-based categories (THRIFT, LUXURY, SENATORS) to product-type-based categories (POLOS, T-SHIRTS, JEANS, SHIRTS), focusing the entire shopping experience on thrift products organized by clothing type.
Changes Made
🛍️ Shop Navigation Component
File:
src/components/pages/shop/shop-navigation-bar.component.tsx["ALL", "THRIFT", "LUXURY", "SENATORS"]→["ALL", "POLOS", "T-SHIRTS", "JEANS", "SHIRTS"]🏠 Home Products Carousel
File:
src/app/_Home/components/Products.tsx📦 Products Data Structure
File:
src/data/products.tspolosProducts- Classic and premium polo shirtstshirtsProducts- Basic tees, logo tees, vintage and graphic stylesjeansProducts- Slim fit, straight cut, distressed, and bootcut stylesshirtsProducts- Dress shirts, casual shirts, flannel, and oxford stylesTechnical Details
Category Mapping
category === "polos"category === "t-shirts"category === "jeans"category === "shirts"Data Architecture
Affected Components
Testing Checklist
Commits
Breaking Changes: None (backward compatibility maintained)
Database Changes: None required
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores