├── src ├── types │ ├── icon-props.ts │ ├── Menu.ts │ ├── testimonial.ts │ ├── coupon.ts │ ├── wishlistItem.ts │ ├── countdown.ts │ ├── order.ts │ ├── category.ts │ ├── blogItem.ts │ ├── response.ts │ ├── author.ts │ ├── hero.ts │ └── product.ts ├── components │ ├── Header │ │ ├── types.ts │ │ ├── menuData.ts │ │ ├── CustomSelect.tsx │ │ ├── DropdownGroup.tsx │ │ └── DesktopMenu.tsx │ ├── Common │ │ ├── Loader.tsx │ │ ├── Newsletter │ │ │ ├── Graphics.tsx │ │ │ ├── NewsletterForm.tsx │ │ │ └── index.tsx │ │ ├── QuickViewButton.tsx │ │ ├── PreLoader.tsx │ │ ├── CartSidebarModal │ │ │ ├── EmptyCart.tsx │ │ │ ├── SingleItem.tsx │ │ │ └── index.tsx │ │ ├── ScrollToTop.tsx │ │ └── Tooltip.tsx │ ├── Home │ │ ├── Countdown │ │ │ ├── index.tsx │ │ │ ├── TimeDisplay.tsx │ │ │ ├── CountdownTimer.tsx │ │ │ └── CountdownBanner.tsx │ │ ├── Categories │ │ │ ├── index.tsx │ │ │ ├── SingleItem.tsx │ │ │ ├── categoryData.ts │ │ │ └── CategoryCarouselArea.tsx │ │ ├── BestSeller │ │ │ ├── BestSellerSectionTitle.tsx │ │ │ ├── index.tsx │ │ │ └── ActionBtn.tsx │ │ ├── Testimonials │ │ │ ├── testimonialsData.ts │ │ │ ├── useTestimonialSwiper.ts │ │ │ ├── TestimonialsHeader.tsx │ │ │ └── index.tsx │ │ ├── NewArrivals │ │ │ ├── NewArrivalTitle.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── Hero │ │ │ ├── index.tsx │ │ │ ├── FooterFeature.tsx │ │ │ ├── HeroBannerItem.tsx │ │ │ └── HeroCarousel.tsx │ │ └── PromoBanner │ │ │ ├── LargePromoBanner.tsx │ │ │ ├── SmallPromoBanner.tsx │ │ │ └── index.tsx │ ├── Providers │ │ ├── CartProvider.tsx │ │ └── CartHydration.tsx │ ├── Shop │ │ ├── CheckoutBtn.tsx │ │ ├── ReviewStar.tsx │ │ └── shopData.ts │ ├── ui │ │ └── input │ │ │ ├── radio.tsx │ │ │ ├── index.tsx │ │ │ └── TagInput.tsx │ ├── Footer │ │ ├── AccountLinks.tsx │ │ ├── FooterSocials.tsx │ │ ├── QuickLinks.tsx │ │ ├── FooterAbout.tsx │ │ ├── FooterBottom.tsx │ │ └── icons.tsx │ ├── Wishlist │ │ ├── AddWishlistButton.tsx │ │ └── icons.tsx │ └── Error │ │ └── index.tsx ├── app │ ├── (site) │ │ ├── page.tsx │ │ ├── Providers.tsx │ │ └── layout.tsx │ ├── api │ │ └── review │ │ │ └── route.ts │ ├── context │ │ ├── PreviewSliderContext.tsx │ │ └── QuickViewModalContext.tsx │ ├── not-found.tsx │ └── layout.tsx ├── utils │ ├── cn.ts │ ├── dateUtils.ts │ ├── calculateDiscountPercentage.ts │ ├── formatePrice.ts │ ├── formateDateString.ts │ ├── slugGenerate.ts │ ├── sendResponse.ts │ └── confirmDialog.ts ├── proxy.ts ├── redux │ ├── provider.tsx │ ├── features │ │ ├── product-details.ts │ │ ├── quickView-slice.ts │ │ └── wishlist-slice.ts │ └── store.ts ├── lib │ ├── validateEmai.ts │ ├── prismaDB.ts │ ├── response.ts │ └── cartStorage.ts ├── get-api-data │ ├── header-setting.ts │ ├── countdown.ts │ ├── reviews.ts │ ├── category.ts │ ├── seo-setting.ts │ └── hero.ts └── hooks │ └── useCart.ts ├── public ├── favicon.ico ├── images │ ├── avatar.jpeg │ ├── blog │ │ ├── blog-1.png │ │ ├── blog-2.png │ │ ├── blog-3.png │ │ ├── blog-4.png │ │ ├── blog-5.png │ │ ├── blog-6.png │ │ ├── blog-7.png │ │ └── blog-8.png │ ├── cart │ │ ├── cart-01.png │ │ ├── cart-02.png │ │ └── cart-03.png │ ├── hero │ │ ├── bannar-1.png │ │ ├── bannar-2.png │ │ ├── hero-01.png │ │ ├── hero-02.png │ │ ├── hero-03.png │ │ ├── hero-bg.png │ │ ├── slider-1.png │ │ ├── slider-2.png │ │ └── slider-3.png │ ├── users │ │ ├── user-01.jpg │ │ ├── user-02.jpg │ │ ├── user-03.jpg │ │ └── user-04.jpg │ ├── promo │ │ ├── promo-01.png │ │ ├── promo-02.png │ │ └── promo-03.png │ ├── countdown │ │ ├── speaker.png │ │ ├── countdown-01.png │ │ └── countdown-bg.png │ ├── products │ │ ├── image 156.png │ │ ├── product-1-bg-1.png │ │ ├── product-1-bg-2.png │ │ ├── product-1-sm-1.png │ │ ├── product-1-sm-2.png │ │ ├── product-2-bg-1.png │ │ ├── product-2-bg-2.png │ │ ├── product-2-sm-1.png │ │ ├── product-2-sm-2.png │ │ ├── product-3-bg-1.png │ │ ├── product-3-bg-2.png │ │ ├── product-3-sm-1.png │ │ ├── product-3-sm-2.png │ │ ├── product-4-bg-1.png │ │ ├── product-4-bg-2.png │ │ ├── product-4-sm-1.png │ │ ├── product-4-sm-2.png │ │ ├── product-5-bg-1.png │ │ ├── product-5-bg-2.png │ │ ├── product-5-sm-1.png │ │ ├── product-5-sm-2.png │ │ ├── product-6-bg-1.png │ │ ├── product-6-bg-2.png │ │ ├── product-6-sm-1.png │ │ ├── product-6-sm-2.png │ │ ├── product-7-bg-1.png │ │ ├── product-7-bg-2.png │ │ ├── product-7-sm-1.png │ │ ├── product-7-sm-2.png │ │ ├── product-8-bg-1.png │ │ ├── product-8-sm-1.png │ │ ├── product-8-sm-2.png │ │ ├── product-9-bg-1.png │ │ ├── product-9-bg-2.png │ │ ├── product-9-sm-1.png │ │ ├── product-9-sm-2.png │ │ ├── product-10-bg-1.png │ │ ├── product-10-bg-2.png │ │ ├── product-11-bg-1.png │ │ ├── product-11-bg-2.png │ │ ├── product-12-bg-1.png │ │ └── product-12-bg-2.png │ ├── quickview │ │ ├── thumb-01.png │ │ ├── thumb-02.png │ │ ├── thumb-03.png │ │ ├── thumb-04.png │ │ ├── thumb-05.png │ │ ├── thumb-06.png │ │ ├── thumb-07.png │ │ ├── quick-view-01.png │ │ ├── quick-view-02.png │ │ ├── quick-view-03.png │ │ ├── quick-view-04.png │ │ ├── quick-view-05.png │ │ ├── quick-view-06.png │ │ ├── quick-view-07.png │ │ ├── quick-view-big-03.png │ │ ├── quick-view-big-04.png │ │ ├── quick-view-big-05.png │ │ ├── quick-view-big-06.png │ │ └── quick-view-big-07.png │ ├── sellers │ │ ├── sellers-01.png │ │ ├── sellers-02.png │ │ ├── sellers-03.png │ │ ├── sellers-04.png │ │ ├── sellers-05.png │ │ └── sellers-06.png │ ├── arrivals │ │ ├── arrivals-01.png │ │ ├── arrivals-02.png │ │ ├── arrivals-03.png │ │ ├── arrivals-04.png │ │ ├── arrivals-05.png │ │ ├── arrivals-06.png │ │ ├── arrivals-07.png │ │ ├── arrivals-08.png │ │ ├── arrivals-09.png │ │ ├── arrivals-10.png │ │ ├── arrivals-11.png │ │ └── arrivals-12.png │ ├── shapes │ │ └── newsletter-bg.jpg │ ├── categories │ │ ├── categories-01.png │ │ ├── categories-02.png │ │ ├── categories-03.png │ │ ├── categories-04.png │ │ ├── categories-05.png │ │ ├── categories-06.png │ │ └── categories-07.png │ ├── 404.svg │ ├── checkout │ │ ├── cash.svg │ │ ├── stripe.svg │ │ ├── fedex.svg │ │ ├── bank.svg │ │ └── dhl.svg │ ├── icons │ │ ├── icon-star.svg │ │ ├── icon-06.svg │ │ ├── icon-02.svg │ │ ├── icon-04.svg │ │ ├── icon-08.svg │ │ ├── icon-07.svg │ │ ├── icon-03.svg │ │ └── icon-05.svg │ ├── payment │ │ ├── payment-01.svg │ │ ├── payment-04.svg │ │ ├── payment-02.svg │ │ └── payment-05.svg │ └── logo │ │ └── logo-icon.svg ├── vercel.svg └── next.svg ├── postcss.config.js ├── prisma └── schema.prisma ├── prisma.config.ts ├── .gitignore ├── next.config.js ├── tsconfig.json └── package.json /src/types/icon-props.ts: -------------------------------------------------------------------------------- 1 | export type IconProps = React.SVGProps; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/images/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/avatar.jpeg -------------------------------------------------------------------------------- /public/images/blog/blog-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-1.png -------------------------------------------------------------------------------- /public/images/blog/blog-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-2.png -------------------------------------------------------------------------------- /public/images/blog/blog-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-3.png -------------------------------------------------------------------------------- /public/images/blog/blog-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-4.png -------------------------------------------------------------------------------- /public/images/blog/blog-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-5.png -------------------------------------------------------------------------------- /public/images/blog/blog-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-6.png -------------------------------------------------------------------------------- /public/images/blog/blog-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-7.png -------------------------------------------------------------------------------- /public/images/blog/blog-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/blog/blog-8.png -------------------------------------------------------------------------------- /public/images/cart/cart-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/cart/cart-01.png -------------------------------------------------------------------------------- /public/images/cart/cart-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/cart/cart-02.png -------------------------------------------------------------------------------- /public/images/cart/cart-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/cart/cart-03.png -------------------------------------------------------------------------------- /public/images/hero/bannar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/bannar-1.png -------------------------------------------------------------------------------- /public/images/hero/bannar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/bannar-2.png -------------------------------------------------------------------------------- /public/images/hero/hero-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/hero-01.png -------------------------------------------------------------------------------- /public/images/hero/hero-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/hero-02.png -------------------------------------------------------------------------------- /public/images/hero/hero-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/hero-03.png -------------------------------------------------------------------------------- /public/images/hero/hero-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/hero-bg.png -------------------------------------------------------------------------------- /public/images/hero/slider-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/slider-1.png -------------------------------------------------------------------------------- /public/images/hero/slider-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/slider-2.png -------------------------------------------------------------------------------- /public/images/hero/slider-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/hero/slider-3.png -------------------------------------------------------------------------------- /public/images/users/user-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/users/user-01.jpg -------------------------------------------------------------------------------- /public/images/users/user-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/users/user-02.jpg -------------------------------------------------------------------------------- /public/images/users/user-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/users/user-03.jpg -------------------------------------------------------------------------------- /public/images/users/user-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/users/user-04.jpg -------------------------------------------------------------------------------- /public/images/promo/promo-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/promo/promo-01.png -------------------------------------------------------------------------------- /public/images/promo/promo-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/promo/promo-02.png -------------------------------------------------------------------------------- /public/images/promo/promo-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/promo/promo-03.png -------------------------------------------------------------------------------- /public/images/countdown/speaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/countdown/speaker.png -------------------------------------------------------------------------------- /public/images/products/image 156.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/image 156.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-01.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-02.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-03.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-04.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-05.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-06.png -------------------------------------------------------------------------------- /public/images/quickview/thumb-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/thumb-07.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-01.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-02.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-03.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-04.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-05.png -------------------------------------------------------------------------------- /public/images/sellers/sellers-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/sellers/sellers-06.png -------------------------------------------------------------------------------- /src/components/Header/types.ts: -------------------------------------------------------------------------------- 1 | export interface MenuItem { 2 | title: string; 3 | path?: string; 4 | submenu?: MenuItem[]; 5 | } 6 | -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-01.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-02.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-03.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-04.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-05.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-06.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-07.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-08.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-09.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-10.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-11.png -------------------------------------------------------------------------------- /public/images/arrivals/arrivals-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/arrivals/arrivals-12.png -------------------------------------------------------------------------------- /public/images/shapes/newsletter-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/shapes/newsletter-bg.jpg -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | } 8 | -------------------------------------------------------------------------------- /public/images/countdown/countdown-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/countdown/countdown-01.png -------------------------------------------------------------------------------- /public/images/countdown/countdown-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/countdown/countdown-bg.png -------------------------------------------------------------------------------- /public/images/products/product-1-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-1-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-1-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-1-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-1-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-1-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-1-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-1-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-2-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-2-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-2-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-2-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-2-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-2-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-2-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-2-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-3-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-3-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-3-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-3-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-3-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-3-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-3-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-3-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-4-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-4-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-4-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-4-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-4-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-4-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-4-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-4-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-5-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-5-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-5-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-5-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-5-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-5-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-5-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-5-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-6-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-6-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-6-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-6-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-6-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-6-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-6-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-6-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-7-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-7-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-7-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-7-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-7-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-7-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-7-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-7-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-8-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-8-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-8-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-8-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-8-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-8-sm-2.png -------------------------------------------------------------------------------- /public/images/products/product-9-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-9-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-9-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-9-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-9-sm-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-9-sm-1.png -------------------------------------------------------------------------------- /public/images/products/product-9-sm-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-9-sm-2.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-01.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-02.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-03.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-04.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-05.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-06.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-07.png -------------------------------------------------------------------------------- /src/app/(site)/page.tsx: -------------------------------------------------------------------------------- 1 | import Home from "@/components/Home"; 2 | 3 | export default async function HomePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /public/images/categories/categories-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-01.png -------------------------------------------------------------------------------- /public/images/categories/categories-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-02.png -------------------------------------------------------------------------------- /public/images/categories/categories-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-03.png -------------------------------------------------------------------------------- /public/images/categories/categories-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-04.png -------------------------------------------------------------------------------- /public/images/categories/categories-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-05.png -------------------------------------------------------------------------------- /public/images/categories/categories-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-06.png -------------------------------------------------------------------------------- /public/images/categories/categories-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/categories/categories-07.png -------------------------------------------------------------------------------- /public/images/products/product-10-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-10-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-10-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-10-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-11-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-11-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-11-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-11-bg-2.png -------------------------------------------------------------------------------- /public/images/products/product-12-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-12-bg-1.png -------------------------------------------------------------------------------- /public/images/products/product-12-bg-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/products/product-12-bg-2.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-big-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-big-03.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-big-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-big-04.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-big-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-big-05.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-big-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-big-06.png -------------------------------------------------------------------------------- /public/images/quickview/quick-view-big-07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CozyCommerce/cozycommerce-lite/HEAD/public/images/quickview/quick-view-big-07.png -------------------------------------------------------------------------------- /src/types/Menu.ts: -------------------------------------------------------------------------------- 1 | export type Menu = { 2 | id: number; 3 | title: string; 4 | path?: string; 5 | newTab: boolean; 6 | submenu?: Menu[]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/types/testimonial.ts: -------------------------------------------------------------------------------- 1 | export type Testimonial = { 2 | review: string; 3 | authorName: string; 4 | authorRole: string; 5 | authorImg: string; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export default function cn(...inputs: any) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/types/coupon.ts: -------------------------------------------------------------------------------- 1 | export type Coupon = { 2 | _id: string; 3 | name: string; 4 | code: string; 5 | discount: number; 6 | maxRedemptions: number; 7 | timesRedemed: number; 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | export function getSevenDaysFromToday() { 2 | const date = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); 3 | return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; 4 | } -------------------------------------------------------------------------------- /src/types/wishlistItem.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type WishlistItem = { 4 | id: string; 5 | title: string; 6 | slug: string; 7 | image:string; 8 | price:number; 9 | quantity: number; 10 | color?: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/types/countdown.ts: -------------------------------------------------------------------------------- 1 | export type Countdown = { 2 | _id: string; 3 | title: string; 4 | image: any; 5 | subtitle: string; 6 | date: string; 7 | product: { 8 | slug: string; 9 | name: string; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/types/order.ts: -------------------------------------------------------------------------------- 1 | 2 | export type Order = { 3 | _id: string; 4 | orderId: string; 5 | status: string; 6 | totalPrice: number; 7 | userId: string; 8 | quantity: number; 9 | orderTitle: string; 10 | createdAt: string; 11 | }; 12 | -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | export async function proxy(req: NextRequest) { 5 | 6 | return NextResponse.next(); 7 | } 8 | 9 | export const config = { 10 | matcher: [], 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/Common/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loader = () => { 4 | return ( 5 |
6 | ); 7 | }; 8 | 9 | export default Loader; 10 | -------------------------------------------------------------------------------- /src/types/category.ts: -------------------------------------------------------------------------------- 1 | type Slug = { 2 | current: string; 3 | _type: string; 4 | }; 5 | 6 | export type Category = { 7 | title: string; 8 | _id: string; 9 | image: string; 10 | slug: Slug; 11 | description?: string; 12 | productCount: number; 13 | postCount?: number; 14 | }; 15 | -------------------------------------------------------------------------------- /src/redux/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { store } from "./store"; 4 | import { Provider } from "react-redux"; 5 | import React from "react"; 6 | 7 | export function ReduxProvider({ children }: { children: React.ReactNode }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/blogItem.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type Blog = { 4 | id: string; 5 | title: string; 6 | metadata: string | null; 7 | slug: string; 8 | authorId: number; 9 | categoryId: number; 10 | tags: string[]; 11 | mainImage: string; 12 | body: string; 13 | createdAt: Date; 14 | updatedAt: Date; 15 | }; 16 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig, env } from 'prisma/config'; 3 | 4 | export default defineConfig({ 5 | schema: 'prisma/schema.prisma', 6 | migrations: { 7 | path: 'prisma/migrations' 8 | }, 9 | datasource: { 10 | url: env('DATABASE_URL'), 11 | }, 12 | }); -------------------------------------------------------------------------------- /src/lib/validateEmai.ts: -------------------------------------------------------------------------------- 1 | export const validateEmail = (email: string) => { 2 | return email.match( 3 | // eslint-disable-next-line no-useless-escape 4 | /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 5 | ); 6 | }; 7 | -------------------------------------------------------------------------------- /src/get-api-data/header-setting.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | // get all header settings 5 | export const getHeaderSettings = unstable_cache( 6 | async () => { 7 | return await prisma.headerSetting.findFirst(); 8 | }, 9 | ['header-setting'], { tags: ['header-setting'] } 10 | ); 11 | -------------------------------------------------------------------------------- /src/components/Home/Countdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCountdowns } from '@/get-api-data/countdown'; 2 | import CountdownBanner from './CountdownBanner'; 3 | 4 | const CountDown = async () => { 5 | const countdown = await getCountdowns(); 6 | 7 | return
{countdown && }
; 8 | }; 9 | 10 | export default CountDown; 11 | -------------------------------------------------------------------------------- /src/components/Providers/CartProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | function CartProvider({ children }: { children: React.ReactNode }) { 6 | // Cart state is managed by Redux, so this component is just a passthrough wrapper 7 | // Kept for backwards compatibility with the existing app structure 8 | return <>{children}; 9 | } 10 | 11 | export default CartProvider; 12 | -------------------------------------------------------------------------------- /src/utils/calculateDiscountPercentage.ts: -------------------------------------------------------------------------------- 1 | export function calculateDiscountPercentage( 2 | discountedPrice: number, 3 | originalPrice: number 4 | ): number { 5 | if (originalPrice <= 0 || discountedPrice < 0) return 0; 6 | 7 | const discount = ((originalPrice - discountedPrice) / originalPrice) * 100; 8 | return Math.round(discount); // Returns whole number like 30 for 30% 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/formatePrice.ts: -------------------------------------------------------------------------------- 1 | // formate price 2 | 3 | export const formatPrice = (price: number) => { 4 | const hasDecimals = price % 1 !== 0; 5 | 6 | return new Intl.NumberFormat("en-US", { 7 | style: "currency", 8 | currency: "USD", 9 | minimumFractionDigits: hasDecimals ? 2 : 0, 10 | maximumFractionDigits: hasDecimals ? 2 : 0, 11 | }).format(price); 12 | }; 13 | 14 | -------------------------------------------------------------------------------- /src/types/response.ts: -------------------------------------------------------------------------------- 1 | export interface IGenericErrorMessage { 2 | field?: string; 3 | message: string; 4 | } 5 | 6 | export type IGenericErrorResponse = { 7 | statusCode: number; 8 | message: string; 9 | errorMessages?: IGenericErrorMessage[]; 10 | }; 11 | 12 | export type IGenericSuccessResponse = { 13 | statusCode: number; 14 | success: boolean; 15 | message: string; 16 | data?: T; 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/Shop/CheckoutBtn.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | const CheckoutBtn = () => { 5 | return ( 6 | 10 | Checkout 11 | 12 | ); 13 | }; 14 | 15 | export default CheckoutBtn; 16 | -------------------------------------------------------------------------------- /src/utils/formateDateString.ts: -------------------------------------------------------------------------------- 1 | // utils/formatDateString.ts 2 | import dayjs from "dayjs"; 3 | import utc from "dayjs/plugin/utc"; 4 | 5 | dayjs.extend(utc); 6 | 7 | /** 8 | * Formats a UTC date string consistently across server and client 9 | * @param {string|Date} dateInput 10 | * @returns {string} 11 | */ 12 | export function formatDateString(dateInput: Date | string) { 13 | return dayjs.utc(dateInput).format("MMMM D, YYYY h:mm A"); 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/prismaDB.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | import { PrismaPg } from '@prisma/adapter-pg' 3 | 4 | const globalForPrisma = global as unknown as { 5 | prisma: PrismaClient 6 | } 7 | 8 | const adapter = new PrismaPg({ 9 | connectionString: process.env.DATABASE_URL, 10 | }) 11 | 12 | export const prisma = globalForPrisma.prisma || new PrismaClient({ 13 | adapter, 14 | }) 15 | 16 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma 17 | -------------------------------------------------------------------------------- /src/get-api-data/countdown.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | 5 | export const getCountdowns = unstable_cache( 6 | async () => { 7 | return await prisma.countdown.findMany({ 8 | orderBy: { updatedAt: "desc" }, 9 | include: { 10 | product: { 11 | select: { 12 | title: true, 13 | } 14 | } 15 | } 16 | }); 17 | }, 18 | ['countdowns'], { tags: ['countdowns'] } 19 | ); -------------------------------------------------------------------------------- /src/types/author.ts: -------------------------------------------------------------------------------- 1 | type BioBlock = { 2 | style: "normal"; 3 | _key: string; 4 | markDefs: any[]; 5 | children: BioBlockChild[]; 6 | _type: "block"; 7 | }; 8 | 9 | type BioBlockChild = { 10 | _type: "span"; 11 | text: string; 12 | }; 13 | 14 | type Slug = { current: string; _type: string }; 15 | 16 | export type Author = { 17 | name: string; 18 | image?: string; 19 | bio?: BioBlock[]; 20 | numberOfPosts: number; 21 | _id: string; 22 | slug?: Slug; 23 | description?: string; 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/slugGenerate.ts: -------------------------------------------------------------------------------- 1 | export function generateSlug(text: string): string { 2 | return text 3 | .toString() // Convert to string if not already 4 | .toLowerCase() // Convert to lowercase 5 | .trim() // Remove leading/trailing whitespace 6 | .replace(/&/g, "-") // Replace & with - 7 | .replace(/[^a-z0-9\s-]/g, "") // Remove all except alphanum, space, and hyphen 8 | .replace(/\s+/g, "-") // Replace spaces with hyphen 9 | .replace(/-+/g, "-"); // Replace multiple hyphens with single hyphen 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Common/Newsletter/Graphics.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | 3 | export default function Graphics() { 4 | return ( 5 | <> 6 | background illustration 13 |
14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Home/Countdown/TimeDisplay.tsx: -------------------------------------------------------------------------------- 1 | interface TimeDisplayProps { 2 | value: number; 3 | label: string; 4 | } 5 | 6 | const TimeDisplay = ({ value, label }: TimeDisplayProps) => ( 7 |
8 | 9 | {value < 10 ? `0${value}` : value} 10 | 11 | {label} 12 |
13 | ); 14 | 15 | export default TimeDisplay; 16 | -------------------------------------------------------------------------------- /src/components/Home/Categories/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCategories } from "@/get-api-data/category"; 2 | import CategoryCarouselArea from "./CategoryCarouselArea"; 3 | 4 | const Categories = async () => { 5 | const categories = await getCategories(); 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Categories; 17 | -------------------------------------------------------------------------------- /src/components/Shop/ReviewStar.tsx: -------------------------------------------------------------------------------- 1 | import { FullStarIcon, HalfStarIcon, EmptyStarIcon } from '@/assets/icons'; 2 | 3 | const ReviewStar = ({ avgRating }: { avgRating: number }) => { 4 | return ( 5 |
6 | {[...Array(5)].map((_, index) => { 7 | const rating = index + 1; 8 | if (avgRating >= rating) return ; 9 | if (avgRating >= rating - 0.5) return ; 10 | return ; 11 | })} 12 |
13 | ); 14 | }; 15 | 16 | export default ReviewStar; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | .cph 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | 40 | .tst 41 | .xata 42 | .xatarc 43 | 44 | 45 | .env 46 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Home/BestSeller/BestSellerSectionTitle.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | export default function BestSellerSectionTitle() { 4 | return ( 5 |
6 |
7 |

8 | Best Selling Products 9 |

10 |

11 | These top picks are flying off the shelves! Find out what everyone’s 12 | loving right now. 13 |

14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Common/QuickViewButton.tsx: -------------------------------------------------------------------------------- 1 | // components/QuickViewButton.tsx 2 | 3 | import { EyeIcon } from "@/assets/icons"; 4 | import Tooltip from "./Tooltip"; 5 | 6 | const QuickViewButton = ({ onClick }: { onClick: () => void }) => { 7 | return ( 8 | 9 | 15 | 16 | ); 17 | }; 18 | 19 | export default QuickViewButton; 20 | -------------------------------------------------------------------------------- /src/components/Common/PreLoader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState, useEffect } from "react"; 3 | 4 | const PreLoader = () => { 5 | const [loading, setLoading] = useState(true); 6 | 7 | useEffect(() => { 8 | setTimeout(() => setLoading(false), 100); 9 | }, []); 10 | 11 | return ( 12 | loading && ( 13 |
14 |
15 |
16 | ) 17 | ); 18 | }; 19 | 20 | export default PreLoader; 21 | -------------------------------------------------------------------------------- /src/components/Home/Testimonials/testimonialsData.ts: -------------------------------------------------------------------------------- 1 | const testimonialsData = [ 2 | { 3 | review: `Lorem ipsum dolor sit amet, adipiscing elit. Donec malesuada justo vitaeaugue suscipit beautiful vehicula`, 4 | authorName: 'Davis Dorwart', 5 | authorImg: '/images/users/user-01.jpg', 6 | authorRole: 'Serial Entrepreneur', 7 | }, 8 | { 9 | review: 10 | 'Lorem ipsum dolor sit amet, adipiscing elit. Donec malesuada justo vitaeaugue suscipit beautiful vehicula', 11 | authorName: 'Wilson Dias', 12 | authorImg: '/images/users/user-04.jpg', 13 | authorRole: 'Backend Developer', 14 | }, 15 | ]; 16 | 17 | export default testimonialsData; 18 | -------------------------------------------------------------------------------- /src/components/Home/NewArrivals/NewArrivalTitle.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function NewArrivalTitle() { 4 | return ( 5 |
6 |
7 |

8 | New Arrivals 9 |

10 |
11 | 15 | View All 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/get-api-data/reviews.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | export const getReviews = unstable_cache( 5 | async (productSlug: string) => { 6 | const reviews = await prisma.review.findMany({ 7 | where: { 8 | AND: [ 9 | { 10 | productSlug: productSlug, 11 | }, 12 | { 13 | isApproved: true, 14 | }, 15 | ], 16 | }, 17 | }); 18 | return { 19 | reviews, 20 | avgRating: reviews.length > 0 ? reviews.reduce((sum, review) => sum + review?.ratings, 0) / reviews.length : 0, 21 | totalRating: reviews.length, 22 | }; 23 | }, 24 | ["reviews"], 25 | { tags: ["reviews"] } 26 | ); 27 | -------------------------------------------------------------------------------- /src/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import Newsletter from "../Common/Newsletter"; 2 | import BestSeller from "./BestSeller"; 3 | import Categories from "./Categories"; 4 | import CountDown from "./Countdown"; 5 | import Hero from "./Hero"; 6 | import FooterFeature from "./Hero/FooterFeature"; 7 | import NewArrival from "./NewArrivals"; 8 | import PromoBanner from "./PromoBanner"; 9 | import Testimonials from "./Testimonials"; 10 | 11 | const Home = () => { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default Home; 28 | -------------------------------------------------------------------------------- /src/components/Common/Newsletter/NewsletterForm.tsx: -------------------------------------------------------------------------------- 1 | export default function NewsletterForm() { 2 | return ( 3 |
4 |
5 | 12 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ui/input/radio.tsx: -------------------------------------------------------------------------------- 1 | import { type HTMLProps, useId } from 'react'; 2 | 3 | type PropsType = Omit, 'label'> & { 4 | label: React.ReactNode; 5 | spacing?: number; 6 | }; 7 | 8 | export function RadioInput({ label, spacing = 0.875, ...rest }: PropsType) { 9 | const id = useId(); 10 | 11 | return ( 12 |
13 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/types/hero.ts: -------------------------------------------------------------------------------- 1 | 2 | export type IHeroSlider = { 3 | id: number; 4 | sliderName: string; 5 | sliderImage: string; 6 | discountRate: number; 7 | slug: string; 8 | productId: string; 9 | createdAt: Date; 10 | updatedAt: Date; 11 | product: { 12 | price: number; 13 | discountedPrice?: number | null; 14 | title: string; 15 | slug: string; 16 | shortDescription: string; 17 | } 18 | }; 19 | 20 | export type IHeroBanner = { 21 | id: number; 22 | bannerName: string | null; 23 | bannerImage: string; 24 | slug: string; 25 | productId: string; 26 | createdAt: Date; // Change from string to Date 27 | updatedAt: Date; // Change from string to Date 28 | product: { 29 | price: string; 30 | discountedPrice?: string | null; 31 | title: string; 32 | slug: string; 33 | } 34 | } -------------------------------------------------------------------------------- /src/components/Home/NewArrivals/index.tsx: -------------------------------------------------------------------------------- 1 | import ProductItem from "@/components/Common/ProductItem"; 2 | import NewArrivalTitle from "./NewArrivalTitle"; 3 | import { getNewArrivalsProduct } from "@/get-api-data/product"; 4 | 5 | const NewArrival = async () => { 6 | const newProducts = await getNewArrivalsProduct(); 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 | {newProducts.map((item, key) => ( 14 | 15 | ))} 16 |
17 |
18 |
19 | ); 20 | }; 21 | 22 | export default NewArrival; 23 | -------------------------------------------------------------------------------- /src/utils/sendResponse.ts: -------------------------------------------------------------------------------- 1 | import { IGenericErrorMessage } from "@/types/response"; 2 | import { NextResponse } from "next/server"; 3 | 4 | 5 | export const sendErrorResponse = ( 6 | statusCode: number, 7 | message: string, 8 | errorMessages?: IGenericErrorMessage[], 9 | success = false, 10 | ) => { 11 | return NextResponse.json( 12 | { 13 | statusCode, 14 | success, 15 | message, 16 | errorMessages: errorMessages || [], 17 | }, 18 | { status: statusCode } 19 | ); 20 | }; 21 | 22 | export const sendSuccessResponse = ( 23 | statusCode: number, 24 | message: string, 25 | data?: T 26 | ) => { 27 | return NextResponse.json( 28 | { 29 | statusCode, 30 | success: true, 31 | message, 32 | data, 33 | }, 34 | { status: statusCode } 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "lh3.googleusercontent.com", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "avatars.githubusercontent.com", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "res.cloudinary.com", 17 | }, 18 | ], 19 | }, 20 | redirects: async () => { 21 | return [ 22 | { 23 | source: "/admin", 24 | destination: "/admin/dashboard", 25 | permanent: true, 26 | }, 27 | ]; 28 | }, 29 | experimental: { 30 | serverActions: { 31 | bodySizeLimit: "3mb", 32 | }, 33 | }, 34 | }; 35 | 36 | module.exports = nextConfig; 37 | -------------------------------------------------------------------------------- /src/components/Providers/CartHydration.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import { loadCartFromStorage as loadCartFromStorageAction } from "@/redux/features/cart-slice"; 5 | import { loadCartFromStorage } from "@/lib/cartStorage"; 6 | 7 | /** 8 | * CartHydration component loads cart from localStorage after initial render 9 | * This prevents SSR hydration mismatches 10 | */ 11 | export default function CartHydration() { 12 | const dispatch = useDispatch(); 13 | 14 | useEffect(() => { 15 | // Load cart from localStorage only on client-side after hydration 16 | const savedCart = loadCartFromStorage(); 17 | if (savedCart.length > 0) { 18 | dispatch(loadCartFromStorageAction(savedCart)); 19 | } 20 | }, [dispatch]); 21 | 22 | return null; // This component doesn't render anything 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Common/CartSidebarModal/EmptyCart.tsx: -------------------------------------------------------------------------------- 1 | import { EmptyCartIcon } from "@/assets/icons"; 2 | import Link from "next/link"; 3 | import { useCart } from "@/hooks/useCart"; 4 | 5 | const EmptyCart = () => { 6 | const { handleCartClick } = useCart(); 7 | 8 | return ( 9 |
10 |
11 | 12 |
13 | 14 |

Your cart is empty!

15 | 16 | { 18 | handleCartClick(); 19 | }} 20 | href="/shop-with-sidebar" 21 | className="w-full lg:w-10/12 mx-auto flex justify-center font-medium text-white bg-dark py-[13px] px-6 rounded-lg ease-out duration-200 hover:bg-opacity-95" 22 | > 23 | Continue Shopping 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default EmptyCart; 30 | -------------------------------------------------------------------------------- /src/components/Home/Categories/SingleItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | import Image from "next/image"; 4 | import { Category } from "@prisma/client"; 5 | 6 | const SingleItem = ({ item }: { item: Category }) => { 7 | return ( 8 | 12 |
13 | {item.img && ( 14 | Category 15 | )} 16 |
17 | 18 |
19 |

20 | {item.title} 21 |

22 |
23 | 24 | ); 25 | }; 26 | 27 | export default SingleItem; 28 | -------------------------------------------------------------------------------- /src/components/Home/Testimonials/useTestimonialSwiper.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef, useState } from 'react'; 2 | import type { SwiperRef } from 'swiper/react'; 3 | 4 | export const useTestimonialSwiper = () => { 5 | const sliderRef = useRef(null); 6 | const [currentIndex, setCurrentIndex] = useState(0); 7 | 8 | const handlePrev = useCallback(() => { 9 | if (!sliderRef.current?.swiper) return; 10 | sliderRef.current.swiper.slidePrev(); 11 | }, []); 12 | 13 | const handleNext = useCallback(() => { 14 | if (!sliderRef.current?.swiper) return; 15 | sliderRef.current.swiper.slideNext(); 16 | }, []); 17 | 18 | const onSlideChange = useCallback(() => { 19 | if (sliderRef.current?.swiper) { 20 | setCurrentIndex(sliderRef.current.swiper.activeIndex); 21 | } 22 | }, []); 23 | 24 | return { 25 | sliderRef, 26 | handlePrev, 27 | handleNext, 28 | onSlideChange, 29 | currentIndex, 30 | }; 31 | }; -------------------------------------------------------------------------------- /src/get-api-data/category.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | 5 | // get all categories 6 | export const getCategories = unstable_cache( 7 | async () => { 8 | return await prisma.category.findMany({ 9 | orderBy: { updatedAt: "desc" }, 10 | }); 11 | }, 12 | ['categories'], { tags: ['categories'] } 13 | ); 14 | 15 | // GET CATEGORY BY SLUG 16 | export const getCategoryBySlug = unstable_cache( 17 | async (slug: string) => { 18 | return await prisma.category.findUnique({ 19 | where: { 20 | slug: slug 21 | } 22 | }); 23 | }, 24 | ['categories'], { tags: ['categories'] } 25 | ); 26 | 27 | // GET CATEGORY BY ID 28 | export const getCategoryById = unstable_cache( 29 | async (id: number) => { 30 | return await prisma.category.findUnique({ 31 | where: { 32 | id: id 33 | } 34 | }); 35 | }, 36 | ['categories'], { tags: ['categories'] } 37 | ); -------------------------------------------------------------------------------- /src/app/api/review/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { prisma } from "@/lib/prismaDB"; 3 | 4 | export async function POST(req: NextRequest) { 5 | const body = await req.json(); 6 | 7 | const { productSlug } = body; 8 | 9 | try { 10 | const reviews = await prisma.review.findMany({ 11 | where: { 12 | AND: [ 13 | { 14 | productSlug: productSlug, 15 | }, 16 | { 17 | isApproved: true, 18 | }, 19 | ] 20 | }, 21 | }); 22 | 23 | if (!reviews) { 24 | return NextResponse.json( 25 | { message: "No reviews found" }, 26 | { status: 200 } 27 | ); 28 | } 29 | 30 | return NextResponse.json({ review: reviews }, { status: 200 }); 31 | } catch (err) { 32 | console.error(err); 33 | 34 | return NextResponse.json( 35 | { error: "Internal Server Error" }, 36 | { status: 500 } 37 | ); 38 | } 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /src/components/Home/Categories/categoryData.ts: -------------------------------------------------------------------------------- 1 | const data = [ 2 | { 3 | title: "Televisions", 4 | id: 1, 5 | img: "/images/categories/categories-01.png", 6 | }, 7 | { 8 | title: "Laptop & PC", 9 | id: 2, 10 | img: "/images/categories/categories-02.png", 11 | }, 12 | { 13 | title: "Mobile & Tablets", 14 | id: 3, 15 | img: "/images/categories/categories-03.png", 16 | }, 17 | { 18 | title: "Games & Videos", 19 | id: 4, 20 | img: "/images/categories/categories-04.png", 21 | }, 22 | { 23 | title: "Home Appliances", 24 | id: 5, 25 | img: "/images/categories/categories-05.png", 26 | }, 27 | { 28 | title: "Health & Sports", 29 | id: 6, 30 | img: "/images/categories/categories-06.png", 31 | }, 32 | { 33 | title: "Watches", 34 | id: 7, 35 | img: "/images/categories/categories-07.png", 36 | }, 37 | { 38 | title: "Televisions", 39 | id: 8, 40 | img: "/images/categories/categories-04.png", 41 | }, 42 | ]; 43 | 44 | export default data; 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "react-jsx", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | "tailwind.config.js", 37 | "reindex.js", 38 | "src/algolia/reindex.ts", 39 | "src/algolia/reindex.ts", 40 | "scripts/import-demo-data.js", 41 | ".next/dev/types/**/*.ts" 42 | ], 43 | "exclude": [ 44 | "node_modules" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Footer/AccountLinks.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const accountLinks = [ 4 | { 5 | id: 1, 6 | label: "Login / Register", 7 | href: "/signin", 8 | }, 9 | { 10 | id: 2, 11 | label: "Cart", 12 | href: "/cart", 13 | }, 14 | { 15 | id: 3, 16 | label: "Wishlist", 17 | href: "/wishlist", 18 | }, 19 | { 20 | id: 4, 21 | label: "Shop", 22 | href: "/shop-with-sidebar", 23 | }, 24 | ]; 25 | export default function AccountLinks() { 26 | return ( 27 |
28 |

Account

29 | 30 |
    31 | {accountLinks.map((link) => ( 32 |
  • 33 | 37 | {link.label} 38 | 39 |
  • 40 | ))} 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /public/images/404.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/redux/features/product-details.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { Product } from "@/types/product"; 3 | 4 | type InitialState = { 5 | value: Product; 6 | }; 7 | 8 | const initialState = { 9 | value: { 10 | _id: 0, 11 | name: "", 12 | reviews: [""], 13 | price: 0, 14 | discountedPrice: 0, 15 | category: "", 16 | tags: [""], 17 | description: [], 18 | shortDescription: "", 19 | colors: [""], 20 | thumbnails: [""], 21 | previewImages: [""], 22 | additionalInformation: {}, 23 | customAttributes: {}, 24 | status: true, 25 | offers: [""], 26 | } as unknown as Product, 27 | } as InitialState; 28 | 29 | export const productDetails = createSlice({ 30 | name: "productDetails", 31 | initialState, 32 | reducers: { 33 | updateproductDetails: (_, action) => { 34 | return { 35 | value: { 36 | ...action.payload, 37 | }, 38 | }; 39 | }, 40 | }, 41 | }); 42 | 43 | export const { updateproductDetails } = productDetails.actions; 44 | export default productDetails.reducer; 45 | -------------------------------------------------------------------------------- /src/app/(site)/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ModalProvider } from "../context/QuickViewModalContext"; 3 | import { ReduxProvider } from "@/redux/provider"; 4 | import QuickViewModal from "@/components/Common/QuickViewModal"; 5 | import CartSidebarModal from "@/components/Common/CartSidebarModal"; 6 | import { PreviewSliderProvider } from "../context/PreviewSliderContext"; 7 | import PreviewSliderModal from "@/components/Common/PreviewSlider"; 8 | import CartProvider from "@/components/Providers/CartProvider"; 9 | import CartHydration from "@/components/Providers/CartHydration"; 10 | 11 | 12 | const Providers = ({ children }: { children: React.ReactNode }) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | {children} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default Providers; 31 | -------------------------------------------------------------------------------- /src/components/Footer/FooterSocials.tsx: -------------------------------------------------------------------------------- 1 | import { FacebookIcon, InstagramIcon, LinkedInIcon, TwitterIcon } from '@/assets/icons/social' 2 | import Link from 'next/link' 3 | 4 | const socials = [ 5 | { 6 | id: 1, 7 | href: '#', 8 | name: 'Facebook', 9 | icon: FacebookIcon, 10 | }, 11 | { 12 | id: 2, 13 | href: '#', 14 | name: 'Twitter', 15 | icon: TwitterIcon, 16 | }, 17 | { 18 | id: 3, 19 | href: '#', 20 | name: 'Instagram', 21 | icon: InstagramIcon, 22 | }, 23 | { 24 | id: 4, 25 | href: '#', 26 | name: 'LinkedIn', 27 | icon: LinkedInIcon, 28 | }, 29 | ] 30 | 31 | export default function FooterSocials() { 32 | return ( 33 |
34 | { 35 | socials.map((social) => ( 36 | 41 | {social.name} link 42 | 43 | 44 | ))} 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/response.ts: -------------------------------------------------------------------------------- 1 | 2 | // Generic error message interface (assuming this exists elsewhere) 3 | interface IGenericErrorMessage { 4 | path: string; 5 | message: string; 6 | } 7 | 8 | // Error response type 9 | interface ErrorResponse { 10 | statusCode: number; 11 | success: false; 12 | message: string; 13 | errorMessages: IGenericErrorMessage[]; 14 | } 15 | 16 | // Success response type 17 | interface SuccessResponse { 18 | statusCode: number; 19 | success: true; 20 | message: string; 21 | data?: T; 22 | } 23 | 24 | // Update errorResponse function 25 | export const errorResponse = ( 26 | statusCode: number, 27 | message: string, 28 | errorMessages?: IGenericErrorMessage[] 29 | ): ErrorResponse => { 30 | return { 31 | statusCode, 32 | success: false, 33 | message, 34 | errorMessages: errorMessages || [], 35 | }; 36 | }; 37 | 38 | // Update successResponse function 39 | export const successResponse = ( 40 | statusCode: number, 41 | message: string, 42 | data?: T 43 | ): SuccessResponse => { 44 | return { 45 | statusCode, 46 | success: true, 47 | message, 48 | data, 49 | }; 50 | }; -------------------------------------------------------------------------------- /src/components/Footer/QuickLinks.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | const quickLinks = [ 4 | { 5 | id: 1, 6 | label: "Privacy Policy", 7 | href: "/privacy-policy", 8 | }, 9 | { 10 | id: 2, 11 | label: "Refund Policy", 12 | href: "/terms-conditions", 13 | }, 14 | { 15 | id: 3, 16 | label: "Terms of Use", 17 | href: "/terms-condition", 18 | }, 19 | { 20 | id: 4, 21 | label: "FAQ's", 22 | href: "#", 23 | }, 24 | { 25 | id: 5, 26 | label: "Contact", 27 | href: "/contact", 28 | }, 29 | ]; 30 | 31 | export default function QuickLinks() { 32 | return ( 33 |
34 |

Quick Link

35 | 36 |
    37 | {quickLinks.map((link) => ( 38 |
  • 39 | 43 | {link.label} 44 | 45 |
  • 46 | ))} 47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/app/context/PreviewSliderContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { createContext, useContext, useState } from "react"; 3 | 4 | interface PreviewSliderType { 5 | isModalPreviewOpen: boolean; 6 | openPreviewModal: () => void; 7 | closePreviewModal: () => void; 8 | } 9 | 10 | const PreviewSlider = createContext(undefined); 11 | 12 | export const usePreviewSlider = () => { 13 | const context = useContext(PreviewSlider); 14 | if (!context) { 15 | throw new Error("usePreviewSlider must be used within a ModalProvider"); 16 | } 17 | return context; 18 | }; 19 | 20 | export const PreviewSliderProvider = ({ 21 | children, 22 | }: { 23 | children: React.ReactNode; 24 | }) => { 25 | const [isModalPreviewOpen, setIsModalOpen] = useState(false); 26 | 27 | const openPreviewModal = () => { 28 | setIsModalOpen(true); 29 | }; 30 | 31 | const closePreviewModal = () => { 32 | setIsModalOpen(false); 33 | }; 34 | 35 | return ( 36 | 39 | {children} 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/Home/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | import { getHeroBanners, getHeroSliders } from "@/get-api-data/hero"; 2 | import HeroBannerItem from "./HeroBannerItem"; 3 | import HeroCarousel from "./HeroCarousel"; 4 | 5 | const Hero = async () => { 6 | const data = await getHeroBanners(); 7 | const sliders = await getHeroSliders(); 8 | 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 | {data.map((bannerItem, key: number) => ( 21 | 22 | ))} 23 |
24 |
25 |
26 |
27 | ); 28 | }; 29 | 30 | export default Hero; 31 | -------------------------------------------------------------------------------- /src/components/Footer/FooterAbout.tsx: -------------------------------------------------------------------------------- 1 | import { CallIcon, EmailIcon, MapIcon } from '@/assets/icons' 2 | import FooterSocials from './FooterSocials' 3 | 4 | const aboutData = [ 5 | { 6 | id: 1, 7 | icon: MapIcon, 8 | text: '685 Market Street,Las Vegas, LA 95820,United States.', 9 | }, 10 | { 11 | id: 2, 12 | icon: CallIcon, 13 | text: '(+099) 532-786-9843', 14 | }, 15 | { 16 | id: 3, 17 | icon: EmailIcon, 18 | text: 'support@example.com', 19 | } 20 | ] 21 | 22 | export default function FooterAbout() { 23 | return ( 24 |
25 |

26 | Help & Support 27 |

28 | 29 |
    30 | { 31 | aboutData.map((item) => ( 32 |
  • 33 | 34 | 35 | 36 | {item.text} 37 |
  • 38 | )) 39 | } 40 |
41 | 42 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Common/Newsletter/index.tsx: -------------------------------------------------------------------------------- 1 | import Graphics from "./Graphics"; 2 | import NewsletterForm from "./NewsletterForm"; 3 | 4 | const Newsletter = () => { 5 | return ( 6 |
7 |
8 |
9 | 10 | 11 |
12 |
13 |

14 | Don't Miss Out Latest Trends & Offers 15 |

16 |

17 | Register to receive news about the latest offers & discount 18 | codes 19 |

20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default Newsletter; 33 | -------------------------------------------------------------------------------- /src/components/ui/input/index.tsx: -------------------------------------------------------------------------------- 1 | import cn from "@/utils/cn"; 2 | import { useId, type HTMLProps } from "react"; 3 | 4 | type PropsType = Omit, "label"> & { 5 | label: string; 6 | required?: boolean; 7 | errorMessage?: string; 8 | error?: boolean; 9 | }; 10 | 11 | export function InputGroup({ 12 | label, 13 | className, 14 | error, 15 | errorMessage, 16 | ...props 17 | }: PropsType) { 18 | const id = useId(); 19 | return ( 20 |
21 | 27 | 28 | 39 | 40 | {error &&

{errorMessage}

} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /public/images/checkout/cash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/utils/confirmDialog.ts: -------------------------------------------------------------------------------- 1 | import Swal from "sweetalert2"; 2 | 3 | /** 4 | * Reusable confirmation dialog 5 | * @param {string} title - Title of the dialog 6 | * @param {string} text - Message body 7 | * @param {string} confirmButtonText - Confirm button text 8 | * @returns {Promise} - Resolves to true if confirmed, false otherwise 9 | */ 10 | export const confirmDialog = async ( 11 | title: string, 12 | text: string, 13 | confirmButtonText = "Yes, delete it!" 14 | ): Promise => { 15 | const result = await Swal.fire({ 16 | title, 17 | text, 18 | icon: "warning", 19 | showCancelButton: true, 20 | confirmButtonColor: "#3085d6", 21 | cancelButtonColor: "#d33", 22 | confirmButtonText, 23 | }); 24 | 25 | return result.isConfirmed; 26 | }; 27 | 28 | /* 29 | * success dialog 30 | * @param {string} title - Title of the dialog 31 | */ 32 | export const successDialog = (title: string) => { 33 | return Swal.fire({ 34 | title, 35 | icon: "success", 36 | }); 37 | }; 38 | 39 | 40 | /* 41 | * error dialog 42 | * @param {string} title - Title of the dialog 43 | */ 44 | export const errorDialog = (title: string) => { 45 | return Swal.fire({ 46 | title, 47 | icon: "error", 48 | }); 49 | }; -------------------------------------------------------------------------------- /src/redux/features/quickView-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { Product } from "@/types/product"; 3 | import React from "react"; 4 | 5 | type InitialState = { 6 | value: Product; 7 | }; 8 | 9 | const initialState = { 10 | value: { 11 | _id: 0, 12 | name: "", 13 | reviews: [""], 14 | price: 0, 15 | discountedPrice: 0, 16 | category: "", 17 | tags: [""], 18 | description: [], 19 | shortDescription: "", 20 | colors: [""], 21 | thumbnails: [""], 22 | previewImages: [""], 23 | additionalInformation: {}, 24 | customAttributes: {}, 25 | status: true, 26 | offers: [""], 27 | } as unknown as Product, 28 | } as InitialState; 29 | 30 | export const quickView = createSlice({ 31 | name: "quickView", 32 | initialState, 33 | reducers: { 34 | updateQuickView: (_, action) => { 35 | return { 36 | value: { 37 | ...action.payload, 38 | }, 39 | }; 40 | }, 41 | 42 | resetQuickView: () => { 43 | return { 44 | value: initialState.value, 45 | }; 46 | }, 47 | }, 48 | }); 49 | 50 | export const { updateQuickView, resetQuickView } = quickView.actions; 51 | export default quickView.reducer; 52 | -------------------------------------------------------------------------------- /public/images/icons/icon-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/(site)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "../../components/Footer"; 2 | import ScrollToTop from "@/components/Common/ScrollToTop"; 3 | import PreLoader from "@/components/Common/PreLoader"; 4 | import { Toaster } from "react-hot-toast"; 5 | import Providers from "./Providers"; 6 | import NextTopLoader from "nextjs-toploader"; 7 | import MainHeader from "@/components/Header/MainHeader"; 8 | import { getHeaderSettings } from "@/get-api-data/header-setting"; 9 | import Breadcrumb from "@/components/Common/Breadcrumb"; 10 | 11 | export default async function SiteLayout({ 12 | children, 13 | }: { 14 | children: React.ReactNode; 15 | }) { 16 | const headerSettingData = await getHeaderSettings(); 17 | return ( 18 |
19 | 20 | <> 21 | 22 | 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Home/BestSeller/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import BestSellerSectionTitle from "./BestSellerSectionTitle"; 3 | import SingleItem from "./SingleItem"; 4 | import { getBestSellingProducts } from "@/get-api-data/product"; 5 | 6 | const BestSeller = async () => { 7 | const bestSellProducts = await getBestSellingProducts(); 8 | 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 | {bestSellProducts.length > 0 && 16 | bestSellProducts.map((item, key) => ( 17 | 18 | ))} 19 |
20 | 21 |
22 | 26 | View All 27 | 28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default BestSeller; 35 | -------------------------------------------------------------------------------- /src/types/product.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type Product = { 4 | id: string; 5 | title: string; 6 | price: number; 7 | discountedPrice?: number | null; 8 | slug: string; 9 | quantity: number; 10 | updatedAt: Date; 11 | reviews: number; 12 | shortDescription: string; 13 | productVariants: { 14 | color: string; 15 | image: string; 16 | size: string; 17 | isDefault: boolean; 18 | }[]; 19 | }; 20 | 21 | 22 | export type IProductByDetails = { 23 | id: string; 24 | title: string; 25 | shortDescription: string; 26 | description: string | null; 27 | price: number; 28 | discountedPrice?: number | null; 29 | slug: string; 30 | quantity: number; 31 | updatedAt: Date; 32 | category: { 33 | title: string; 34 | slug: string; 35 | } | null; 36 | productVariants: { 37 | color: string; 38 | image: string; 39 | size: string; 40 | isDefault: boolean; 41 | }[]; 42 | reviews: number; 43 | additionalInformation: { 44 | name: string; 45 | description: string; 46 | }[]; 47 | customAttributes: { 48 | attributeName: string; 49 | attributeValues: { 50 | id: string; 51 | title: string; 52 | }[]; 53 | }[]; 54 | body: string | null; 55 | tags: string[] | null; 56 | offers: string[] | null; 57 | sku: string | null; 58 | }; 59 | 60 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, Middleware } from "@reduxjs/toolkit"; 2 | 3 | import quickViewReducer from "./features/quickView-slice"; 4 | import cartReducer from "./features/cart-slice"; 5 | import wishlistReducer from "./features/wishlist-slice"; 6 | import productDetailsReducer from "./features/product-details"; 7 | 8 | import { TypedUseSelectorHook, useSelector } from "react-redux"; 9 | import { saveCartToStorage } from "@/lib/cartStorage"; 10 | 11 | const localStorageMiddleware: Middleware = (store) => (next) => (action: any) => { 12 | const result = next(action); 13 | 14 | // Save cart to localStorage after any cart action 15 | if (action.type?.startsWith("cart/")) { 16 | const state = store.getState(); 17 | saveCartToStorage(state.cartReducer.items); 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | export const store = configureStore({ 24 | reducer: { 25 | quickViewReducer, 26 | cartReducer, 27 | wishlistReducer, 28 | productDetailsReducer, 29 | }, 30 | middleware: (getDefaultMiddleware) => 31 | getDefaultMiddleware().concat(localStorageMiddleware), 32 | }); 33 | 34 | export type RootState = ReturnType; 35 | export type AppDispatch = typeof store.dispatch; 36 | 37 | export const useAppSelector: TypedUseSelectorHook = useSelector; 38 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Home/Countdown/CountdownTimer.tsx: -------------------------------------------------------------------------------- 1 | import { getSevenDaysFromToday } from '@/utils/dateUtils'; 2 | import { useEffect, useState } from 'react'; 3 | import TimeDisplay from './TimeDisplay'; 4 | 5 | export interface TimeState { 6 | days: number; 7 | hours: number; 8 | minutes: number; 9 | seconds: number; 10 | } 11 | 12 | export const CountdownTimer = () => { 13 | const [date, setDate] = useState({ 14 | days: 0, 15 | hours: 0, 16 | minutes: 0, 17 | seconds: 0, 18 | }); 19 | 20 | const getTime = () => { 21 | const time = Date.parse(getSevenDaysFromToday()) - Date.now(); 22 | 23 | setDate({ 24 | days: Math.floor(time / (1000 * 60 * 60 * 24)), 25 | hours: Math.floor((time / (1000 * 60 * 60)) % 24), 26 | minutes: Math.floor((time / 1000 / 60) % 60), 27 | seconds: Math.floor((time / 1000) % 60), 28 | }); 29 | }; 30 | 31 | useEffect(() => { 32 | const interval = setInterval(getTime, 1000); 33 | return () => clearInterval(interval); 34 | }, []); 35 | 36 | return ( 37 |
38 | 39 | 40 | 41 | 42 |
43 | ); 44 | }; -------------------------------------------------------------------------------- /src/app/context/QuickViewModalContext.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client"; 3 | import React, { createContext, useContext, useEffect, useState } from "react"; 4 | 5 | interface ModalContextType { 6 | isModalOpen: boolean; 7 | openModal: () => void; 8 | closeModal: () => void; 9 | } 10 | 11 | const ModalContext = createContext(undefined); 12 | 13 | export const useModalContext = () => { 14 | const context = useContext(ModalContext); 15 | if (!context) { 16 | throw new Error("useModalContext must be used within a ModalProvider"); 17 | } 18 | return context; 19 | }; 20 | 21 | export const ModalProvider = ({ children }: { children: React.ReactNode }) => { 22 | const [isModalOpen, setIsModalOpen] = useState(false); 23 | 24 | useEffect(() => { 25 | if (isModalOpen) { 26 | document.body.classList.add("overflow-hidden"); 27 | } else { 28 | document.body.classList.remove("overflow-hidden"); 29 | } 30 | 31 | // Cleanup in case the component unmounts while modal is open 32 | return () => { 33 | document.body.classList.remove("overflow-hidden"); 34 | }; 35 | }, [isModalOpen]); 36 | 37 | const openModal = () => setIsModalOpen(true); 38 | const closeModal = () => setIsModalOpen(false); 39 | 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/Common/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ChevronUpIcon } from "@/assets/icons"; 3 | import { useEffect, useState } from "react"; 4 | 5 | export default function ScrollToTop() { 6 | const [isVisible, setIsVisible] = useState(false); 7 | 8 | // Top: 0 takes us all the way back to the top of the page 9 | // Behavior: smooth keeps it smooth! 10 | const scrollToTop = () => { 11 | window.scrollTo({ 12 | top: 0, 13 | behavior: "smooth", 14 | }); 15 | }; 16 | 17 | useEffect(() => { 18 | // Button is displayed after scrolling for 500 pixels 19 | const toggleVisibility = () => { 20 | if (window.pageYOffset > 300) { 21 | setIsVisible(true); 22 | } else { 23 | setIsVisible(false); 24 | } 25 | }; 26 | 27 | window.addEventListener("scroll", toggleVisibility); 28 | 29 | return () => window.removeEventListener("scroll", toggleVisibility); 30 | }, []); 31 | 32 | return ( 33 | <> 34 | {isVisible && ( 35 | 43 | )} 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Home/PromoBanner/LargePromoBanner.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | interface LargePromoBannerProps { 5 | imageUrl: string; 6 | subtitle: string; 7 | title: string; 8 | description: string; 9 | link: string; 10 | buttonText: string; 11 | } 12 | 13 | export default function LargePromoBanner({ 14 | imageUrl, 15 | subtitle, 16 | title, 17 | description, 18 | link, 19 | buttonText, 20 | }: LargePromoBannerProps) { 21 | return ( 22 |
23 |
24 | 25 | {subtitle} 26 | 27 |

28 | {title} 29 |

30 |

{description}

31 | 35 | {buttonText} 36 | 37 |
38 | promo img 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /public/images/payment/payment-01.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeftIcon } from "@/assets/icons"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React from "react"; 5 | 6 | export default function NotFound() { 7 | return ( 8 |
9 |
10 |
11 |
12 | 404 19 | 20 |

21 | Sorry, the page can’t be found 22 |

23 | 24 |

25 | The page you were looking for appears to have been moved, deleted 26 | or does not exist. 27 |

28 | 29 | 33 | 34 | Back to Home 35 | 36 |
37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Home/Hero/FooterFeature.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | 4 | const featureData = [ 5 | { 6 | img: "/images/icons/icon-01.svg", 7 | title: "Free Shipping", 8 | description: "For all orders $200", 9 | }, 10 | { 11 | img: "/images/icons/icon-02.svg", 12 | title: "1 & 1 Returns", 13 | description: "Cancellation after 1 day", 14 | }, 15 | { 16 | img: "/images/icons/icon-03.svg", 17 | title: "100% Secure Payments", 18 | description: "Gurantee secure payments", 19 | }, 20 | { 21 | img: "/images/icons/icon-04.svg", 22 | title: "24/7 Dedicated Support", 23 | description: "Anywhere & anytime", 24 | }, 25 | ]; 26 | 27 | const FooterFeature = () => { 28 | return ( 29 |
30 |
31 |
32 | {featureData.map((item, key) => ( 33 |
34 | icons 35 | 36 |
37 |

38 | {item.title} 39 |

40 |

{item.description}

41 |
42 |
43 | ))} 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default FooterFeature; 51 | -------------------------------------------------------------------------------- /src/components/Home/Testimonials/TestimonialsHeader.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeftIcon, ChevronRightIcon } from "@/assets/icons"; 2 | 3 | interface TestimonialsHeaderProps { 4 | onPrev: () => void; 5 | onNext: () => void; 6 | isPrevDisabled: boolean; 7 | isNextDisabled: boolean; 8 | } 9 | 10 | const TestimonialsHeader = ({ 11 | onPrev, 12 | onNext, 13 | isPrevDisabled, 14 | isNextDisabled, 15 | }: TestimonialsHeaderProps) => { 16 | return ( 17 |
18 |
19 |

20 | User Feedbacks 21 |

22 |
23 | 24 |
25 | 35 | 36 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default TestimonialsHeader; 52 | -------------------------------------------------------------------------------- /src/components/Wishlist/AddWishlistButton.tsx: -------------------------------------------------------------------------------- 1 | import { HeartFilledIcon, HeartIcon, HeartSolid } from "@/assets/icons"; 2 | import { useAppSelector } from "@/redux/store"; 3 | import { Product } from "@/types/product"; 4 | import { useEffect, useState } from "react"; 5 | import Tooltip from "../Common/Tooltip"; 6 | 7 | // prop type 8 | type IProps = { 9 | item: Product; 10 | handleItemToWishList: () => void; 11 | }; 12 | 13 | const WishlistButton = ({ item, handleItemToWishList }: IProps) => { 14 | const wishlistItems = useAppSelector((state) => state.wishlistReducer.items); 15 | const [hasMounted, setHasMounted] = useState(false); 16 | 17 | useEffect(() => { 18 | setHasMounted(true); 19 | }, []); 20 | 21 | if (!hasMounted) { 22 | return null; // Avoid hydration issues by not rendering mismatched content 23 | } 24 | 25 | const isAlreadyWishListed = wishlistItems.some( 26 | (wishlistItem) => wishlistItem.id === item.id 27 | ); 28 | 29 | return ( 30 | 31 | 42 | 43 | ); 44 | }; 45 | 46 | export default WishlistButton; 47 | -------------------------------------------------------------------------------- /public/images/logo/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowLeftIcon } from "@/assets/icons"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | 5 | const Error = () => { 6 | return ( 7 | <> 8 |
9 |
10 |
11 |
12 | 404 19 | 20 |

21 | Sorry, the page can’t be found 22 |

23 | 24 |

25 | The page you were looking for appears to have been moved, 26 | deleted or does not exist. 27 |

28 | 29 | 33 | 34 | Back to Home 35 | 36 |
37 |
38 |
39 |
40 | 41 | ); 42 | }; 43 | 44 | export default Error; 45 | -------------------------------------------------------------------------------- /src/get-api-data/seo-setting.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | // get all seo settings 5 | export const getSeoSettings = unstable_cache( 6 | async () => { 7 | return await prisma.seoSetting.findFirst(); 8 | }, 9 | ['seo-setting'], { tags: ['seo-setting'] } 10 | ); 11 | 12 | export const getSiteName = unstable_cache( 13 | async () => { 14 | const siteName = await prisma.seoSetting.findFirst({ 15 | select: { 16 | siteName: true, 17 | }, 18 | }); 19 | return siteName ? siteName.siteName : process.env.SITE_NAME ? process.env.SITE_NAME : "Cozy-commerce"; 20 | }, 21 | ['site-name'], { tags: ['site-name'] } 22 | ); 23 | 24 | // get logo 25 | export const getLogo = unstable_cache( 26 | async () => { 27 | const headerLogo = await prisma.headerSetting.findFirst({ 28 | select: { 29 | headerLogo: true, 30 | }, 31 | }); 32 | const logo = headerLogo ? headerLogo.headerLogo : "https://res.cloudinary.com/dc6svbdh9/image/upload/v1746335068/header/tsvfm6pvfwpbpyqdtxwn.svg"; 33 | return logo; 34 | }, 35 | ['header-logo'], { tags: ['header-logo'] } 36 | ); 37 | 38 | // get email logo 39 | export const getEmailLogo = unstable_cache( 40 | async () => { 41 | const emailLogo = await prisma.headerSetting.findFirst({ 42 | select: { 43 | emailLogo: true, 44 | }, 45 | }); 46 | const logo = emailLogo ? emailLogo.emailLogo : "https://res.cloudinary.com/dc6svbdh9/image/upload/v1746693785/logo_ouegg7.png"; 47 | return logo; 48 | }, 49 | ['email-logo'], { tags: ['email-logo'] } 50 | ); 51 | 52 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./css/style.css"; 2 | import { Metadata } from "next"; 3 | import { getSeoSettings, getSiteName } from "@/get-api-data/seo-setting"; 4 | import { GoogleTagManager } from '@next/third-parties/google'; 5 | import { DM_Sans } from 'next/font/google' 6 | 7 | const dm_sans = DM_Sans({ 8 | weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], 9 | variable: "--font-body", 10 | subsets: ['latin'], 11 | }) 12 | 13 | export async function generateMetadata(): Promise { 14 | const seoSettings = await getSeoSettings(); 15 | const site_name = await getSiteName(); 16 | return { 17 | title: `${seoSettings?.siteTitle || "Home Page"} | ${site_name}`, 18 | description: seoSettings?.metadescription || "Cozy-commerce is a next.js e-commerce boilerplate built with nextjs, typescript, tailwindcss, and prisma.", 19 | keywords: seoSettings?.metaKeywords || "e-commerce, online store", 20 | openGraph: { 21 | images: seoSettings?.metaImage ? [seoSettings.metaImage] : [], 22 | }, 23 | icons: { 24 | icon: seoSettings?.favicon || "/favicon.ico", 25 | shortcut: seoSettings?.favicon || "/favicon.ico", 26 | apple: seoSettings?.favicon || "/favicon.ico", 27 | }, 28 | }; 29 | } 30 | 31 | export default async function RootLayout({ 32 | children, 33 | }: { 34 | children: React.ReactNode; 35 | }) { 36 | const seoSettings = await getSeoSettings(); 37 | return ( 38 | 39 | 40 | {children} 41 | {seoSettings?.gtmId && } 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /public/images/icons/icon-06.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Header/menuData.ts: -------------------------------------------------------------------------------- 1 | import type { MenuItem } from "./types"; 2 | 3 | export const menuData: MenuItem[] = [ 4 | { 5 | title: "Popular", 6 | path: "/popular?sort=popular", 7 | }, 8 | { 9 | title: "Shop", 10 | path: "/shop-with-sidebar", 11 | }, 12 | 13 | { 14 | title: "Pages", 15 | submenu: [ 16 | { 17 | title: "Shop without sidebar", 18 | path: "/shop-without-sidebar", 19 | }, 20 | { 21 | title: "Checkout", 22 | path: "/checkout", 23 | }, 24 | { 25 | title: "Cart", 26 | path: "/cart", 27 | }, 28 | { 29 | title: "Wishlist", 30 | path: "/wishlist", 31 | }, 32 | { 33 | title: "Sign in", 34 | path: "/signin", 35 | }, 36 | { 37 | title: "Sign up", 38 | path: "/signup", 39 | }, 40 | { 41 | title: "Error", 42 | path: "/error", 43 | }, 44 | { 45 | title: "Mail Success", 46 | path: "/mail-success", 47 | }, 48 | { 49 | title:"Privacy Policy", 50 | path:"/privacy-policy" 51 | }, 52 | { 53 | title:"Terms & Conditions", 54 | path:"/terms-conditions" 55 | } 56 | ], 57 | }, 58 | { 59 | title: "Blog", 60 | submenu: [ 61 | { 62 | title: "Blog Grid with Sidebar", 63 | path: "/blogs/blog-grid-with-sidebar", 64 | }, 65 | { 66 | title: "Blog Grid", 67 | path: "/blogs/blog-grid", 68 | }, 69 | { 70 | title: "Blog details with sidebar", 71 | path: "/blogs/blog-details-with-sidebar", 72 | }, 73 | { 74 | title: "Blog Details", 75 | path: "/blogs/blog-details", 76 | }, 77 | ], 78 | }, 79 | { 80 | title: "Contact", 81 | path: "/contact", 82 | }, 83 | ]; 84 | -------------------------------------------------------------------------------- /src/components/Common/CartSidebarModal/SingleItem.tsx: -------------------------------------------------------------------------------- 1 | import { TrashIcon } from "@/assets/icons"; 2 | import Image from "next/image"; 3 | import { useCart } from "@/hooks/useCart"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | const SingleItem = ({ item }: any) => { 7 | const { removeItem, handleCartClick } = useCart(); 8 | const router = useRouter(); 9 | const handleRemoveFromCart = () => { 10 | removeItem(item.id); 11 | }; 12 | 13 | const handleProductClick = () => { 14 | router.push(`/products/${item.slug}`); 15 | setTimeout(() => { 16 | handleCartClick(); 17 | }, 500); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 | product 25 |
26 | 27 |
28 |

29 | 32 |

33 |

Price: ${item.price}

34 |
35 |
36 | 37 |
38 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default SingleItem; 51 | -------------------------------------------------------------------------------- /public/images/icons/icon-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Home/Testimonials/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Swiper, SwiperSlide } from "swiper/react"; 3 | import SingleItem from "./SingleItem"; 4 | import testimonialsData from "./testimonialsData"; 5 | import TestimonialsHeader from "./TestimonialsHeader"; 6 | import { useTestimonialSwiper } from "./useTestimonialSwiper"; 7 | 8 | // Import Swiper styles 9 | import "swiper/css"; 10 | import "swiper/css/navigation"; 11 | 12 | const BREAKPOINTS = { 13 | 0: { slidesPerView: 1 }, 14 | 1000: { slidesPerView: 2 }, 15 | 1200: { slidesPerView: 3 }, 16 | } as const; 17 | 18 | const Testimonials = () => { 19 | const { sliderRef, handlePrev, handleNext, onSlideChange, currentIndex } = 20 | useTestimonialSwiper(); 21 | 22 | return ( 23 |
24 |
25 |
26 | 32 | 33 | 41 | {[ 42 | ...testimonialsData, 43 | ...testimonialsData, 44 | ...testimonialsData, 45 | ].map((item, key) => ( 46 | 47 | 48 | 49 | ))} 50 | 51 |
52 |
53 |
54 | ); 55 | }; 56 | 57 | export default Testimonials; 58 | -------------------------------------------------------------------------------- /src/components/Footer/FooterBottom.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | const paymentsData = [ 4 | { 5 | id: 1, 6 | image: "/images/payment/payment-01.svg", 7 | alt: "visa card", 8 | width: 66, 9 | height: 22, 10 | }, 11 | { 12 | id: 2, 13 | image: "/images/payment/payment-02.svg", 14 | alt: "paypal", 15 | width: 18, 16 | height: 21, 17 | }, 18 | { 19 | id: 3, 20 | image: "/images/payment/payment-03.svg", 21 | alt: "master card", 22 | width: 33, 23 | height: 24, 24 | }, 25 | { 26 | id: 4, 27 | image: "/images/payment/payment-04.svg", 28 | alt: "apple pay", 29 | width: 52.94, 30 | height: 22, 31 | }, 32 | { 33 | id: 5, 34 | image: "/images/payment/payment-05.svg", 35 | alt: "google pay", 36 | width: 56, 37 | height: 22, 38 | }, 39 | ]; 40 | 41 | export default function FooterBottom() { 42 | const year = new Date().getFullYear(); 43 | 44 | return ( 45 |
46 |
47 |
48 |

49 | © {year}. All rights reserved by Pimjo. 50 |

51 | 52 |
53 |

We Accept:

54 | 55 |
56 | {paymentsData.map((payment) => ( 57 | {payment.alt} 65 | ))} 66 |
67 |
68 |
69 |
70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/cartStorage.ts: -------------------------------------------------------------------------------- 1 | import { CartItem } from "@/redux/features/cart-slice"; 2 | 3 | const CART_STORAGE_KEY = "cozycommerce-cart"; 4 | 5 | /** 6 | * Save cart items to localStorage 7 | */ 8 | export const saveCartToStorage = (items: CartItem[]): void => { 9 | try { 10 | const serializedCart = JSON.stringify(items); 11 | localStorage.setItem(CART_STORAGE_KEY, serializedCart); 12 | } catch (error) { 13 | // Handle quota exceeded or other localStorage errors 14 | if (error instanceof Error) { 15 | if (error.name === "QuotaExceededError") { 16 | console.error("LocalStorage quota exceeded. Unable to save cart."); 17 | } else if (error.name === "SecurityError") { 18 | console.error("LocalStorage access denied (private browsing mode?)."); 19 | } else { 20 | console.error("Failed to save cart to localStorage:", error.message); 21 | } 22 | } 23 | } 24 | }; 25 | 26 | /** 27 | * Load cart items from localStorage 28 | */ 29 | export const loadCartFromStorage = (): CartItem[] => { 30 | try { 31 | const serializedCart = localStorage.getItem(CART_STORAGE_KEY); 32 | if (serializedCart === null) { 33 | return []; 34 | } 35 | return JSON.parse(serializedCart) as CartItem[]; 36 | } catch (error) { 37 | // Handle JSON parse errors or localStorage access errors 38 | if (error instanceof Error) { 39 | console.error("Failed to load cart from localStorage:", error.message); 40 | } 41 | return []; 42 | } 43 | }; 44 | 45 | /** 46 | * Clear cart data from localStorage 47 | */ 48 | export const clearCartStorage = (): void => { 49 | try { 50 | localStorage.removeItem(CART_STORAGE_KEY); 51 | } catch (error) { 52 | if (error instanceof Error) { 53 | console.error("Failed to clear cart from localStorage:", error.message); 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/Home/PromoBanner/SmallPromoBanner.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | 4 | interface SmallPromoBannerProps { 5 | imageUrl: string; 6 | subtitle: string; 7 | title: string; 8 | discount?: string; 9 | link: string; 10 | buttonText: string; 11 | rightAlign?: boolean; 12 | 13 | buttonColor?: string; 14 | description?: string; 15 | } 16 | 17 | export default function SmallPromoBanner({ 18 | imageUrl, 19 | subtitle, 20 | title, 21 | discount, 22 | link, 23 | buttonText, 24 | rightAlign = false, 25 | buttonColor = "bg-teal hover:bg-teal-dark", 26 | description, 27 | }: SmallPromoBannerProps) { 28 | return ( 29 |
32 |
33 | {subtitle} 34 |

35 | {title} 36 |

37 | {description ? ( 38 |

43 | {description} 44 |

45 | ) : ( 46 |

{discount}

47 | )} 48 | 52 | {buttonText} 53 | 54 |
55 | promo img 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/Home/Hero/HeroBannerItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { formatPrice } from "@/utils/formatePrice"; 4 | 5 | export default function HeroBannerItem({ bannerItem }: { bannerItem: any }) { 6 | 7 | return ( 8 |
9 |
10 |
11 |
12 |

13 | 14 | {" "} 15 | {bannerItem.bannerName}{" "} 16 | 17 |

18 |

19 | {bannerItem?.subtitle} 20 |

21 |
22 |
23 |

24 | limited time offer 25 |

26 | 27 | 28 | {formatPrice( 29 | bannerItem?.product?.discountedPrice 30 | ? bannerItem?.product?.discountedPrice 31 | : bannerItem?.product?.price 32 | )} 33 | 34 | {bannerItem?.product?.discountedPrice && ( 35 | 36 | {formatPrice(bannerItem?.product?.price)} 37 | 38 | )} 39 | 40 |
41 |
42 |
43 | mobile image 49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /public/images/checkout/stripe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from "react-redux"; 2 | import { AppDispatch } from "@/redux/store"; 3 | import { 4 | addItemToCart, 5 | removeItemFromCart, 6 | incrementItem, 7 | decrementItem, 8 | clearCart, 9 | toggleCartModal, 10 | selectCartCount, 11 | selectCartDetails, 12 | selectTotalPrice, 13 | selectFormattedTotalPrice, 14 | selectShouldDisplayCart, 15 | CartItem, 16 | } from "@/redux/features/cart-slice"; 17 | import { clearCartStorage } from "@/lib/cartStorage"; 18 | 19 | export const useCart = () => { 20 | const dispatch = useDispatch(); 21 | 22 | // Selectors 23 | const cartCount = useSelector(selectCartCount); 24 | const cartDetails = useSelector(selectCartDetails); 25 | const totalPrice = useSelector(selectTotalPrice); 26 | const formattedTotalPrice = useSelector(selectFormattedTotalPrice); 27 | const shouldDisplayCart = useSelector(selectShouldDisplayCart); 28 | 29 | // Actions 30 | const addItem = (item: CartItem) => { 31 | dispatch(addItemToCart(item)); 32 | }; 33 | 34 | const removeItem = (id: string | number) => { 35 | dispatch(removeItemFromCart(id)); 36 | }; 37 | 38 | const incrementItemQuantity = (id: string | number) => { 39 | dispatch(incrementItem(id)); 40 | }; 41 | 42 | const decrementItemQuantity = (id: string | number) => { 43 | dispatch(decrementItem(id)); 44 | }; 45 | 46 | const clearAllItems = () => { 47 | dispatch(clearCart()); 48 | clearCartStorage(); 49 | }; 50 | 51 | const handleCartClick = () => { 52 | dispatch(toggleCartModal()); 53 | }; 54 | 55 | return { 56 | // State 57 | cartCount, 58 | cartDetails, 59 | totalPrice, 60 | formattedTotalPrice, 61 | shouldDisplayCart, 62 | 63 | // Actions 64 | addItem, 65 | removeItem, 66 | incrementItem: incrementItemQuantity, 67 | decrementItem: decrementItemQuantity, 68 | clearCart: clearAllItems, 69 | handleCartClick, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /src/get-api-data/hero.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/prismaDB"; 2 | import { unstable_cache } from "next/cache"; 3 | 4 | 5 | // get hero banners 6 | export const getHeroBanners = unstable_cache( 7 | async () => { 8 | const heroBanners = await prisma.heroBanner.findMany({ 9 | orderBy: { updatedAt: "desc" }, 10 | include: { 11 | product: { 12 | select: { 13 | price: true, 14 | discountedPrice: true, 15 | title: true, 16 | slug: true 17 | } 18 | } 19 | } 20 | }); 21 | 22 | return heroBanners.map((item) => ({ 23 | ...item, 24 | product: { 25 | ...item.product, 26 | price: item.product.price.toNumber(), 27 | discountedPrice: item.product.discountedPrice ? item.product.discountedPrice.toNumber() : null 28 | } 29 | })); 30 | }, 31 | ['heroBanners'], { tags: ['heroBanners'] } 32 | ); 33 | 34 | // get hero sliders 35 | export const getHeroSliders = unstable_cache( 36 | async () => { 37 | const heroSliders = await prisma.heroSlider.findMany({ 38 | orderBy: { updatedAt: "desc" }, 39 | include: { 40 | product: { 41 | select: { 42 | price: true, 43 | discountedPrice: true, 44 | title: true, 45 | slug: true, 46 | shortDescription: true 47 | } 48 | } 49 | } 50 | }); 51 | 52 | return heroSliders.map((item) => ({ 53 | ...item, 54 | product: { 55 | ...item.product, 56 | price: item.product.price.toNumber(), 57 | discountedPrice: item.product.discountedPrice ? item.product.discountedPrice.toNumber() : null 58 | } 59 | })) 60 | }, 61 | ['heroSliders'], { tags: ['heroSliders'] } 62 | ); 63 | 64 | 65 | // single hero banner 66 | export const getSingleHeroBanner = async (id:number) => 67 | unstable_cache( 68 | async () => { 69 | return await prisma.heroBanner.findUnique({ 70 | where: { 71 | id: id 72 | } 73 | }); 74 | }, 75 | ['single-hero-banner'], { tags: [`single-hero-banner-${id}`] } 76 | ) 77 | -------------------------------------------------------------------------------- /src/components/Home/Countdown/CountdownBanner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import { CountdownTimer } from "./CountdownTimer"; 5 | import { Countdown } from "@prisma/client"; 6 | 7 | interface CountdownBannerProps { 8 | data: Countdown & { product: { title: string } }; 9 | } 10 | 11 | const CountdownBanner = ({ data }: CountdownBannerProps) => { 12 | if (!data) return null; 13 | 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 | {data.subtitle} 21 | 22 | 23 |

24 | {data.title} 25 |

26 |

27 | {data?.product?.title} 28 |

29 | 30 | 31 | 32 | 36 | Check it Out! 37 | 38 |
39 | 40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | 47 | const BackgroundImages = ({ data }: CountdownBannerProps) => ( 48 | <> 49 | bg shapes 56 | product 63 | 64 | ); 65 | 66 | export default CountdownBanner; 67 | -------------------------------------------------------------------------------- /public/images/icons/icon-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/redux/features/wishlist-slice.ts: -------------------------------------------------------------------------------- 1 | import { WishlistItem } from '@/types/wishlistItem'; 2 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 3 | import toast from 'react-hot-toast'; 4 | 5 | // Load wishlist items from local storage if available 6 | let initialItemsState: WishlistItem[] = []; 7 | 8 | export const wishlist = createSlice({ 9 | name: 'wishlist', 10 | initialState: { items: initialItemsState }, 11 | reducers: { 12 | setWishlistItems: (state, action: PayloadAction) => { 13 | state.items = action.payload; 14 | }, 15 | addItemToWishlist: (state, action: PayloadAction) => { 16 | const { 17 | id, 18 | title, 19 | price, 20 | slug, 21 | image, 22 | quantity, 23 | color 24 | } = action.payload; 25 | const existingItem = state.items.find((item) => item.id === id); 26 | 27 | if (existingItem) { 28 | state.items = state.items.filter((item) => item.id !== id); 29 | toast.error('Product removed from wishlist!'); 30 | return; 31 | } else { 32 | state.items.push({ 33 | id, 34 | title, 35 | slug, 36 | image, 37 | price, 38 | quantity, 39 | color 40 | }); 41 | 42 | if (typeof window !== 'undefined' && window.localStorage) { 43 | localStorage.setItem('wishlistItems', JSON.stringify(state.items)); 44 | } 45 | toast.success('Product added to wishlist!'); 46 | } 47 | }, 48 | removeItemFromWishlist: (state, action: PayloadAction) => { 49 | const itemId = action.payload; 50 | state.items = state.items.filter((item) => item.id !== itemId); 51 | if (typeof window !== 'undefined' && window.localStorage) { 52 | localStorage.setItem('wishlistItems', JSON.stringify(state.items)); 53 | } 54 | }, 55 | removeAllItemsFromWishlist: (state) => { 56 | state.items = []; 57 | if (typeof window !== 'undefined' && window.localStorage) { 58 | localStorage.setItem('wishlistItems', JSON.stringify(state.items)); 59 | } 60 | }, 61 | }, 62 | }); 63 | 64 | export const { 65 | addItemToWishlist, 66 | removeItemFromWishlist, 67 | removeAllItemsFromWishlist, 68 | setWishlistItems 69 | } = wishlist.actions; 70 | export default wishlist.reducer; 71 | -------------------------------------------------------------------------------- /public/images/payment/payment-04.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/Common/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | // components/ui/Tooltip.tsx 2 | "use client"; 3 | 4 | import { useState, useRef } from "react"; 5 | import type { ReactNode } from "react"; 6 | 7 | interface TooltipProps { 8 | content: ReactNode; 9 | children: ReactNode; 10 | placement?: "top" | "bottom" | "left" | "right"; 11 | } 12 | 13 | const Tooltip = ({ content, children, placement = "top" }: TooltipProps) => { 14 | const [visible, setVisible] = useState(false); 15 | const timeoutRef = useRef(null); 16 | 17 | const show = () => { 18 | timeoutRef.current = setTimeout(() => setVisible(true), 100); 19 | }; 20 | 21 | const hide = () => { 22 | if (timeoutRef.current) clearTimeout(timeoutRef.current); 23 | setVisible(false); 24 | }; 25 | 26 | const getPositionClasses = () => { 27 | switch (placement) { 28 | case "top": 29 | return "bottom-full left-1/2 -translate-x-1/2 mb-2"; 30 | case "bottom": 31 | return "top-full left-1/2 -translate-x-1/2 mt-2"; 32 | case "left": 33 | return "right-full top-1/2 -translate-y-1/2 mr-2"; 34 | case "right": 35 | return "left-full top-1/2 -translate-y-1/2 ml-2"; 36 | } 37 | }; 38 | 39 | const getArrowPosition = () => { 40 | switch (placement) { 41 | case "top": 42 | return "-bottom-1 left-1/2 -translate-x-1/2"; 43 | case "bottom": 44 | return "bottom-full left-1/2 -translate-x-1/2 rotate-180"; 45 | case "left": 46 | return "left-full top-1/2 -translate-y-1/2 -rotate-90"; 47 | case "right": 48 | return "right-full top-1/2 -translate-y-1/2 rotate-90"; 49 | } 50 | }; 51 | 52 | return ( 53 |
58 | {children} 59 | {visible && ( 60 |
63 |
64 | {content} 65 |
69 |
70 |
71 | )} 72 |
73 | ); 74 | }; 75 | 76 | export default Tooltip; 77 | -------------------------------------------------------------------------------- /public/images/icons/icon-08.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Header/CustomSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Category } from '@/types/category'; 2 | import Link from 'next/link'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | const CustomSelect = () => { 6 | const [isOpen, setIsOpen] = useState(false); 7 | const [selectedOption, setSelectedOption] = useState({ 8 | title: 'All Categories', 9 | }); 10 | const [categories, setCategories] = useState([]); 11 | 12 | const toggleDropdown = () => { 13 | setIsOpen(!isOpen); 14 | }; 15 | 16 | const handleOptionClick = (option: any) => { 17 | setSelectedOption(option); 18 | toggleDropdown(); 19 | }; 20 | 21 | useEffect(() => { 22 | // closing modal while clicking outside 23 | function handleClickOutside(event: any) { 24 | if (!event.target.closest('.dropdown-content')) { 25 | setIsOpen(!isOpen); 26 | } 27 | } 28 | 29 | if (isOpen) { 30 | document.addEventListener('mousedown', handleClickOutside); 31 | } 32 | 33 | const fetchCategories = async () => { 34 | try { 35 | const data = await fetch('/api/category'); 36 | const result = await data.json(); 37 | if(result?.success){ 38 | setCategories(result?.data); 39 | } 40 | } catch (error) { 41 | console.log(error,'error to fetch categories'); 42 | } 43 | }; 44 | 45 | fetchCategories(); 46 | 47 | return () => { 48 | document.removeEventListener('mousedown', handleClickOutside); 49 | }; 50 | }, [isOpen]); 51 | 52 | return ( 53 |
57 |
63 | {selectedOption?.title} 64 |
65 |
66 | {categories.map((option, index) => ( 67 |
handleOptionClick(option)} 70 | className={`select-item ${ 71 | selectedOption === option ? 'same-as-selected' : '' 72 | }`} 73 | > 74 | 75 | {option.title} 76 | 77 |
78 | ))} 79 |
80 |
81 | ); 82 | }; 83 | 84 | export default CustomSelect; 85 | -------------------------------------------------------------------------------- /public/images/icons/icon-07.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/payment/payment-02.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/checkout/fedex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cozycommerce", 3 | "version": "1.3.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "prisma generate && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "algolia:reindex": "node src/algolia/reindex.ts", 11 | "import-demo-data": "node scripts/import-demo-data.js" 12 | }, 13 | "dependencies": { 14 | "@auth/prisma-adapter": "^1.0.10", 15 | "@next/third-parties": "^16.0.6", 16 | "@prisma/adapter-pg": "^7.0.1", 17 | "@prisma/client": "^7.0.1", 18 | "@radix-ui/react-slider": "^1.2.0", 19 | "@react-email/components": "^0.0.38", 20 | "@react-email/render": "^1.1.0", 21 | "@reduxjs/toolkit": "^2.2.2", 22 | "@stripe/react-stripe-js": "^3.5.0", 23 | "@stripe/stripe-js": "^3.1.0", 24 | "@tippyjs/react": "^4.2.6", 25 | "algoliasearch": "^4.23.3", 26 | "axios": "^1.6.8", 27 | "bcrypt": "^5.1.1", 28 | "chart.js": "^4.4.8", 29 | "cheerio": "^1.0.0-rc.12", 30 | "cloudinary": "^2.5.1", 31 | "clsx": "^2.1.1", 32 | "dayjs": "^1.11.13", 33 | "dotenv": "^17.2.3", 34 | "next": "16.0.10", 35 | "next-auth": "^4.24.13", 36 | "next-share": "^0.27.0", 37 | "nextjs-toploader": "^1.6.12", 38 | "nodemailer": "^7.0.11", 39 | "pg": "^8.16.3", 40 | "quill": "^2.0.3", 41 | "rate-limiter-flexible": "^7.0.0", 42 | "react": "^19.2.0", 43 | "react-chartjs-2": "^5.3.0", 44 | "react-dom": "^19.2.0", 45 | "react-hook-form": "^7.54.2", 46 | "react-hot-toast": "^2.4.1", 47 | "react-instantsearch": "^7.7.1", 48 | "react-paginate": "^8.3.0", 49 | "react-quilljs": "^2.0.5", 50 | "react-redux": "^9.1.0", 51 | "react-tooltip": "^5.28.1", 52 | "stripe": "^14.8.0", 53 | "styled-components": "^6.1.15", 54 | "sweetalert2": "^11.17.2", 55 | "swiper": "^11.0.7", 56 | "tailwind-merge": "^3.0.1" 57 | }, 58 | "devDependencies": { 59 | "@tailwindcss/forms": "^0.5.10", 60 | "@tailwindcss/postcss": "^4.0.0", 61 | "@tailwindcss/typography": "^0.5.16", 62 | "@types/bcrypt": "^5.0.2", 63 | "@types/node": "^20.11.30", 64 | "@types/nodemailer": "^6.4.14", 65 | "@types/pg": "^8.15.6", 66 | "@types/quill": "^2.0.14", 67 | "@types/react": "^19.1.2", 68 | "@types/react-dom": "^19.1.2", 69 | "@types/react-instantsearch-dom": "^6.12.7", 70 | "@types/react-slider": "^1.3.2", 71 | "eslint": "^8.57.0", 72 | "eslint-config-next": "15.1.7", 73 | "postcss": "^8.4.31", 74 | "prisma": "^7.0.1", 75 | "tailwindcss": "^4.0.0", 76 | "ts-node": "^10.9.2", 77 | "typescript": "^5.4.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/components/ui/input/TagInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Controller, Control } from "react-hook-form"; 3 | 4 | interface TagInputProps { 5 | name: string; 6 | label?: string; 7 | control: Control; 8 | placeholder?: string; 9 | } 10 | 11 | export const TagInput: React.FC = ({ 12 | name, 13 | label = "Enter tags", 14 | control, 15 | placeholder = "Type and press enter...", 16 | }) => { 17 | return ( 18 | { 23 | const handleKeyDown = (e: React.KeyboardEvent) => { 24 | const value = e.currentTarget.value.trim(); 25 | if ((e.key === "Enter" || e.key === ",") && value) { 26 | e.preventDefault(); 27 | const current = field.value || []; 28 | if (!current.includes(value)) { 29 | field.onChange([...current, value]); 30 | } 31 | e.currentTarget.value = ""; 32 | } 33 | }; 34 | 35 | const removeTag = (tagToRemove: string) => { 36 | const updated = field.value?.filter( 37 | (tag: string) => tag !== tagToRemove 38 | ); 39 | field.onChange(updated); 40 | }; 41 | 42 | return ( 43 |
44 | {label && ( 45 | 48 | )} 49 | 50 | 56 |
57 | {(field.value || []).map((tag: string) => ( 58 |
62 | {tag} 63 | 70 |
71 | ))} 72 |
73 |
74 | ); 75 | }} 76 | /> 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /public/images/icons/icon-03.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Footer/icons.tsx: -------------------------------------------------------------------------------- 1 | import { IconProps } from "@/types/icon-props"; 2 | 3 | export function AppStoreIcon(props: IconProps) { 4 | return ( 5 | 13 | 17 | 18 | ); 19 | } 20 | 21 | export function GooglePlayIcon(props: IconProps) { 22 | return ( 23 | 31 | 32 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Home/Hero/HeroCarousel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Swiper, SwiperSlide } from "swiper/react"; 3 | import { Autoplay, Pagination } from "swiper/modules"; 4 | 5 | // Import Swiper styles 6 | import "swiper/css/pagination"; 7 | import "swiper/css"; 8 | 9 | import Image from "next/image"; 10 | import Link from "next/link"; 11 | import { IHeroSlider } from "@/types/hero"; 12 | 13 | const HeroCarousal = ({ sliders }: { sliders: any }) => { 14 | return ( 15 | 28 | {sliders?.map((slider: IHeroSlider, key: number) => ( 29 | 30 |
31 |
32 |
33 | 34 | {slider?.discountRate}% 35 | 36 | 37 | Sale 38 |
39 | Off 40 |
41 |
42 | 43 |

44 | 45 | {slider?.product?.title} 46 | 47 |

48 | 49 |

50 | {slider?.product?.shortDescription?.slice(0, 100)} 51 |

52 | 53 | 57 | Shop Now 58 | 59 |
60 | 61 |
62 | headphone 69 |
70 |
71 |
72 | ))} 73 |
74 | ); 75 | }; 76 | 77 | export default HeroCarousal; 78 | -------------------------------------------------------------------------------- /public/images/icons/icon-05.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/payment/payment-05.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Header/DropdownGroup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect } from "react"; 4 | 5 | interface CustomDropdownProps { 6 | options: string[]; 7 | selected: string; 8 | setSelected: (value: string) => void; 9 | } 10 | 11 | const CustomDropdown: React.FC = ({ 12 | options, 13 | selected, 14 | setSelected, 15 | }) => { 16 | const [open, setOpen] = useState(false); 17 | const dropdownRef = useRef(null); 18 | 19 | useEffect(() => { 20 | const handleClickOutside = (event: MouseEvent) => { 21 | if ( 22 | dropdownRef.current && 23 | !dropdownRef.current.contains(event.target as Node) 24 | ) { 25 | setOpen(false); 26 | } 27 | }; 28 | 29 | document.addEventListener("mousedown", handleClickOutside); 30 | return () => { 31 | document.removeEventListener("mousedown", handleClickOutside); 32 | }; 33 | }, []); 34 | 35 | return ( 36 |
37 | 62 | 63 | {open && ( 64 |
    65 | {options.map((option) => ( 66 |
  • { 69 | setSelected(option); 70 | setOpen(false); 71 | }} 72 | className="px-4 py-2 text-sm font-medium rounded-lg cursor-pointer hover:bg-gray-3 text-dark" 73 | > 74 | {option} 75 |
  • 76 | ))} 77 |
78 | )} 79 |
80 | ); 81 | }; 82 | 83 | const DropdownGroup: React.FC = () => { 84 | const [country, setCountry] = useState("United State"); 85 | const [currency, setCurrency] = useState("USD"); 86 | 87 | return ( 88 |
89 | 94 | 99 |
100 | ); 101 | }; 102 | 103 | export default DropdownGroup; 104 | -------------------------------------------------------------------------------- /public/images/checkout/bank.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Home/BestSeller/ActionBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | CartIcon2, 4 | EyeIcon, 5 | HeartFilledIcon, 6 | HeartIcon, 7 | HeartSolid, 8 | } from "@/assets/icons"; 9 | import Link from "next/link"; 10 | import { useEffect, useState } from "react"; 11 | 12 | const ActionBtn = ({ 13 | handleClick, 14 | text, 15 | icon, 16 | addedToWishlist, 17 | isDisabled, 18 | }: any) => { 19 | const [show, setShow] = useState(false); 20 | const [hasMounted, setHasMounted] = useState(false); 21 | 22 | useEffect(() => { 23 | setHasMounted(true); 24 | }, []); 25 | 26 | if (!hasMounted) return null; 27 | 28 | return ( 29 |
setShow(true)} 32 | onMouseLeave={() => setShow(false)} 33 | > 34 |
35 | {/* Button */} 36 | 76 | 77 | {/* Tooltip */} 78 |

83 | {isDisabled ? "Out of stock" : addedToWishlist ? "Added" : text} 84 | 85 |

86 |
87 |
88 | ); 89 | }; 90 | 91 | export default ActionBtn; 92 | -------------------------------------------------------------------------------- /src/components/Home/Categories/CategoryCarouselArea.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCallback, useEffect, useRef, useState } from "react"; 3 | import { Swiper, SwiperSlide } from "swiper/react"; 4 | import type { SwiperRef } from "swiper/react"; 5 | 6 | // Import Swiper styles 7 | import { ChevronLeftIcon, ChevronRightIcon } from "@/assets/icons"; 8 | import "swiper/css"; 9 | import "swiper/css/navigation"; 10 | import SingleItem from "./SingleItem"; 11 | import { Category } from "@prisma/client"; 12 | 13 | export default function CategoryCarouselArea({ 14 | categories, 15 | }: { 16 | categories: Category[]; 17 | }) { 18 | const sliderRef = useRef(null); 19 | const [currentIndex, setCurrentIndex] = useState(0); 20 | const [isEnd, setIsEnd] = useState(false); 21 | 22 | const handlePrev = useCallback(() => { 23 | if (!sliderRef.current?.swiper) return; 24 | sliderRef.current.swiper.slidePrev(); 25 | }, []); 26 | 27 | const handleNext = useCallback(() => { 28 | if (!sliderRef.current?.swiper) return; 29 | sliderRef.current.swiper.slideNext(); 30 | }, []); 31 | 32 | useEffect(() => { 33 | if (sliderRef.current) { 34 | // @ts-ignore 35 | sliderRef.current.swiper.init(); 36 | } 37 | }, []); 38 | const onSlideChange = useCallback(() => { 39 | if (sliderRef.current?.swiper) { 40 | setCurrentIndex(sliderRef.current.swiper.activeIndex); 41 | setIsEnd(sliderRef.current.swiper.isEnd); 42 | } 43 | }, []); 44 | return ( 45 |
46 | {/* */} 47 |
48 |
49 |

50 | Browse by Category 51 |

52 |
53 | 54 |
55 | 65 | 66 | 76 |
77 |
78 | 79 | = 640px 85 | 0: { 86 | slidesPerView: 2, 87 | }, 88 | 1000: { 89 | slidesPerView: 4, 90 | // spaceBetween: 4, 91 | }, 92 | // when window width is >= 768px 93 | 1200: { 94 | slidesPerView: 6, 95 | }, 96 | }} 97 | > 98 | {categories.length > 0 && 99 | categories.map((item, key) => ( 100 | 101 | 102 | 103 | ))} 104 | 105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/components/Header/DesktopMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useState } from "react"; 5 | import type { MenuItem } from "./types"; 6 | import { usePathname } from "next/navigation"; 7 | 8 | interface DesktopMenuProps { 9 | menuData: MenuItem[]; 10 | stickyMenu: boolean; 11 | } 12 | 13 | const DesktopMenu = ({ menuData, stickyMenu }: DesktopMenuProps) => { 14 | const [activeDropdown, setActiveDropdown] = useState(null); 15 | const pathname = usePathname(); 16 | 17 | const handleMouseEnter = (index: number) => { 18 | setActiveDropdown(index); 19 | }; 20 | 21 | const handleMouseLeave = () => { 22 | setActiveDropdown(null); 23 | }; 24 | 25 | return ( 26 | 88 | ); 89 | }; 90 | 91 | export default DesktopMenu; 92 | -------------------------------------------------------------------------------- /src/components/Shop/shopData.ts: -------------------------------------------------------------------------------- 1 | const shopData = [ 2 | { 3 | title: "Havit HV-G69 USB Gamepad", 4 | reviews: 15, 5 | price: 59.0, 6 | discountedPrice: 29.0, 7 | id: 1, 8 | imgs: { 9 | thumbnails: [ 10 | "/images/products/product-1-sm-1.png", 11 | "/images/products/product-1-sm-2.png", 12 | ], 13 | previews: [ 14 | "/images/products/product-1-bg-1.png", 15 | "/images/products/product-1-bg-2.png", 16 | ], 17 | }, 18 | }, 19 | { 20 | title: "iPhone 14 Plus , 6/128GB", 21 | reviews: 5, 22 | price: 899.0, 23 | discountedPrice: 99.0, 24 | id: 2, 25 | imgs: { 26 | thumbnails: [ 27 | "/images/products/product-2-sm-1.png", 28 | "/images/products/product-2-sm-2.png", 29 | ], 30 | previews: [ 31 | "/images/products/product-2-bg-1.png", 32 | "/images/products/product-2-bg-2.png", 33 | ], 34 | }, 35 | }, 36 | { 37 | title: "Apple iMac M1 24-inch 2021", 38 | reviews: 5, 39 | price: 59.0, 40 | discountedPrice: 29.0, 41 | id: 3, 42 | imgs: { 43 | thumbnails: [ 44 | "/images/products/product-3-sm-1.png", 45 | "/images/products/product-3-sm-2.png", 46 | ], 47 | previews: [ 48 | "/images/products/product-3-bg-1.png", 49 | "/images/products/product-3-bg-2.png", 50 | ], 51 | }, 52 | }, 53 | { 54 | title: "MacBook Air M1 chip, 8/256GB", 55 | reviews: 6, 56 | price: 59.0, 57 | discountedPrice: 29.0, 58 | id: 4, 59 | imgs: { 60 | thumbnails: [ 61 | "/images/products/product-4-sm-1.png", 62 | "/images/products/product-4-sm-2.png", 63 | ], 64 | previews: [ 65 | "/images/products/product-4-bg-1.png", 66 | "/images/products/product-4-bg-2.png", 67 | ], 68 | }, 69 | }, 70 | { 71 | title: "Apple Watch Ultra", 72 | reviews: 3, 73 | price: 99.0, 74 | discountedPrice: 29.0, 75 | id: 5, 76 | imgs: { 77 | thumbnails: [ 78 | "/images/products/product-5-sm-1.png", 79 | "/images/products/product-5-sm-2.png", 80 | ], 81 | previews: [ 82 | "/images/products/product-5-bg-1.png", 83 | "/images/products/product-5-bg-2.png", 84 | ], 85 | }, 86 | }, 87 | { 88 | title: "Logitech MX Master 3 Mouse", 89 | reviews: 15, 90 | price: 59.0, 91 | discountedPrice: 29.0, 92 | id: 6, 93 | imgs: { 94 | thumbnails: [ 95 | "/images/products/product-6-sm-1.png", 96 | "/images/products/product-6-sm-2.png", 97 | ], 98 | previews: [ 99 | "/images/products/product-6-bg-1.png", 100 | "/images/products/product-6-bg-2.png", 101 | ], 102 | }, 103 | }, 104 | { 105 | title: "Apple iPad Air 5th Gen - 64GB", 106 | reviews: 15, 107 | price: 59.0, 108 | discountedPrice: 29.0, 109 | id: 7, 110 | imgs: { 111 | thumbnails: [ 112 | "/images/products/product-7-sm-1.png", 113 | "/images/products/product-7-sm-2.png", 114 | ], 115 | previews: [ 116 | "/images/products/product-7-bg-1.png", 117 | "/images/products/product-7-bg-2.png", 118 | ], 119 | }, 120 | }, 121 | { 122 | title: "Asus RT Dual Band Router", 123 | reviews: 15, 124 | price: 59.0, 125 | discountedPrice: 29.0, 126 | id: 8, 127 | imgs: { 128 | thumbnails: [ 129 | "/images/products/product-8-sm-1.png", 130 | "/images/products/product-8-sm-2.png", 131 | ], 132 | previews: [ 133 | "/images/products/product-8-bg-1.png", 134 | "/images/products/product-8-bg-2.png", 135 | ], 136 | }, 137 | }, 138 | ]; 139 | 140 | export default shopData; 141 | -------------------------------------------------------------------------------- /src/components/Home/PromoBanner/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import LargePromoBanner from "./LargePromoBanner"; 4 | import SmallPromoBanner from "./SmallPromoBanner"; 5 | 6 | const TreadmillPromoBanner = () => { 7 | return ( 8 |
9 | promo img 16 |
17 | 18 | Foldable Motorised Treadmill 19 | 20 |

21 | Workout At Home 22 |

23 |

Flat 20% off

24 | 28 | Grab Now 29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | const WatchPromoBanner = () => { 36 | return ( 37 |
38 | promo img 45 |
46 | 47 | Apple Watch Ultra 48 | 49 |

50 | Up to 40% off 51 |

52 |

53 | The aerospace-grade titanium case strikes the perfect balance of 54 | everything. 55 |

56 | 60 | Buy Now 61 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | const PromoBanner = () => { 68 | return ( 69 |
70 |
71 | 79 |
80 | 88 | 89 | 97 |
98 |
99 |
100 | ); 101 | }; 102 | 103 | export default PromoBanner; 104 | -------------------------------------------------------------------------------- /src/components/Wishlist/icons.tsx: -------------------------------------------------------------------------------- 1 | type IconProps = React.SVGProps; 2 | 3 | export function CircleCheckIcon(props: IconProps) { 4 | return ( 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export function HeartFolderIcon(props: IconProps) { 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | export function CircleXIcon(props: IconProps) { 36 | return ( 37 | 44 | 48 | 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /public/images/checkout/dhl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/Common/CartSidebarModal/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CloseLine } from "@/assets/icons"; 3 | import Link from "next/link"; 4 | import { useEffect } from "react"; 5 | import { useCart } from "@/hooks/useCart"; 6 | import EmptyCart from "./EmptyCart"; 7 | import SingleItem from "./SingleItem"; 8 | import { formatPrice } from "@/utils/formatePrice"; 9 | import { useRouter } from "next/navigation"; 10 | 11 | const CartSidebarModal = () => { 12 | const { 13 | cartCount, 14 | shouldDisplayCart, 15 | handleCartClick, 16 | cartDetails, 17 | totalPrice, 18 | } = useCart(); 19 | 20 | useEffect(() => { 21 | // closing modal while clicking outside 22 | function handleClickOutside(event: any) { 23 | if (!event.target.closest(".modal-content")) { 24 | handleCartClick(); 25 | } 26 | } 27 | 28 | if (shouldDisplayCart) { 29 | document.addEventListener("mousedown", handleClickOutside); 30 | } 31 | 32 | return () => { 33 | document.removeEventListener("mousedown", handleClickOutside); 34 | }; 35 | }, [shouldDisplayCart, handleCartClick]); 36 | 37 | const router = useRouter(); 38 | const handleCheckout = () => { 39 | router.push("/checkout"); 40 | handleCartClick(); 41 | }; 42 | 43 | return ( 44 | <> 45 |
49 | 50 | {/*
*/} 51 |
55 |
56 |

57 | Cart View 58 |

59 | 66 |
67 | 68 |
69 |
70 | {/* */} 71 | {cartCount ? ( 72 | <> 73 | {Object.values(cartDetails ?? {}).map((item, key) => ( 74 | 75 | ))} 76 | 77 | ) : ( 78 | 79 | )} 80 |
81 |
82 | 83 |
84 |
85 |

Subtotal:

86 | 87 |

88 | {totalPrice && formatPrice(totalPrice)} 89 |

90 |
91 | 92 |
93 | handleCartClick()} 95 | href="/cart" 96 | className="flex justify-center w-full px-6 py-3 text-base font-medium text-white duration-200 ease-out rounded-lg bg-blue hover:bg-blue-dark" 97 | > 98 | View Cart 99 | 100 | 101 | 107 |
108 |
109 |
110 | 111 | ); 112 | }; 113 | 114 | export default CartSidebarModal; 115 | --------------------------------------------------------------------------------