├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma-uml.png ├── prisma ├── migrations │ ├── 20240616182501_ecommerce │ │ └── migration.sql │ ├── 20240616183917_e_commerce │ │ └── migration.sql │ ├── 20240617211627_e_commerce │ │ └── migration.sql │ ├── 20240617211653_e_commerce │ │ └── migration.sql │ ├── 20240618132832_e_commerce │ │ └── migration.sql │ ├── 20240620150951_e_commerce │ │ └── migration.sql │ ├── 20240620152310_e_commerce │ │ └── migration.sql │ ├── 20240623194402_add_new_user_type │ │ └── migration.sql │ ├── 20240704155234_c_commerce │ │ └── migration.sql │ ├── 20240708135856_e_commerce │ │ └── migration.sql │ ├── 20240726185938_major_push │ │ └── migration.sql │ ├── 20240727161717_add_product_categories │ │ └── migration.sql │ ├── 20240727175839_up │ │ └── migration.sql │ ├── 20240727175941_up │ │ └── migration.sql │ ├── 20240806150734_product_attributes_etc │ │ └── migration.sql │ ├── 20240829074838_new │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── account-shop.jpg ├── brands │ ├── 1.jpg │ ├── 10.jpg │ ├── 11.jpg │ ├── 12.jpg │ ├── 13.webp │ ├── 14.jpg │ ├── 15.jpg │ ├── 16.jpg │ ├── 17.jpg │ ├── 18.jpg │ ├── 19.webp │ ├── 2.jpg │ ├── 20.webp │ ├── 21.webp │ ├── 22.webp │ ├── 23.webp │ ├── 24.webp │ ├── 25.webp │ ├── 26.webp │ ├── 27.webp │ ├── 28.webp │ ├── 29.webp │ ├── 3.jpg │ ├── 30.png │ ├── 31.png │ ├── 32.png │ ├── 33.png │ ├── 34.png │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ ├── 7.jpg │ ├── 8.jpg │ └── 9.jpg ├── logo-icon.png └── logo.png ├── src ├── actions │ ├── account │ │ ├── add-new-address.ts │ │ ├── add-new-review.ts │ │ ├── delete-review.ts │ │ ├── index.ts │ │ └── update-account-settings.ts │ ├── auth │ │ ├── index.ts │ │ ├── login.ts │ │ ├── logout.ts │ │ ├── new-password.ts │ │ ├── new-verification.ts │ │ ├── register.ts │ │ └── reset.ts │ ├── cart │ │ ├── add-to-cart.ts │ │ ├── change-quantity.ts │ │ ├── create-order.ts │ │ ├── delete-cart.ts │ │ ├── delete-from-cart.ts │ │ ├── delete-order.ts │ │ └── index.ts │ ├── dashboard │ │ └── create-new-category.ts │ ├── favorites │ │ ├── add-to-favorites.ts │ │ ├── delete-from-favorites.ts │ │ ├── index.ts │ │ └── toggle-favorite.ts │ ├── search │ │ ├── delete-search-history.ts │ │ ├── index.ts │ │ └── record-search-history.ts │ └── store │ │ ├── create-new-carrier.ts │ │ ├── create-new-discount.ts │ │ ├── create-new-product.ts │ │ ├── delete-product.ts │ │ ├── edit-product.ts │ │ ├── index.ts │ │ ├── new-application.ts │ │ └── update-product-status.ts ├── app │ ├── (auth) │ │ ├── error │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── new-password │ │ │ └── page.tsx │ │ ├── new-verification │ │ │ └── page.tsx │ │ ├── reset │ │ │ └── page.tsx │ │ ├── signin │ │ │ └── page.tsx │ │ └── signup │ │ │ └── page.tsx │ ├── (customerFacing) │ │ ├── c │ │ │ └── [category-slug] │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ ├── cart │ │ │ ├── checkout │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── favorites │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── p │ │ │ └── [product-slug] │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── payment-success │ │ │ └── page.tsx │ │ ├── products │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── s │ │ │ └── [store-slug] │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ └── search │ │ │ └── page.tsx │ ├── account │ │ ├── accountsettings │ │ │ └── page.tsx │ │ ├── addresses │ │ │ └── page.tsx │ │ ├── help │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── orders │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── reviews │ │ │ └── page.tsx │ │ └── store-application │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── categories │ │ │ └── [slug] │ │ │ │ └── route.ts │ │ ├── create-payment-intent │ │ │ └── route.ts │ │ ├── products │ │ │ └── route.ts │ │ └── s │ │ │ └── route.ts │ ├── dashboard │ │ ├── categories │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── not-found.tsx │ └── store │ │ ├── carriers │ │ └── page.tsx │ │ ├── discounts │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── mystore │ │ └── page.tsx │ │ ├── orders │ │ └── page.tsx │ │ └── products │ │ └── page.tsx ├── components │ ├── auth │ │ ├── FormSwitcher.tsx │ │ ├── LoginForm.tsx │ │ ├── NewPasswordForm.tsx │ │ ├── NewVerificationForm.tsx │ │ ├── RegisterForm.tsx │ │ ├── ResetForm.tsx │ │ └── index.ts │ ├── main │ │ ├── cart │ │ │ ├── CartTable.tsx │ │ │ ├── CartTableRow.tsx │ │ │ ├── CheckoutStripeForm.tsx │ │ │ ├── CreateOrderForm.tsx │ │ │ ├── OrderSummary.tsx │ │ │ ├── PathBasedPageTitle.tsx │ │ │ └── TotalPrice.tsx │ │ ├── footer │ │ │ └── Footer.tsx │ │ ├── header │ │ │ ├── DropdownNavMenu.tsx │ │ │ ├── PublicHeader.tsx │ │ │ └── ShoppingCart.tsx │ │ ├── home │ │ │ ├── BrandCarousel.tsx │ │ │ ├── EmblaCarouselDotButton.tsx │ │ │ ├── EmptyProductCarousel.tsx │ │ │ ├── HeroCarousel.tsx │ │ │ ├── ProductCarousel.tsx │ │ │ └── StyledProductCarousel.tsx │ │ ├── p │ │ │ ├── ProductDetailsTab.tsx │ │ │ ├── ProductReviews.tsx │ │ │ └── ProductShow.tsx │ │ ├── payment-success │ │ │ └── OrderDetailsShow.tsx │ │ └── products │ │ │ ├── FilterClean.tsx │ │ │ ├── ProductList.tsx │ │ │ ├── ProductSidebar.tsx │ │ │ ├── SidebarCategoryAttItem.tsx │ │ │ └── SidebarCategoryItem.tsx │ ├── protected │ │ ├── account │ │ │ ├── AccountSettingsForm.tsx │ │ │ ├── AddNewAddressForm.tsx │ │ │ ├── MyOrdersList.tsx │ │ │ ├── NewReviewForm.tsx │ │ │ ├── OrderItem.tsx │ │ │ ├── OrderProgress.tsx │ │ │ ├── PrivateHeader.tsx │ │ │ ├── ProductsForReview.tsx │ │ │ ├── ReviewShowCard.tsx │ │ │ ├── ReviewsList.tsx │ │ │ └── store-application │ │ │ │ └── ApplicationForm.tsx │ │ └── dashboard │ │ │ ├── CategoriesTable.tsx │ │ │ ├── CategoryAttributeInput.tsx │ │ │ ├── CategoryTableRow.tsx │ │ │ └── NewCategoryUpdateForm.tsx │ ├── shared │ │ └── ui │ │ │ ├── AutoPlayCarousel.tsx │ │ │ ├── Box.tsx │ │ │ ├── BreadcrumbNavigation.tsx │ │ │ ├── Button.tsx │ │ │ ├── ClientSideMap.tsx │ │ │ ├── Container.tsx │ │ │ ├── CustomTableHead.tsx │ │ │ ├── DatePicker.tsx │ │ │ ├── EducationalSiteDisclaimerBanner .tsx │ │ │ ├── EmptyTableBody.tsx │ │ │ ├── ErrorCard.tsx │ │ │ ├── Filter.tsx │ │ │ ├── Form.tsx │ │ │ ├── FormError.tsx │ │ │ ├── FormSelectInput.tsx │ │ │ ├── FormSuccess.tsx │ │ │ ├── HeaderDropdown.tsx │ │ │ ├── Heading.tsx │ │ │ ├── ImageInput.tsx │ │ │ ├── ImageZoom.tsx │ │ │ ├── Input.tsx │ │ │ ├── Logo.tsx │ │ │ ├── Map.tsx │ │ │ ├── MiniSpinner.tsx │ │ │ ├── MultiSelect.tsx │ │ │ ├── Pagination.tsx │ │ │ ├── PieChart.tsx │ │ │ ├── PriceRange.tsx │ │ │ ├── ProductCard.tsx │ │ │ ├── ProductPriceDisplay.tsx │ │ │ ├── Rating.tsx │ │ │ ├── Search.tsx │ │ │ ├── SearchLayout.tsx │ │ │ ├── Searchbar.tsx │ │ │ ├── Seperator.tsx │ │ │ ├── SideNavigation.tsx │ │ │ ├── SimpleLineChart.tsx │ │ │ ├── Socials.tsx │ │ │ ├── SortBy.tsx │ │ │ ├── SubmitButton.tsx │ │ │ ├── TextArea.tsx │ │ │ └── index.ts │ ├── store │ │ ├── ExcelButton.tsx │ │ ├── carriers │ │ │ ├── AddNewCarrierForm.tsx │ │ │ ├── CarrierTable.tsx │ │ │ └── CarrierTableRow.tsx │ │ ├── discounts │ │ │ ├── DiscountTable.tsx │ │ │ ├── DiscountTableRow.tsx │ │ │ └── NewDiscountForm.tsx │ │ ├── mystore │ │ │ ├── LatestOrderTableRow.tsx │ │ │ ├── LatestOrdersTable.tsx │ │ │ ├── OrderLineChart.tsx │ │ │ ├── OrderPieChart.tsx │ │ │ ├── PerformanceStatsPanel.tsx │ │ │ └── StatCard.tsx │ │ ├── orders │ │ │ ├── OrderTable.tsx │ │ │ └── OrderTableRow.tsx │ │ └── products │ │ │ ├── AttributeInput.tsx │ │ │ ├── HierarchicalCategorySelector.tsx │ │ │ ├── NewProductUpdateForm.tsx │ │ │ ├── ProductRowOperations.tsx │ │ │ ├── ProductTable.tsx │ │ │ ├── ProductTableRow.tsx │ │ │ └── SubCategoryInput.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── carousel.tsx │ │ ├── command.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── hover-card.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── select.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ └── tabs.tsx ├── config │ └── auth.config.ts ├── contexts │ ├── CartContext.tsx │ ├── CategoryContext.tsx │ └── Providers.tsx ├── hooks │ ├── use-current-role.ts │ ├── use-current-user.ts │ └── use-debounce.ts ├── lib │ ├── auth.ts │ ├── constants.ts │ ├── db.ts │ ├── fonts.ts │ ├── helpers.ts │ ├── services │ │ ├── address.ts │ │ ├── carrier.ts │ │ ├── cart.ts │ │ ├── category.ts │ │ ├── discount.ts │ │ ├── favorites.ts │ │ ├── image.ts │ │ ├── mail.ts │ │ ├── order.ts │ │ ├── product.ts │ │ ├── review.ts │ │ ├── search.ts │ │ ├── store.ts │ │ ├── token.ts │ │ └── user.ts │ ├── stripe.ts │ ├── supabase.ts │ ├── utils.ts │ └── validation │ │ ├── account.ts │ │ ├── auth.ts │ │ ├── cart.ts │ │ ├── category.ts │ │ └── store.ts ├── middleware.ts ├── paths.ts ├── routes.ts └── types │ ├── Carrier.ts │ ├── Cart.ts │ ├── Category.ts │ ├── CategoryAttribute.ts │ ├── Discount.ts │ ├── Favorites.ts │ ├── Order.ts │ ├── Product.ts │ ├── ProductAttributes.ts │ ├── Review.ts │ ├── Search.ts │ ├── Store.ts │ ├── User.ts │ └── index.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files, 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2024 Papeiron 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const nextConfig = { 4 | // reactStrictMode: false 5 | 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: 'https', 10 | hostname: 'lh3.googleusercontent.com', 11 | }, 12 | { 13 | protocol: 'https', 14 | hostname: 'githubusercontent.com', 15 | }, 16 | { 17 | protocol: 'https', 18 | hostname: 'fhnqpyisstbfjkvuzmvn.supabase.co', 19 | }, 20 | { 21 | protocol: 'https', 22 | hostname: 'flagcdn.com', 23 | pathname: '/**', 24 | }, 25 | { 26 | protocol: 'https', 27 | hostname: 'fakestoreapi.com', 28 | pathname: '/**', 29 | }, 30 | { 31 | protocol: 'https', 32 | hostname: 'fastly.picsum.photos', 33 | pathname: '/**', 34 | }, 35 | ], 36 | }, 37 | logging: { 38 | fetches: { 39 | fullUrl: true, 40 | }, 41 | }, 42 | }; 43 | 44 | export default nextConfig; 45 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma-uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/prisma-uml.png -------------------------------------------------------------------------------- /prisma/migrations/20240616182501_ecommerce/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `Users` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `title` VARCHAR(191) NOT NULL, 5 | `code` VARCHAR(191) NOT NULL, 6 | 7 | PRIMARY KEY (`id`) 8 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240616183917_e_commerce/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `code` on the `users` table. All the data in the column will be lost. 5 | - You are about to drop the column `title` on the `users` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE `users` DROP COLUMN `code`, 10 | DROP COLUMN `title`; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20240617211653_e_commerce/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `deleted_at` on the `categories` table. All the data in the column will be lost. 5 | - Added the required column `updated_at` to the `Adresses` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `updated_at` to the `Categories` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `image` to the `Products` table without a default value. This is not possible if the table is not empty. 8 | - Added the required column `content` to the `Reviews` table without a default value. This is not possible if the table is not empty. 9 | 10 | */ 11 | -- AlterTable 12 | ALTER TABLE `adresses` ADD COLUMN `updated_at` DATETIME(3) NOT NULL; 13 | 14 | -- AlterTable 15 | ALTER TABLE `categories` DROP COLUMN `deleted_at`, 16 | ADD COLUMN `updated_at` DATETIME(3) NOT NULL; 17 | 18 | -- AlterTable 19 | ALTER TABLE `products` ADD COLUMN `image` VARCHAR(191) NOT NULL; 20 | 21 | -- AlterTable 22 | ALTER TABLE `reviews` ADD COLUMN `content` VARCHAR(191) NOT NULL; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20240620152310_e_commerce/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `order_no` to the `Orders` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE `orders` ADD COLUMN `order_no` VARCHAR(191) NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240623194402_add_new_user_type/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `created_at` on the `account` table. All the data in the column will be lost. 5 | - You are about to drop the column `updated_at` on the `account` table. All the data in the column will be lost. 6 | - You are about to alter the column `role` on the `user` table. The data in that column could be lost. The data in that column will be cast from `Enum(EnumId(1))` to `Enum(EnumId(0))`. 7 | - You are about to drop the `session` table. If the table is not empty, all the data it contains will be lost. 8 | - You are about to drop the `verificationtoken` table. If the table is not empty, all the data it contains will be lost. 9 | - Added the required column `updatedAt` to the `Account` table without a default value. This is not possible if the table is not empty. 10 | 11 | */ 12 | -- DropForeignKey 13 | ALTER TABLE `session` DROP FOREIGN KEY `Session_userId_fkey`; 14 | 15 | -- AlterTable 16 | ALTER TABLE `account` DROP COLUMN `created_at`, 17 | DROP COLUMN `updated_at`, 18 | ADD COLUMN `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 19 | ADD COLUMN `updatedAt` DATETIME(3) NOT NULL; 20 | 21 | -- AlterTable 22 | ALTER TABLE `user` ADD COLUMN `password` VARCHAR(191) NULL, 23 | MODIFY `role` ENUM('ADMIN', 'USER') NOT NULL DEFAULT 'USER'; 24 | 25 | -- DropTable 26 | DROP TABLE `session`; 27 | 28 | -- DropTable 29 | DROP TABLE `verificationtoken`; 30 | -------------------------------------------------------------------------------- /prisma/migrations/20240727161717_add_product_categories/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `category_id` on the `product` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE `product` DROP FOREIGN KEY `Product_category_id_fkey`; 9 | 10 | -- AlterTable 11 | ALTER TABLE `product` DROP COLUMN `category_id`; 12 | 13 | -- CreateTable 14 | CREATE TABLE `ProductCategory` ( 15 | `id` VARCHAR(191) NOT NULL, 16 | `product_id` VARCHAR(191) NOT NULL, 17 | `category_id` VARCHAR(191) NOT NULL, 18 | 19 | UNIQUE INDEX `ProductCategory_product_id_category_id_key`(`product_id`, `category_id`), 20 | PRIMARY KEY (`id`) 21 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE `ProductCategory` ADD CONSTRAINT `ProductCategory_product_id_fkey` FOREIGN KEY (`product_id`) REFERENCES `Product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE `ProductCategory` ADD CONSTRAINT `ProductCategory_category_id_fkey` FOREIGN KEY (`category_id`) REFERENCES `Category`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /prisma/migrations/20240727175839_up/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `productcategory` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE `productcategory` DROP FOREIGN KEY `ProductCategory_category_id_fkey`; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE `productcategory` DROP FOREIGN KEY `ProductCategory_product_id_fkey`; 12 | 13 | -- DropTable 14 | DROP TABLE `productcategory`; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240727175941_up/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `ProductCategory` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `product_id` VARCHAR(191) NOT NULL, 5 | `category_id` VARCHAR(191) NOT NULL, 6 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 7 | `updated_at` DATETIME(3) NOT NULL, 8 | 9 | UNIQUE INDEX `ProductCategory_product_id_category_id_key`(`product_id`, `category_id`), 10 | PRIMARY KEY (`id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE `ProductCategory` ADD CONSTRAINT `ProductCategory_product_id_fkey` FOREIGN KEY (`product_id`) REFERENCES `Product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE `ProductCategory` ADD CONSTRAINT `ProductCategory_category_id_fkey` FOREIGN KEY (`category_id`) REFERENCES `Category`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /public/account-shop.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/account-shop.jpg -------------------------------------------------------------------------------- /public/brands/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/1.jpg -------------------------------------------------------------------------------- /public/brands/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/10.jpg -------------------------------------------------------------------------------- /public/brands/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/11.jpg -------------------------------------------------------------------------------- /public/brands/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/12.jpg -------------------------------------------------------------------------------- /public/brands/13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/13.webp -------------------------------------------------------------------------------- /public/brands/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/14.jpg -------------------------------------------------------------------------------- /public/brands/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/15.jpg -------------------------------------------------------------------------------- /public/brands/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/16.jpg -------------------------------------------------------------------------------- /public/brands/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/17.jpg -------------------------------------------------------------------------------- /public/brands/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/18.jpg -------------------------------------------------------------------------------- /public/brands/19.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/19.webp -------------------------------------------------------------------------------- /public/brands/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/2.jpg -------------------------------------------------------------------------------- /public/brands/20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/20.webp -------------------------------------------------------------------------------- /public/brands/21.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/21.webp -------------------------------------------------------------------------------- /public/brands/22.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/22.webp -------------------------------------------------------------------------------- /public/brands/23.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/23.webp -------------------------------------------------------------------------------- /public/brands/24.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/24.webp -------------------------------------------------------------------------------- /public/brands/25.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/25.webp -------------------------------------------------------------------------------- /public/brands/26.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/26.webp -------------------------------------------------------------------------------- /public/brands/27.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/27.webp -------------------------------------------------------------------------------- /public/brands/28.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/28.webp -------------------------------------------------------------------------------- /public/brands/29.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/29.webp -------------------------------------------------------------------------------- /public/brands/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/3.jpg -------------------------------------------------------------------------------- /public/brands/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/30.png -------------------------------------------------------------------------------- /public/brands/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/31.png -------------------------------------------------------------------------------- /public/brands/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/32.png -------------------------------------------------------------------------------- /public/brands/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/33.png -------------------------------------------------------------------------------- /public/brands/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/34.png -------------------------------------------------------------------------------- /public/brands/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/4.jpg -------------------------------------------------------------------------------- /public/brands/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/5.jpg -------------------------------------------------------------------------------- /public/brands/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/6.jpg -------------------------------------------------------------------------------- /public/brands/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/7.jpg -------------------------------------------------------------------------------- /public/brands/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/8.jpg -------------------------------------------------------------------------------- /public/brands/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/brands/9.jpg -------------------------------------------------------------------------------- /public/logo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/logo-icon.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onurdev17/next.js-ecommerce-app/7690d84454a7a2029e51b359f0fe4c044ad5f275/public/logo.png -------------------------------------------------------------------------------- /src/actions/account/delete-review.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { fetchReviewsByUser } from '@/lib/services/review'; 6 | import { paths } from '@/paths'; 7 | import { revalidatePath } from 'next/cache'; 8 | 9 | export type FormStateType = { 10 | error?: { message?: string[] }; 11 | success?: { message?: string }; 12 | }; 13 | 14 | type DeleteFromCartProps = { 15 | reviewId: string; 16 | formState: FormStateType; 17 | }; 18 | 19 | const deleteReview = async ( 20 | reviewId: string, 21 | formState: FormStateType, 22 | ): Promise => { 23 | const user = await currentUser(); 24 | 25 | if (!user || !user.id) { 26 | return { error: { message: ['You must be signed in to do this.'] } }; 27 | } 28 | 29 | const reviews = await fetchReviewsByUser(user.id); 30 | 31 | const isHisReview = reviews?.some((r) => r.id === reviewId); 32 | 33 | if (!isHisReview) { 34 | return { error: { message: ['You do not have a permission to do that.'] } }; 35 | } 36 | 37 | try { 38 | await db.review.delete({ 39 | where: { 40 | id: reviewId, 41 | }, 42 | }); 43 | } catch (err: unknown) { 44 | if (err instanceof Error) { 45 | return { 46 | error: { 47 | message: [err.message], 48 | }, 49 | }; 50 | } else { 51 | return { 52 | error: { 53 | message: ['Something went wrong.'], 54 | }, 55 | }; 56 | } 57 | } 58 | 59 | return { 60 | success: { 61 | message: 'Review successfully deleted.', 62 | }, 63 | }; 64 | }; 65 | 66 | export default deleteReview; 67 | -------------------------------------------------------------------------------- /src/actions/account/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createNewAddress } from './add-new-address'; 2 | export { default as createNewReview } from './add-new-review'; 3 | export { default as deleteReview } from './delete-review'; 4 | export { default as updateAccountSettings } from './update-account-settings'; 5 | -------------------------------------------------------------------------------- /src/actions/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default as login } from './login'; 2 | export { default as logout } from './logout'; 3 | export { default as newPassword } from './new-password'; 4 | export { default as newVerification } from './new-verification'; 5 | export { default as register } from './register'; 6 | export { default as reset } from './reset'; 7 | -------------------------------------------------------------------------------- /src/actions/auth/logout.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { signOut } from '@/lib/auth'; 4 | 5 | const logout = async () => { 6 | await signOut(); 7 | }; 8 | 9 | export default logout; 10 | -------------------------------------------------------------------------------- /src/actions/auth/new-verification.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { getVerificationTokenByToken } from '@/lib/services/token'; 5 | import { getUserByEmail } from '@/lib/services/user'; 6 | 7 | const newVerification = async (token: string) => { 8 | const existingToken = await getVerificationTokenByToken(token); 9 | 10 | if (!existingToken) { 11 | return { errors: { _form: ['Token does not exist!'] } }; 12 | } 13 | 14 | const hasExpired = new Date(existingToken.expires) < new Date(); 15 | 16 | if (hasExpired) { 17 | return { errors: { _form: ['Token has expired!'] } }; 18 | } 19 | 20 | const existingUser = await getUserByEmail(existingToken.email); 21 | 22 | if (!existingUser) { 23 | return { errors: { _form: ['Email does not exist!'] } }; 24 | } 25 | 26 | await db.user.update({ 27 | where: { id: existingUser.id }, 28 | data: { 29 | emailVerified: new Date(), 30 | email: existingToken.email, 31 | }, 32 | }); 33 | 34 | await db.verificationToken.delete({ 35 | where: { id: existingToken.id }, 36 | }); 37 | 38 | return { success: { message: 'Email verified.' } }; 39 | }; 40 | 41 | export default newVerification; 42 | -------------------------------------------------------------------------------- /src/actions/auth/reset.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { sendResetPasswordEmail } from '@/lib/services/mail'; 4 | import { generatePasswordResetToken } from '@/lib/services/token'; 5 | import { getUserByEmail } from '@/lib/services/user'; 6 | import { resetSchema } from '@/lib/validation/auth'; 7 | 8 | type ResetFormState = { 9 | errors?: { 10 | email?: string[]; 11 | _form?: string[]; 12 | }; 13 | success?: { message?: string }; 14 | }; 15 | 16 | const reset = async ( 17 | formState: ResetFormState, 18 | formData: FormData, 19 | ): Promise => { 20 | const validatedFields = resetSchema.safeParse({ 21 | email: formData.get('email'), 22 | }); 23 | 24 | if (!validatedFields.success) { 25 | return { 26 | errors: validatedFields.error.flatten().fieldErrors, 27 | }; 28 | } 29 | 30 | const { email } = validatedFields.data; 31 | 32 | const existingUser = await getUserByEmail(email); 33 | 34 | if (!existingUser) { 35 | return { 36 | errors: { 37 | _form: ['Email not found!'], 38 | }, 39 | }; 40 | } 41 | 42 | const resetVerificationToken = await generatePasswordResetToken(email); 43 | 44 | await sendResetPasswordEmail( 45 | resetVerificationToken.email, 46 | resetVerificationToken.token, 47 | ); 48 | 49 | return { 50 | success: { message: 'Reset email sent!' }, 51 | }; 52 | }; 53 | 54 | export default reset; 55 | -------------------------------------------------------------------------------- /src/actions/cart/change-quantity.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { fetchCartByUser } from '@/lib/services/cart'; 6 | import { revalidateTag } from 'next/cache'; 7 | 8 | export type FormStateType = { 9 | error?: { message?: string[] }; 10 | success?: { message?: string }; 11 | }; 12 | 13 | type ChangeQuantityProps = { 14 | value: number; 15 | productId: string; 16 | cartItemId: string; 17 | formState: FormStateType; 18 | }; 19 | 20 | const changeQuantity = async ({ 21 | value, 22 | productId, 23 | cartItemId, 24 | formState, 25 | }: ChangeQuantityProps) => { 26 | const user = await currentUser(); 27 | 28 | if (!user) { 29 | return { error: { message: ['You must be signed in to do this.'] } }; 30 | } 31 | 32 | const cart = await fetchCartByUser(user.id as string); 33 | 34 | const isOwnProduct = cart?.cart_items.some((item) => { 35 | if (item.product_id === productId) { 36 | return true; 37 | } 38 | }); 39 | 40 | if (!isOwnProduct) { 41 | return { 42 | error: { 43 | message: ['You do not have permission to do that.'], 44 | }, 45 | }; 46 | } 47 | 48 | try { 49 | await db.cartItem.update({ 50 | where: { 51 | id: cartItemId, 52 | }, 53 | data: { 54 | quantity: value, 55 | }, 56 | }); 57 | } catch (err: unknown) { 58 | if (err instanceof Error) { 59 | return { 60 | error: { 61 | message: [err.message], 62 | }, 63 | }; 64 | } else { 65 | return { 66 | error: { 67 | message: ['Something went wrong.'], 68 | }, 69 | }; 70 | } 71 | } 72 | 73 | revalidateTag('cart'); 74 | 75 | return { 76 | success: { 77 | message: 'Cart updated!', 78 | }, 79 | }; 80 | }; 81 | 82 | export default changeQuantity; 83 | -------------------------------------------------------------------------------- /src/actions/cart/delete-cart.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { revalidatePath, revalidateTag } from 'next/cache'; 4 | 5 | import { db } from '@/lib/db'; 6 | import { getUserById } from '@/lib/services/user'; 7 | import { paths } from '@/paths'; 8 | 9 | const deleteCart = async (userId: string) => { 10 | const currentUser = await getUserById(userId); 11 | 12 | if (currentUser?.cart && currentUser?.cart.length > 0) { 13 | await db.cart.delete({ 14 | where: { 15 | id: currentUser.cart[0].id, 16 | }, 17 | }); 18 | 19 | revalidatePath(paths.cart()); 20 | revalidateTag('cart'); 21 | } else { 22 | return null; 23 | } 24 | 25 | // TODO: fix the unexpected behaviour (cant' revalidate cart) 26 | }; 27 | 28 | export default deleteCart; 29 | -------------------------------------------------------------------------------- /src/actions/cart/delete-from-cart.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { fetchCartByUser } from '@/lib/services/cart'; 6 | import { revalidateTag } from 'next/cache'; 7 | 8 | export type FormStateType = { 9 | error?: { message?: string[] }; 10 | success?: { message?: string }; 11 | }; 12 | 13 | type DeleteFromCartProps = { 14 | cartItemId: string; 15 | productId: string; 16 | formState: FormStateType; 17 | }; 18 | 19 | const deleteFromCart = async ({ 20 | productId, 21 | cartItemId, 22 | formState, 23 | }: DeleteFromCartProps): Promise => { 24 | const user = await currentUser(); 25 | 26 | if (!user) { 27 | return { error: { message: ['You must be signed in to do this.'] } }; 28 | } 29 | 30 | const cart = await fetchCartByUser(user.id as string); 31 | 32 | const isOwnProduct = cart?.cart_items.some((item) => { 33 | if (item.product_id === productId) { 34 | return true; 35 | } 36 | }); 37 | 38 | if (!isOwnProduct) { 39 | return { 40 | error: { 41 | message: ['You do not have permission to do that'], 42 | }, 43 | }; 44 | } 45 | 46 | try { 47 | await db.cartItem.delete({ 48 | where: { 49 | id: cartItemId, 50 | }, 51 | }); 52 | } catch (err: unknown) { 53 | if (err instanceof Error) { 54 | return { 55 | error: { 56 | message: [err.message], 57 | }, 58 | }; 59 | } else { 60 | return { 61 | error: { 62 | message: ['Something went wrong.'], 63 | }, 64 | }; 65 | } 66 | } 67 | 68 | revalidateTag('cart'); 69 | 70 | 71 | return { 72 | success: { 73 | message: 'Product successfully deleted from the cart.', 74 | }, 75 | }; 76 | }; 77 | 78 | export default deleteFromCart; 79 | -------------------------------------------------------------------------------- /src/actions/cart/delete-order.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | 5 | const deleteOrder = (orderNo: string) => { 6 | try { 7 | db.order.delete({ 8 | where: { 9 | order_no: orderNo, 10 | }, 11 | }); 12 | } catch (err: unknown) { 13 | if (err instanceof Error) { 14 | return { 15 | error: { 16 | message: [err.message], 17 | }, 18 | }; 19 | } else { 20 | return { 21 | error: { 22 | message: ['Something went wrong.'], 23 | }, 24 | }; 25 | } 26 | } 27 | }; 28 | 29 | export default deleteOrder; 30 | -------------------------------------------------------------------------------- /src/actions/cart/index.ts: -------------------------------------------------------------------------------- 1 | export { default as addToCart } from './add-to-cart'; 2 | export { default as deleteFromCart } from './delete-from-cart'; 3 | export { default as changeQuantity } from './change-quantity'; 4 | export { default as createOrder } from './create-order'; 5 | export { default as deleteOrder } from './delete-order'; 6 | export { default as deleteCart } from './delete-cart'; 7 | -------------------------------------------------------------------------------- /src/actions/favorites/add-to-favorites.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { revalidateTag } from 'next/cache'; 6 | 7 | type FormStateType = { 8 | error?: { message?: { [key: string]: string[] } }; 9 | success?: { message?: string }; 10 | }; 11 | 12 | type AddToFavoritesType = { 13 | productId: string; 14 | formState: FormStateType; 15 | }; 16 | 17 | const addToFavorites = async ({ 18 | productId, 19 | formState, 20 | }: AddToFavoritesType): Promise => { 21 | const user = await currentUser(); 22 | 23 | 24 | if (!user) { 25 | return { error: { message: { auth: ['You must be signed in to do this.'] } } }; 26 | } 27 | 28 | try { 29 | const fav = await db.favorite.findFirst({ 30 | where: { 31 | product_id: productId, 32 | }, 33 | }); 34 | 35 | if (fav) { 36 | return { error: { message: { auth: ["It's already in your favorite list."] } } }; 37 | } 38 | 39 | await db.favorite.create({ 40 | data: { 41 | product: { 42 | connect: { 43 | id: productId, 44 | }, 45 | }, 46 | user: { 47 | connect: { 48 | id: user.id, 49 | }, 50 | }, 51 | }, 52 | }); 53 | } catch (err: unknown) { 54 | if (err instanceof Error) { 55 | return { 56 | error: { 57 | message: { _form: [err.message] }, 58 | }, 59 | }; 60 | } else { 61 | return { 62 | error: { 63 | message: { _form: ['Something went wrong.'] }, 64 | }, 65 | }; 66 | } 67 | } 68 | 69 | revalidateTag('favorites'); 70 | 71 | return { 72 | success: { 73 | message: 'Succesfully added to favorites.', 74 | }, 75 | }; 76 | }; 77 | 78 | export default addToFavorites; 79 | -------------------------------------------------------------------------------- /src/actions/favorites/delete-from-favorites.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { paths } from '@/paths'; 6 | import { revalidatePath, revalidateTag } from 'next/cache'; 7 | 8 | type FormStateType = { 9 | error?: { message?: { [key: string]: string[] } }; 10 | success?: { message?: string }; 11 | }; 12 | 13 | type AddToFavoritesType = { 14 | favoriteId: string; 15 | productSlug: string; 16 | formState: FormStateType; 17 | }; 18 | 19 | const deleteFromFavorites = async ({ 20 | favoriteId, 21 | productSlug, 22 | formState, 23 | }: AddToFavoritesType): Promise => { 24 | const user = await currentUser(); 25 | 26 | if (!user) { 27 | return { error: { message: { auth: ['You must be signed in to do this.'] } } }; 28 | } 29 | let favorite; 30 | try { 31 | const fav = await db.favorite.findFirst({ 32 | where: { 33 | id: favoriteId, 34 | }, 35 | }); 36 | 37 | if (!fav) { 38 | return { error: { message: { auth: ["It's not in your favorite list."] } } }; 39 | } 40 | 41 | favorite = await db.favorite.delete({ 42 | where: { 43 | id: favoriteId, 44 | }, 45 | }); 46 | } catch (err: unknown) { 47 | if (err instanceof Error) { 48 | return { 49 | error: { 50 | message: { _form: [err.message] }, 51 | }, 52 | }; 53 | } else { 54 | return { 55 | error: { 56 | message: { _form: ['Something went wrong.'] }, 57 | }, 58 | }; 59 | } 60 | } 61 | 62 | revalidateTag('favorites'); 63 | revalidatePath(paths.productShow(productSlug)); 64 | 65 | return { 66 | success: { 67 | message: 'Succesfully added to favorites.', 68 | }, 69 | }; 70 | }; 71 | 72 | export default deleteFromFavorites; 73 | -------------------------------------------------------------------------------- /src/actions/favorites/index.ts: -------------------------------------------------------------------------------- 1 | export { default as addToFavorites } from './add-to-favorites'; 2 | export { default as deleteFromFavorites } from './delete-from-favorites'; 3 | export { default as toggleFavorite } from './toggle-favorite'; 4 | -------------------------------------------------------------------------------- /src/actions/favorites/toggle-favorite.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { paths } from '@/paths'; 6 | import { revalidatePath, revalidateTag } from 'next/cache'; 7 | 8 | export default async function toggleFavorite(productId: string, productSlug: string) { 9 | const user = await currentUser(); 10 | 11 | if (!user) { 12 | return { error: 'You must be signed in to do this.' }; 13 | } 14 | 15 | try { 16 | const existingFavorite = await db.favorite.findFirst({ 17 | where: { 18 | product_id: productId, 19 | user_id: user.id, 20 | }, 21 | }); 22 | 23 | if (existingFavorite) { 24 | await db.favorite.delete({ 25 | where: { 26 | id: existingFavorite.id, 27 | }, 28 | }); 29 | } else { 30 | await db.favorite.create({ 31 | data: { 32 | product: { connect: { id: productId } }, 33 | user: { connect: { id: user.id } }, 34 | }, 35 | }); 36 | } 37 | 38 | revalidatePath(paths.productShow(productSlug)); 39 | revalidateTag('favorites'); 40 | 41 | return { success: 'Favorite status updated successfully.' }; 42 | } catch (err) { 43 | console.error(err); 44 | return { error: 'An error occurred while updating favorite status.' }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/actions/search/delete-search-history.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { revalidateTag } from 'next/cache'; 6 | 7 | const deleteSearchHistory = async () => { 8 | const user = await currentUser(); 9 | 10 | try { 11 | await db.searchHistory.deleteMany({ 12 | where: { 13 | user_id: user?.id, 14 | }, 15 | }); 16 | } catch (err: unknown) { 17 | console.error(err instanceof Error ? err.message : 'Something went wrong.'); 18 | } 19 | 20 | revalidateTag('search-history'); 21 | }; 22 | 23 | export default deleteSearchHistory; 24 | -------------------------------------------------------------------------------- /src/actions/search/index.ts: -------------------------------------------------------------------------------- 1 | export { default as recordSearchQuery } from './record-search-history'; 2 | export { default as deleteSearchHistory } from './delete-search-history'; 3 | -------------------------------------------------------------------------------- /src/actions/search/record-search-history.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { db } from '@/lib/db'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { revalidateTag } from 'next/cache'; 6 | 7 | const recordSearchQuery = async (query: string) => { 8 | const user = await currentUser(); 9 | 10 | try { 11 | await db.searchHistory.create({ 12 | data: { 13 | query, 14 | user: { 15 | connect: { 16 | id: user?.id, 17 | }, 18 | }, 19 | }, 20 | }); 21 | } catch (err: unknown) { 22 | if (err instanceof Error) { 23 | return { 24 | error: { 25 | message: err.message, 26 | }, 27 | }; 28 | } else { 29 | return { 30 | error: { 31 | message: 'Something went wrong.', 32 | }, 33 | }; 34 | } 35 | } 36 | 37 | revalidateTag('search-history'); 38 | }; 39 | 40 | export default recordSearchQuery; 41 | -------------------------------------------------------------------------------- /src/actions/store/delete-product.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | import { db } from '@/lib/db'; 5 | import { fetchProductsByStore } from '@/lib/services/product'; 6 | import { paths } from '@/paths'; 7 | import { revalidatePath } from 'next/cache'; 8 | 9 | type FormStateType = { 10 | error?: { message?: string }; 11 | success?: { message?: string }; 12 | }; 13 | 14 | const deleteProduct = async ( 15 | id: string, 16 | formState: FormStateType, 17 | ): Promise => { 18 | const session = await auth(); 19 | 20 | if (!session || !session.user || !session?.user?.store) { 21 | return { 22 | error: { 23 | message: 'You must be signed in to do this', 24 | }, 25 | }; 26 | } 27 | 28 | const { products } = await fetchProductsByStore({ 29 | storeId: session.user.store.id, 30 | }); 31 | const productIds = products?.map((product) => product.id); 32 | 33 | if (!productIds?.includes(id)) { 34 | return { 35 | error: { 36 | message: 'You are not allowed to do that', 37 | }, 38 | }; 39 | } 40 | 41 | try { 42 | await db.product.delete({ 43 | where: { 44 | id, 45 | }, 46 | }); 47 | } catch (err: unknown) { 48 | if (err instanceof Error) { 49 | return { 50 | error: { 51 | message: err.message, 52 | }, 53 | }; 54 | } else { 55 | return { 56 | error: { 57 | message: 'Something went wrong', 58 | }, 59 | }; 60 | } 61 | } 62 | 63 | revalidatePath(paths.productsTable()); 64 | 65 | return { 66 | success: { 67 | message: 'Product successfully deleted', 68 | }, 69 | }; 70 | }; 71 | 72 | export default deleteProduct; 73 | -------------------------------------------------------------------------------- /src/actions/store/index.ts: -------------------------------------------------------------------------------- 1 | export { default as newApplication } from './new-application'; 2 | export { default as createNewProduct } from './create-new-product'; 3 | export { default as updateProductStatus } from './update-product-status'; 4 | export { default as editProduct } from './edit-product'; 5 | export { default as deleteProduct } from './delete-product'; 6 | export { default as createNewDiscount } from './create-new-discount'; 7 | export { default as createNewCarrier } from './create-new-carrier'; 8 | -------------------------------------------------------------------------------- /src/actions/store/update-product-status.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | import { db } from '@/lib/db'; 5 | import { paths } from '@/paths'; 6 | import { revalidatePath } from 'next/cache'; 7 | 8 | const updateProductStatus = async (id: string, status: boolean) => { 9 | const session = await auth(); 10 | 11 | if (!session || !session.user || !session.user.id || !session.user.store) { 12 | throw new Error('You have to signed in to that'); 13 | } 14 | 15 | let product; 16 | try { 17 | product = await db.product.findUnique({ 18 | where: { 19 | id, 20 | }, 21 | include: { 22 | categories: { 23 | include: { 24 | category: { 25 | include: { 26 | sub_categories: true, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }); 33 | } catch { 34 | throw new Error('An unexpected error occurred'); 35 | } 36 | 37 | if (product?.store_id !== session.user.store.id) { 38 | throw new Error('Store not found'); 39 | } 40 | 41 | try { 42 | await db.product.update({ 43 | where: { 44 | id, 45 | }, 46 | data: { 47 | status, 48 | }, 49 | }); 50 | } catch { 51 | throw new Error('An unexpected error occurred'); 52 | } 53 | 54 | const subCategory = product.categories.find((c) => { 55 | if (c.category.sub_categories.length === 0) { 56 | return c; 57 | } 58 | })?.category; 59 | 60 | if (subCategory) revalidatePath(paths.productsListByCategory(subCategory.slug)); 61 | }; 62 | 63 | export default updateProductStatus; 64 | -------------------------------------------------------------------------------- /src/app/(auth)/error/page.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from '@/components/shared/ui'; 2 | 3 | function AuthErrorPage() { 4 | return ; 5 | } 6 | 7 | export default AuthErrorPage; 8 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Logo from '@/components/shared/ui/Logo'; 2 | 3 | function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 |
7 | 12 |
13 |
{children}
14 |
15 | ); 16 | } 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /src/app/(auth)/new-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { NewPasswordForm } from '@/components/auth'; 2 | import { MiniSpinner } from '@/components/shared/ui'; 3 | import { Suspense } from 'react'; 4 | 5 | function NewPasswordPage() { 6 | return ( 7 |
8 |
9 | 12 | 13 |

14 | } 15 | > 16 | 17 |
18 |
19 |
20 | ); 21 | } 22 | 23 | export default NewPasswordPage; 24 | -------------------------------------------------------------------------------- /src/app/(auth)/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { NewVerificationForm } from '@/components/auth'; 4 | import { MiniSpinner } from '@/components/shared/ui'; 5 | import { Suspense } from 'react'; 6 | 7 | function NewVerificationPage() { 8 | return ( 9 |
10 | 13 | 14 |

15 | } 16 | > 17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | export default NewVerificationPage; 24 | -------------------------------------------------------------------------------- /src/app/(auth)/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { FormSwitcher } from '@/components/auth'; 2 | import ResetForm from '@/components/auth/ResetForm'; 3 | 4 | function ResetPasswordPage() { 5 | return ( 6 |
7 |
8 | 9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default ResetPasswordPage; 16 | -------------------------------------------------------------------------------- /src/app/(auth)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { FormSwitcher, LoginForm } from '@/components/auth'; 2 | import { MiniSpinner } from '@/components/shared/ui'; 3 | import { Suspense } from 'react'; 4 | 5 | function SignInPage() { 6 | return ( 7 |
8 |
9 | 10 | 13 | 14 |

15 | } 16 | > 17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | 24 | export default SignInPage; 25 | -------------------------------------------------------------------------------- /src/app/(auth)/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { FormSwitcher, RegisterForm } from '@/components/auth'; 2 | 3 | function SignUpPage() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 |
11 | ); 12 | } 13 | 14 | export default SignUpPage; 15 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/cart/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | import CreateOrderForm from '@/components/main/cart/CreateOrderForm'; 4 | import OrderSummary from '@/components/main/cart/OrderSummary'; 5 | import { currentUser, getUniqueStoreIds } from '@/lib/helpers'; 6 | import { fetchAddressesByUser } from '@/lib/services/address'; 7 | import { fetchCartByUser } from '@/lib/services/cart'; 8 | import { getStoresByIds } from '@/lib/services/store'; 9 | 10 | async function CheckoutPage() { 11 | const user = await currentUser(); 12 | 13 | if (!user || !user.id) { 14 | redirect('/products'); 15 | } 16 | 17 | const [cart, addresses] = await Promise.all([ 18 | fetchCartByUser(user.id), 19 | fetchAddressesByUser(user.id), 20 | ]); 21 | 22 | if (!cart || cart.cart_items.length < 1) { 23 | redirect('/products'); 24 | } 25 | 26 | const uniqueStoreIds = getUniqueStoreIds(cart); 27 | const stores = await getStoresByIds(uniqueStoreIds); 28 | 29 | const carriers = stores.flatMap((store) => 30 | store.carriers.map((carrier) => ({ 31 | ...carrier, 32 | store_id: store.id, 33 | })), 34 | ); 35 | 36 | const userWithDetails = { 37 | user, 38 | addresses, 39 | carriers, 40 | }; 41 | 42 | return ( 43 |
44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 | ); 53 | } 54 | 55 | export default CheckoutPage; 56 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/cart/layout.tsx: -------------------------------------------------------------------------------- 1 | import PathBasedPageTitle from '@/components/main/cart/PathBasedPageTitle'; 2 | import { Container } from '@/components/shared/ui'; 3 | import { CartProvider } from '@/contexts/CartContext'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import { fetchCartByUser } from '@/lib/services/cart'; 6 | import { CartWithDetails } from '@/types/Cart'; 7 | 8 | async function Layout({ children }: { children: React.ReactNode }) { 9 | const user = await currentUser(); 10 | 11 | let cart: CartWithDetails | null = null; 12 | if (user && user.id) { 13 | cart = await fetchCartByUser(user?.id); 14 | } 15 | 16 | if (!cart) { 17 | return

Your cart is empty

; 18 | } 19 | 20 | return ( 21 | 22 | 23 |
24 | 25 |
26 | {!cart ?

Your cart is empty

: children} 27 |
28 |
29 | ); 30 | } 31 | 32 | export default Layout; 33 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import CartTable from '@/components/main/cart/CartTable'; 2 | import TotalPrice from '@/components/main/cart/TotalPrice'; 3 | import { Seperator } from '@/components/shared/ui'; 4 | 5 | async function CartPage() { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default CartPage; 18 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/favorites/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@/components/shared/ui'; 2 | 3 | function Layout({ children }: { children: React.ReactNode }) { 4 | return {children}; 5 | } 6 | 7 | export default Layout; 8 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | import { ProductCard } from '@/components/shared/ui'; 2 | import { currentUser } from '@/lib/helpers'; 3 | import { fetchFavoritesByUser } from '@/lib/services/favorites'; 4 | 5 | async function FavoritesPage() { 6 | const user = await currentUser(); 7 | 8 | if (!user || !user?.id) { 9 | return

Sign in to add to favorites

; 10 | } 11 | 12 | const favorites = await fetchFavoritesByUser(user?.id); 13 | 14 | return ( 15 |
16 |
17 | {favorites?.map((f) => ( 18 | 19 | ))} 20 |
21 |
22 | ); 23 | } 24 | 25 | export default FavoritesPage; 26 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from '@/components/main/footer/Footer'; 2 | import PublicHeader from '@/components/main/header/PublicHeader'; 3 | import EducationalSiteDisclaimerBanner from '@/components/shared/ui/EducationalSiteDisclaimerBanner '; 4 | 5 | function Layout({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
8 | 9 | 10 |
{children}
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default Layout; 18 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/p/[product-slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BreadcrumbNavigation } from '@/components/shared/ui'; 2 | import Container from '@/components/shared/ui/Container'; 3 | import { 4 | fetchCategoryChainByProduct, 5 | fetchMainCategories, 6 | } from '@/lib/services/category'; 7 | import { fetchProductBySlug } from '@/lib/services/product'; 8 | import { notFound } from 'next/navigation'; 9 | 10 | type LayoutProps = { 11 | children: React.ReactNode; 12 | params: { 13 | [key: string]: string | string[]; 14 | }; 15 | }; 16 | 17 | async function Layout({ children, params }: LayoutProps) { 18 | const productSlug = params['product-slug']; 19 | 20 | const [product, mainCategories] = await Promise.all([ 21 | fetchProductBySlug(productSlug as string), 22 | fetchMainCategories(), 23 | ]); 24 | 25 | if (!product) { 26 | notFound(); 27 | } 28 | 29 | const parentCategories = await fetchCategoryChainByProduct(product.slug); 30 | 31 | const editedParentCategories = parentCategories?.map((cat) => ({ 32 | id: cat.id, 33 | name: cat.name, 34 | slug: cat.slug, 35 | })); 36 | 37 | return ( 38 | 39 | 43 | {children} 44 | 45 | ); 46 | } 47 | 48 | export default Layout; 49 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/payment-success/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { deleteCart } from '@/actions/cart'; 4 | import OrderDetailsShow from '@/components/main/payment-success/OrderDetailsShow'; 5 | import { currentUser } from '@/lib/helpers'; 6 | import { fetchOrderByOrderNo } from '@/lib/services/order'; 7 | import { MiniSpinner } from '@/components/shared/ui'; 8 | 9 | async function PaymentSuccessPage({ 10 | searchParams: { order }, 11 | }: { 12 | searchParams: { order: string }; 13 | }) { 14 | const user = await currentUser(); 15 | 16 | // if (order) { 17 | // await deleteCart(user?.id as string); 18 | // } 19 | 20 | const orderToShow = await fetchOrderByOrderNo(order); 21 | 22 | return ( 23 |
24 | {orderToShow ? : } 25 | 26 | Return to Homepage 27 | 28 |
29 | ); 30 | } 31 | 32 | export default PaymentSuccessPage; 33 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/products/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import ProductSidebar from '@/components/main/products/ProductSidebar'; 4 | import { Heading, SortBy } from '@/components/shared/ui'; 5 | import BreadcrumbNavigation from '@/components/shared/ui/BreadcrumbNavigation'; 6 | import Container from '@/components/shared/ui/Container'; 7 | import { fetchMainCategories } from '@/lib/services/category'; 8 | 9 | type LayoutProps = { 10 | children: React.ReactNode; 11 | }; 12 | 13 | const sortOptions = [ 14 | { label: 'Newest', value: 'newest' }, 15 | { label: 'Price: Low to High', value: 'price_asc' }, 16 | { label: 'Price: High to Low', value: 'price_desc' }, 17 | { label: 'Most Reviews', value: 'review' }, 18 | ]; 19 | 20 | async function Layout({ children }: LayoutProps) { 21 | return ( 22 |
23 | {/* */} 24 |
25 | 26 | 27 | All products 28 | 29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 | {children} 40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default Layout; 47 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/products/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductList from '@/components/main/products/ProductList'; 2 | import { EmptyTableBody } from '@/components/shared/ui'; 3 | import { Suspense } from 'react'; 4 | 5 | type PageProps = { 6 | searchParams: { sort?: string; filter?: string }; 7 | }; 8 | 9 | function ProductsPage({ searchParams }: PageProps) { 10 | const filter = searchParams['filter']; 11 | let maxPrice: number | undefined; 12 | let minPrice: number | undefined; 13 | 14 | if (typeof filter === 'string') { 15 | const filterParts = filter.split(','); 16 | filterParts.forEach((part) => { 17 | const [key, value] = part.split(':'); 18 | if (key === 'MaxPrice') maxPrice = Number(value); 19 | if (key === 'MinPrice') minPrice = Number(value); 20 | }); 21 | } 22 | 23 | const sort = searchParams.sort as string; 24 | 25 | return ( 26 |
27 | }> 28 | 33 | 34 |
35 | ); 36 | } 37 | 38 | export default ProductsPage; 39 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/s/[store-slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from '@/components/shared/ui'; 2 | 3 | function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | } 10 | 11 | export default Layout; 12 | -------------------------------------------------------------------------------- /src/app/(customerFacing)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductList from '@/components/main/products/ProductList'; 2 | import SearchLayout from '@/components/shared/ui/SearchLayout'; 3 | import { fetchProductsForList } from '@/lib/services/product'; 4 | import { MdOutlineSearchOff } from 'react-icons/md'; 5 | 6 | type PageProps = { 7 | searchParams: { 8 | [key: string]: string | string[]; 9 | }; 10 | }; 11 | 12 | async function SearchPage({ searchParams }: PageProps) { 13 | const searchQuery = searchParams.q; 14 | const filter = searchParams.filter as string; 15 | 16 | const minPrice = searchParams.minPrice as string | undefined; 17 | const maxPrice = searchParams.maxPrice as string | undefined; 18 | 19 | const sort = searchParams.sort as string; 20 | 21 | const products = await fetchProductsForList({ 22 | minPrice: Number(minPrice), 23 | maxPrice: Number(maxPrice), 24 | filter, 25 | sortBy: sort, 26 | searchQuery: searchQuery as string, 27 | }); 28 | 29 | if (products.length === 0) { 30 | return ( 31 |
32 |
33 | 34 |

35 | No results found for '{searchQuery}' 36 |

37 |
38 |
39 | ); 40 | } 41 | 42 | return ( 43 | 44 |
45 | 52 |
53 |
54 | ); 55 | } 56 | 57 | export default SearchPage; 58 | -------------------------------------------------------------------------------- /src/app/account/accountsettings/page.tsx: -------------------------------------------------------------------------------- 1 | import AccountSettingsForm from '@/components/protected/account/AccountSettingsForm'; 2 | import { Heading, MiniSpinner } from '@/components/shared/ui'; 3 | import { currentUser } from '@/lib/helpers'; 4 | import { getUserById } from '@/lib/services/user'; 5 | 6 | async function AccountSettingsPage() { 7 | const user = await currentUser(); 8 | 9 | const userWithStore = await getUserById(user?.id as string); 10 | 11 | if (!user || !userWithStore) { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | 19 | return ( 20 |
21 | 22 | Account Settings 23 | 24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default AccountSettingsPage; 32 | -------------------------------------------------------------------------------- /src/app/account/help/page.tsx: -------------------------------------------------------------------------------- 1 | function HelpPage() { 2 | return
help
; 3 | } 4 | 5 | export default HelpPage; 6 | -------------------------------------------------------------------------------- /src/app/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import PrivateHeader from '@/components/protected/account/PrivateHeader'; 2 | import { SideNavigation } from '@/components/shared/ui'; 3 | 4 | import { 5 | LuBookOpen, 6 | LuHelpCircle, 7 | LuHome, 8 | LuShoppingCart, 9 | LuStar, 10 | LuWrench, 11 | } from 'react-icons/lu'; 12 | 13 | const navLinks = [ 14 | { 15 | name: 'Home', 16 | href: '/account', 17 | icon: , 18 | }, 19 | { 20 | name: 'My Orders', 21 | href: '/account/orders', 22 | icon: , 23 | }, 24 | { 25 | name: 'My Reviews', 26 | href: '/account/reviews', 27 | icon: , 28 | }, 29 | { 30 | name: 'My Addresses', 31 | href: '/account/addresses', 32 | icon: , 33 | }, 34 | { 35 | name: 'Account Settings', 36 | href: '/account/accountsettings', 37 | icon: , 38 | }, 39 | { 40 | name: 'Help', 41 | href: '/account/help', 42 | icon: , 43 | }, 44 | ]; 45 | 46 | function Layout({ children }: { children: React.ReactNode }) { 47 | return ( 48 | <> 49 | 50 |
51 | 52 |
{children}
53 |
54 | 55 | ); 56 | } 57 | 58 | export default Layout; 59 | -------------------------------------------------------------------------------- /src/app/account/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import MyOrdersList from '@/components/protected/account/MyOrdersList'; 2 | import { EmptyTableBody } from '@/components/shared/ui'; 3 | import Heading from '@/components/shared/ui/Heading'; 4 | import { Suspense } from 'react'; 5 | 6 | function OrdersPage() { 7 | return ( 8 |
9 | Orders 10 | }> 11 | 12 | 13 |
14 | ); 15 | } 16 | 17 | export default OrdersPage; 18 | -------------------------------------------------------------------------------- /src/app/account/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import Button from '@/components/shared/ui/Button'; 4 | import { currentUser } from '@/lib/helpers'; 5 | import AccountImg from '/public/account-shop.jpg'; 6 | 7 | async function AccountPage() { 8 | const user = await currentUser(); 9 | 10 | return ( 11 |
12 |
13 | {user?.store ? ( 14 |
15 |

16 | Go to your 17 | 18 | store! 19 | 20 |

21 |
22 | 25 |
26 |
27 | ) : ( 28 |
29 |

30 | Launch Your 31 | 32 | store 33 | 34 | and Reach Millions! 35 |

36 |

37 | Start your own store today and reach millions of potential customers 38 | effortlessly! 39 |

40 |
41 | 44 |
45 |
46 | )} 47 |
48 |
49 | Account Image 50 |
51 |
52 | ); 53 | } 54 | 55 | export default AccountPage; 56 | -------------------------------------------------------------------------------- /src/app/account/reviews/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import ProductsForReview from '@/components/protected/account/ProductsForReview'; 4 | import ReviewsList from '@/components/protected/account/ReviewsList'; 5 | import { Heading, MiniSpinner } from '@/components/shared/ui'; 6 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 7 | 8 | const ReviewsPage = async () => { 9 | return ( 10 |
11 | Reviews 12 | 13 | 14 | Rate 15 | My Reviews 16 | 17 | 18 | }> 19 | 20 | 21 | 22 | 23 | }> 24 | 25 | 26 | 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default ReviewsPage; 33 | -------------------------------------------------------------------------------- /src/app/account/store-application/page.tsx: -------------------------------------------------------------------------------- 1 | import ApplicationForm from '@/components/protected/account/store-application/ApplicationForm'; 2 | 3 | function StoreApplicationPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export default StoreApplicationPage; 12 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from '@/lib/auth'; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /src/app/api/categories/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import { fetchCategoryBySlug } from '@/lib/services/category'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export async function GET(request: Request, { params }: { params: { slug: string } }) { 6 | const { slug } = params; 7 | 8 | if (!slug) { 9 | return NextResponse.json({ error: 'Slug parameter is required' }, { status: 400 }); 10 | } 11 | 12 | const session = await auth(); 13 | 14 | if (!session || !session.user || !session.user.id || !session.user.store) { 15 | return Response.json( 16 | { message: 'You dont have permission to do that.' }, 17 | { status: 401 }, 18 | ); 19 | } 20 | 21 | try { 22 | const category = await fetchCategoryBySlug(slug); 23 | if (category) { 24 | return Response.json({ 25 | id: category.id, 26 | name: category.name, 27 | slug: category.slug, 28 | sub_categories: category.sub_categories.map((sub) => ({ 29 | id: sub.id, 30 | name: sub.name, 31 | slug: sub.slug, 32 | productCount: sub._count.products, 33 | })), 34 | }); 35 | } else { 36 | return Response.json({ error: 'Category not found' }, { status: 404 }); 37 | } 38 | } catch (error) { 39 | console.error('Error fetching category:', error); 40 | return Response.json({ error: 'Internal Server ErrorA' }, { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/create-payment-intent/route.ts: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@/lib/helpers'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 4 | 5 | export async function POST(request: NextRequest) { 6 | try { 7 | const session = await currentUser(); 8 | if (!session) { 9 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 10 | } 11 | 12 | const { amount } = await request.json(); 13 | 14 | if (typeof amount !== 'number' || amount <= 0) { 15 | return NextResponse.json({ error: 'Invalid amount' }, { status: 400 }); 16 | } 17 | 18 | const paymentIntent = await stripe.paymentIntents.create({ 19 | amount: amount, 20 | currency: 'usd', 21 | automatic_payment_methods: { enabled: true }, 22 | }); 23 | 24 | return NextResponse.json({ clientSecret: paymentIntent.client_secret }); 25 | } catch (error) { 26 | console.log('Internal Error:', error); 27 | 28 | return NextResponse.json( 29 | { error: `Internal Server Error: ${error}` }, 30 | { status: 500 }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/api/products/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import { fetchProductsByStore } from '@/lib/services/product'; 3 | 4 | export async function GET() { 5 | const session = await auth(); 6 | 7 | if (!session || !session.user || !session.user.id || !session.user.store) { 8 | return Response.json( 9 | { message: 'You dont have permission to do that.' }, 10 | { status: 401 }, 11 | ); 12 | } 13 | 14 | try { 15 | const products = await fetchProductsByStore({ storeId: session.user.store.id }); 16 | if (!products) { 17 | Response.json({ message: 'Products not found' }, { status: 404 }); 18 | } 19 | return Response.json({ products }); 20 | } catch (error) { 21 | console.error('Error fetching products:', error); 22 | return Response.json({ message: 'Products not found' }, { status: 404 }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/api/s/route.ts: -------------------------------------------------------------------------------- 1 | import { searchProductsCategoriesStores } from '@/lib/services/search'; 2 | 3 | export async function GET(req: Request) { 4 | const { searchParams } = new URL(req.url); 5 | const query = searchParams.get('q'); 6 | 7 | try { 8 | const results = await searchProductsCategoriesStores(query as string); 9 | return Response.json({ results }); 10 | } catch (error) { 11 | console.error('Error fetching results:', error); 12 | return Response.json({ message: 'Results not found' }, { status: 404 }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/dashboard/categories/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import CategoriesTable from '@/components/protected/dashboard/CategoriesTable'; 4 | import NewCategoryUpdateForm from '@/components/protected/dashboard/NewCategoryUpdateForm'; 5 | import { Box, EmptyTableBody } from '@/components/shared/ui'; 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from '@/components/ui/dialog'; 12 | import { fetchCategories } from '@/lib/services/category'; 13 | 14 | const categoryTableHeads = [ 15 | 'id', 16 | 'order_no', 17 | 'orderItems', 18 | 'user_id', 19 | 'delivery_status', 20 | 'order_status', 21 | 'total_price', 22 | 'created_at', 23 | 'updated_at', 24 | ]; 25 | 26 | async function CategoriesPage() { 27 | const categories = await fetchCategories(); 28 | 29 | return ( 30 | 31 |
32 | 33 | 34 | New Category 35 | 36 | 40 | Create a category 41 |
42 | 43 |
44 |
45 |
46 |
47 | 50 | } 51 | > 52 | 53 | 54 |
55 | ); 56 | } 57 | 58 | export default CategoriesPage; 59 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import PrivateHeader from '@/components/protected/account/PrivateHeader'; 2 | import { SideNavigation } from '@/components/shared/ui'; 3 | import { MdOutlineCategory } from 'react-icons/md'; 4 | 5 | const navLinks = [ 6 | { 7 | name: 'Categories', 8 | href: '/dashboard/categories', 9 | icon: ( 10 | 11 | ), 12 | }, 13 | ]; 14 | 15 | function Layout({ children }: { children: React.ReactNode }) { 16 | return ( 17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | {children} 27 |
28 |
29 |
30 | ); 31 | } 32 | 33 | export default Layout; 34 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | function DashboardPage() { 2 | return
dashboard
; 3 | } 4 | 5 | export default DashboardPage; 6 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | input[type='number']::-webkit-inner-spin-button, 7 | input[type='number']::-webkit-outer-spin-button { 8 | -webkit-appearance: none; 9 | margin: 0; 10 | } 11 | 12 | input[type='number'] { 13 | -moz-appearance: textfield; 14 | } 15 | } 16 | 17 | @layer utilities { 18 | .text-with-border { 19 | @apply flex items-center; 20 | } 21 | .text-with-border::before, 22 | .text-with-border::after { 23 | content: ''; 24 | @apply flex-1 border-b border-gray-300; 25 | } 26 | .text-with-border::before { 27 | @apply mr-2; 28 | } 29 | .text-with-border::after { 30 | @apply ml-2; 31 | } 32 | .icon-color { 33 | @apply fill-current text-gray-800; 34 | } 35 | .soft-text { 36 | @apply text-gray-custom-5; 37 | } 38 | .pause { 39 | animation-play-state: paused; 40 | } 41 | } 42 | 43 | table tbody td:first-child { 44 | border-radius: 12px 0 0 12px; 45 | } 46 | 47 | table tbody td:last-child { 48 | border-radius: 0 12px 12px 0; 49 | } 50 | 51 | table thead th:first-child { 52 | border-radius: 12px 0 0 12px; 53 | } 54 | 55 | table thead th:last-child { 56 | border-radius: 0 12px 12px 0; 57 | } 58 | 59 | .leaflet-popup .leaflet-popup-content { 60 | width: auto !important; 61 | } 62 | 63 | ::-webkit-scrollbar { 64 | width: 10px; 65 | } 66 | 67 | /* Track */ 68 | ::-webkit-scrollbar-track { 69 | background: theme('colors.gray.50'); 70 | } 71 | 72 | /* Handle */ 73 | ::-webkit-scrollbar-thumb { 74 | background: theme('colors.gray.200'); 75 | } 76 | 77 | /* Handle on hover */ 78 | ::-webkit-scrollbar-thumb:hover { 79 | background: theme('colors.gray.300'); 80 | } 81 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Providers from '@/contexts/Providers'; 2 | import { opensans } from '@/lib/fonts'; 3 | import type { Metadata } from 'next'; 4 | import { Toaster } from 'react-hot-toast'; 5 | import './globals.css'; 6 | import NextTopLoader from 'nextjs-toploader'; 7 | 8 | export const metadata: Metadata = { 9 | title: { 10 | template: '%s : Pria', 11 | default: 'Pria | Ecommerce app', 12 | }, 13 | description: 'Generated by Next.js', 14 | }; 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: Readonly<{ 19 | children: React.ReactNode; 20 | }>) { 21 | return ( 22 | 23 | 24 | 25 | 26 | 29 | 30 | {children} 31 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/shared/ui'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |
7 |

404

8 |

9 | Sorry, the page you requested was{' '} 10 | not found. 11 |

12 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/store/discounts/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import { Box, EmptyTableBody, Heading } from '@/components/shared/ui'; 4 | import DiscountTable from '@/components/store/discounts/DiscountTable'; 5 | import NewDiscountForm from '@/components/store/discounts/NewDiscountForm'; 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from '@/components/ui/dialog'; 12 | import { currentStore } from '@/lib/helpers'; 13 | import { fetchProductsByStore } from '@/lib/services/product'; 14 | 15 | const discountTableHeaders = [ 16 | 'ID', 17 | 'Name', 18 | 'Description', 19 | 'Discount', 20 | 'Active', 21 | 'Category', 22 | 'Product', 23 | 'Start date', 24 | 'End date', 25 | 'Created', 26 | 'Updated', 27 | '', 28 | ]; 29 | 30 | async function DiscountsPage() { 31 | const store = await currentStore(); 32 | 33 | const { products } = await fetchProductsByStore({ storeId: store?.id }); 34 | 35 | return ( 36 | 37 |
38 | Discounts 39 |
40 | 41 | 42 | Add discount 43 | 44 | 48 | Create a Discount 49 |
50 | 51 |
52 |
53 |
54 |
55 | 58 | } 59 | > 60 | 61 | 62 |
63 |
64 | ); 65 | } 66 | 67 | export default DiscountsPage; 68 | -------------------------------------------------------------------------------- /src/app/store/layout.tsx: -------------------------------------------------------------------------------- 1 | import PrivateHeader from '@/components/protected/account/PrivateHeader'; 2 | import { SideNavigation } from '@/components/shared/ui'; 3 | import { 4 | MdOutlinePhoneAndroid, 5 | MdOutlineStorefront, 6 | MdOutlineShoppingCart, 7 | MdOutlineDiscount, 8 | } from 'react-icons/md'; 9 | 10 | import { HiOutlineTruck } from 'react-icons/hi2'; 11 | 12 | const navLinks = [ 13 | { 14 | name: 'My Store', 15 | href: '/store/mystore', 16 | icon: , 17 | }, 18 | { 19 | name: 'Products', 20 | href: '/store/products', 21 | icon: ( 22 | 23 | ), 24 | }, 25 | { 26 | name: 'Orders', 27 | href: '/store/orders', 28 | icon: ( 29 | 30 | ), 31 | }, 32 | { 33 | name: 'Discounts', 34 | href: '/store/discounts', 35 | icon: , 36 | }, 37 | { 38 | name: 'Carriers', 39 | href: '/store/carriers', 40 | icon: , 41 | }, 42 | ]; 43 | 44 | function Layout({ children }: { children: React.ReactNode }) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 | {children} 55 |
56 |
57 | ); 58 | } 59 | 60 | export default Layout; 61 | -------------------------------------------------------------------------------- /src/app/store/mystore/page.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Heading, Map } from '@/components/shared/ui'; 2 | import LatestOrdersTable from '@/components/store/mystore/LatestOrdersTable'; 3 | import OrderLineChart from '@/components/store/mystore/OrderLineChart'; 4 | import OrderPieChart from '@/components/store/mystore/OrderPieChart'; 5 | import PerformanceStatsPanel from '@/components/store/mystore/PerformanceStatsPanel'; 6 | 7 | async function MyStorePage() { 8 | return ( 9 |
10 | 11 | 12 | 13 | Sales 14 |
15 | 16 |
17 |
18 | 19 | 20 | Top Selling Categories 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Customers 31 | 32 |
33 | 34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | export default MyStorePage; 41 | -------------------------------------------------------------------------------- /src/components/auth/FormSwitcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { usePathname } from 'next/navigation'; 5 | 6 | function AuthSwitcher() { 7 | const pathname = usePathname(); 8 | 9 | return ( 10 |
11 |
12 | 18 | Sign In 19 | 20 |
21 | 27 | Create Account 28 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export default AuthSwitcher; 35 | -------------------------------------------------------------------------------- /src/components/auth/NewVerificationForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useSearchParams } from 'next/navigation'; 5 | import { useCallback, useEffect, useState } from 'react'; 6 | 7 | import { newVerification } from '@/actions/auth'; 8 | import { FormError, FormSuccess, MiniSpinner } from '../shared/ui'; 9 | 10 | import { FcLock } from 'react-icons/fc'; 11 | 12 | function NewVerificationForm() { 13 | const [error, setError] = useState(); 14 | const [success, setSuccess] = useState(); 15 | 16 | const searchParams = useSearchParams(); 17 | 18 | const token = searchParams.get('token'); 19 | 20 | const onSubmit = useCallback(() => { 21 | if (!token) { 22 | setError('Missing token'); 23 | return; 24 | } 25 | newVerification(token) 26 | .then((data) => { 27 | setSuccess(data.success?.message); 28 | setError(data.errors?._form[0]); 29 | }) 30 | .catch(() => { 31 | setError('Something went wrong'); 32 | }); 33 | }, [token]); 34 | 35 | useEffect(() => { 36 | onSubmit(); 37 | }, [onSubmit]); 38 | 39 | return ( 40 |
41 |
42 | 43 |

Confirming your verification...

44 |
45 | {!success && !error && } 46 | {success && {success}} 47 | {error && {error}} 48 |
49 | Back to Login 50 |
51 |
52 | ); 53 | } 54 | 55 | export default NewVerificationForm; 56 | -------------------------------------------------------------------------------- /src/components/auth/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormState } from 'react-dom'; 4 | 5 | import * as actions from '@/actions/auth'; 6 | 7 | import { Form, FormError, FormSuccess, Input, SubmitButton } from '../shared/ui'; 8 | 9 | function RegisterForm() { 10 | const [formState, action] = useFormState(actions.register, { 11 | errors: {}, 12 | success: {}, 13 | }); 14 | let validationErrors: any = []; 15 | 16 | if (formState.errors && !formState.errors._form) { 17 | validationErrors = Object.values(formState.errors).map((err) => { 18 | if (err instanceof Array) { 19 | return err[0]; 20 | } 21 | return err; 22 | }); 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 39 | 40 | {validationErrors.length > 0 ? ( 41 |
42 | {validationErrors.map((error: any) => ( 43 | {error} 44 | ))} 45 |
46 | ) : null} 47 | 48 | {formState.errors?._form ? ( 49 | {formState.errors._form[0]} 50 | ) : null} 51 | 52 | {formState.success?.message ? ( 53 | {formState.success.message} 54 | ) : null} 55 | 56 | Create an account 57 |
58 | ); 59 | } 60 | 61 | export default RegisterForm; 62 | -------------------------------------------------------------------------------- /src/components/auth/ResetForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormState } from 'react-dom'; 4 | 5 | import * as actions from '@/actions/auth'; 6 | import { Form, FormError, FormSuccess, Input, SubmitButton } from '../shared/ui'; 7 | 8 | function ResetForm() { 9 | const [formState, action] = useFormState(actions.reset, { errors: {} }); 10 | let validationErrors; 11 | 12 | if ( 13 | formState?.errors && 14 | Object.values(formState?.errors).length > 0 && 15 | !formState?.errors._form 16 | ) { 17 | validationErrors = Object.values(formState?.errors).map((e) => ( 18 | {e[0]} 19 | )); 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 | {validationErrors ? ( 27 |
{validationErrors}
28 | ) : null} 29 | {formState?.errors?._form ? ( 30 | {formState.errors._form[0]} 31 | ) : null} 32 | 33 | {formState?.success?.message ? ( 34 | {formState?.success?.message} 35 | ) : null} 36 | Send reset email 37 |
38 | ); 39 | } 40 | 41 | export default ResetForm; 42 | -------------------------------------------------------------------------------- /src/components/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FormSwitcher } from './FormSwitcher'; 2 | export { default as LoginForm } from './LoginForm'; 3 | export { default as NewPasswordForm } from './NewPasswordForm'; 4 | export { default as NewVerificationForm } from './NewVerificationForm'; 5 | export { default as RegisterForm } from './RegisterForm'; 6 | export { default as ResetForm } from './ResetForm'; 7 | -------------------------------------------------------------------------------- /src/components/main/cart/CartTable.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableHead, 5 | TableHeader, 6 | TableRow, 7 | } from '@/components/ui/table'; 8 | import { currentUser } from '@/lib/helpers'; 9 | import { fetchCartByUser } from '@/lib/services/cart'; 10 | import { CartWithDetails } from '@/types'; 11 | import CartTableRow from './CartTableRow'; 12 | 13 | async function CartTable() { 14 | const user = await currentUser(); 15 | 16 | let cart: CartWithDetails | null; 17 | if (user && user.id) { 18 | cart = await fetchCartByUser(user?.id); 19 | } else { 20 | return

Your cart is empty

; 21 | } 22 | 23 | if ((cart && cart?.cart_items.length === 0) || !cart) { 24 | return

Your cart is empty

; 25 | } 26 | 27 | return ( 28 |
29 | {cart?.cart_items.length > 0 && ( 30 | 31 | 32 | 33 | Product 34 | 35 | Price 36 | Pcs 37 | Total 38 | 39 | 40 | 41 | 42 | {cart.cart_items.map((item) => ( 43 | 44 | ))} 45 | 46 |
47 | )} 48 |
49 | ); 50 | } 51 | 52 | export default CartTable; 53 | -------------------------------------------------------------------------------- /src/components/main/home/EmblaCarouselDotButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentPropsWithRef, useCallback, useEffect, useState } from 'react'; 2 | import { EmblaCarouselType } from 'embla-carousel'; 3 | 4 | type UseDotButtonType = { 5 | selectedIndex: number; 6 | scrollSnaps: number[]; 7 | onDotButtonClick: (index: number) => void; 8 | }; 9 | 10 | export const useDotButton = ( 11 | emblaApi: EmblaCarouselType | undefined, 12 | ): UseDotButtonType => { 13 | const [selectedIndex, setSelectedIndex] = useState(0); 14 | const [scrollSnaps, setScrollSnaps] = useState([]); 15 | 16 | const onDotButtonClick = useCallback( 17 | (index: number) => { 18 | if (!emblaApi) return; 19 | emblaApi.scrollTo(index); 20 | }, 21 | [emblaApi], 22 | ); 23 | 24 | const onInit = useCallback((emblaApi: EmblaCarouselType) => { 25 | setScrollSnaps(emblaApi.scrollSnapList()); 26 | }, []); 27 | 28 | const onSelect = useCallback((emblaApi: EmblaCarouselType) => { 29 | setSelectedIndex(emblaApi.selectedScrollSnap()); 30 | }, []); 31 | 32 | useEffect(() => { 33 | if (!emblaApi) return; 34 | 35 | onInit(emblaApi); 36 | onSelect(emblaApi); 37 | emblaApi.on('reInit', onInit).on('reInit', onSelect).on('select', onSelect); 38 | }, [emblaApi, onInit, onSelect]); 39 | 40 | return { 41 | selectedIndex, 42 | scrollSnaps, 43 | onDotButtonClick, 44 | }; 45 | }; 46 | 47 | type PropType = ComponentPropsWithRef<'button'>; 48 | 49 | export const DotButton: React.FC = (props) => { 50 | const { children, ...restProps } = props; 51 | 52 | return ( 53 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/main/home/EmptyProductCarousel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Carousel, 3 | CarouselContent, 4 | CarouselItem, 5 | CarouselNext, 6 | CarouselPrevious, 7 | } from '@/components/ui/carousel'; 8 | import { Skeleton } from '@/components/ui/skeleton'; 9 | 10 | function EmptyProductCarousel({ length }: { length: number }) { 11 | return ( 12 | 13 | 14 | {Array.from({ length }).map((_, index) => ( 15 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | ))} 35 |
36 | 37 | 38 |
39 | ); 40 | } 41 | 42 | export default EmptyProductCarousel; 43 | -------------------------------------------------------------------------------- /src/components/main/home/ProductCarousel.tsx: -------------------------------------------------------------------------------- 1 | import ProductCard from '@/components/shared/ui/ProductCard'; 2 | import { 3 | Carousel, 4 | CarouselContent, 5 | CarouselItem, 6 | CarouselNext, 7 | CarouselPrevious, 8 | } from '@/components/ui/carousel'; 9 | import { ProductForList } from '@/types'; 10 | 11 | export type ProductCarouselProps = { 12 | fetchData: (term: string, store: string | undefined) => Promise; 13 | term: string; 14 | store?: string; 15 | }; 16 | 17 | async function ProductCarousel({ fetchData, term, store }: ProductCarouselProps) { 18 | const products = await fetchData(term, store || undefined); 19 | 20 | return ( 21 | 22 | 23 | {products?.map((product) => ( 24 | 25 | 26 | 27 | ))} 28 | 29 |
30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | export default ProductCarousel; 37 | -------------------------------------------------------------------------------- /src/components/main/home/StyledProductCarousel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ProductCarousel, { ProductCarouselProps } from './ProductCarousel'; 4 | 5 | function StyledProductCarousel(props: ProductCarouselProps) { 6 | return ( 7 |
8 |

Don't miss!

9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default StyledProductCarousel; 17 | -------------------------------------------------------------------------------- /src/components/main/p/ProductDetailsTab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; 3 | import { ProductForShowPage } from '@/types'; 4 | import ProductReviews from './ProductReviews'; 5 | 6 | interface ProductDetailsTabProps { 7 | product: ProductForShowPage | null; 8 | } 9 | 10 | function ProductDetailsTab({ product }: ProductDetailsTabProps) { 11 | const productAttWithout = product?.attributes.filter( 12 | (att) => att.category_attribute.type !== 'multi-select', 13 | ); 14 | 15 | return ( 16 |
17 | 18 | 19 | Description 20 | Technical Specs 21 | Reviews 22 | 23 | 24 | {product?.description} 25 | 26 | 27 |
28 | {productAttWithout?.map((att, index) => ( 29 |
35 |

{att.category_attribute.name}

36 |

37 | {att.value} 38 | 39 | {att.category_attribute.name === 'Weight' && 'kg'} 40 | 41 |

42 |
43 | ))} 44 |
45 |
46 | 47 | 48 | 49 |
50 |
51 | ); 52 | } 53 | 54 | export default ProductDetailsTab; 55 | -------------------------------------------------------------------------------- /src/components/main/payment-success/OrderDetailsShow.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Order } from '@prisma/client'; 4 | 5 | import { formatDateTertiary, roundToTwoDecimals } from '@/lib/utils'; 6 | import { CheckCircle } from 'lucide-react'; 7 | import { useEffect } from 'react'; 8 | import { useRouter } from 'next/navigation'; 9 | 10 | function OrderDetailsShow({ order }: { order: Order }) { 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | router.refresh(); 15 | }, [router]); 16 | 17 | return ( 18 |
19 |
20 | 21 |

Payment Successful

22 |
23 | 24 |
25 |

Order Summary

26 | 27 |
28 | Order NO: 29 | {order?.order_no} 30 |
31 |
32 | Order Date: 33 | 34 | {formatDateTertiary(order?.created_at as Date)} 35 | 36 |
37 |
38 | Amount Paid: 39 | 40 | $ {roundToTwoDecimals(Number(order?.total_price))} 41 | 42 |
43 |
44 | Estimated Delivery: 45 | 2-7 Days 46 |
47 |
48 | Status: 49 | {order?.order_status} 50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | export default OrderDetailsShow; 57 | -------------------------------------------------------------------------------- /src/components/main/products/ProductList.tsx: -------------------------------------------------------------------------------- 1 | import ProductCard from '@/components/shared/ui/ProductCard'; 2 | import { fetchProductsForList } from '@/lib/services/product'; 3 | 4 | type ProductListProps = { 5 | slug?: string; 6 | minPrice?: number; 7 | maxPrice?: number; 8 | filter?: string; 9 | sortBy?: string; 10 | searchQuery?: string; 11 | }; 12 | 13 | async function ProductList({ 14 | slug, 15 | minPrice, 16 | maxPrice, 17 | filter, 18 | sortBy, 19 | searchQuery, 20 | }: ProductListProps) { 21 | const products = await fetchProductsForList({ 22 | categorySlug: slug, 23 | minPrice, 24 | maxPrice, 25 | filter, 26 | sortBy, 27 | searchQuery, 28 | }); 29 | 30 | return ( 31 |
32 | {products?.map((product) => )} 33 |
34 | ); 35 | } 36 | 37 | export default ProductList; 38 | -------------------------------------------------------------------------------- /src/components/main/products/SidebarCategoryItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { 4 | Accordion, 5 | AccordionContent, 6 | AccordionItem, 7 | AccordionTrigger, 8 | } from '@/components/ui/accordion'; 9 | import { CategoryForSidebar } from '@/types'; 10 | 11 | function SidebarCategoryItem({ category }: { category: CategoryForSidebar }) { 12 | let currentCatProductCount; 13 | 14 | if (category.sub_categories.length === 0 && category.products) { 15 | currentCatProductCount = category.products.length; 16 | } else { 17 | currentCatProductCount = category.sub_categories.reduce( 18 | (total, sub) => (total += sub._count.products), 19 | 0, 20 | ); 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | {category.name} 28 | 29 | 30 |
    31 | 35 |

    All

    36 |

    {currentCatProductCount}

    37 | 38 | {category.sub_categories?.map((subCategory) => ( 39 | 44 |

    {subCategory.name}

    45 |

    {subCategory._count.products}

    46 | 47 | ))} 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default SidebarCategoryItem; 56 | -------------------------------------------------------------------------------- /src/components/protected/account/AddNewAddressForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormState } from 'react-dom'; 4 | 5 | import { createNewAddress } from '@/actions/account'; 6 | import { Form, FormSuccess, Input, SubmitButton } from '@/components/shared/ui'; 7 | 8 | function AddNewAddressForm() { 9 | const [formState, action] = useFormState(createNewAddress, { errors: {} }); 10 | 11 | return ( 12 |
13 | 14 | 20 | 26 | 32 | 33 | 39 | 45 | 51 | {formState.success && {formState.success?.message}} 52 | 53 | Add 54 |
55 | ); 56 | } 57 | 58 | export default AddNewAddressForm; 59 | -------------------------------------------------------------------------------- /src/components/protected/account/MyOrdersList.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@/lib/helpers'; 2 | import { fetchOrdersByUser } from '@/lib/services/order'; 3 | import OrderItem from './OrderItem'; 4 | 5 | async function MyOrdersList() { 6 | const user = await currentUser(); 7 | 8 | const orders = await fetchOrdersByUser(user?.id as string); 9 | 10 | return ( 11 |
12 | {orders.map((order, index) => ( 13 | <> 14 | 15 | 16 | ))} 17 |
18 | ); 19 | } 20 | 21 | export default MyOrdersList; 22 | -------------------------------------------------------------------------------- /src/components/protected/account/OrderProgress.tsx: -------------------------------------------------------------------------------- 1 | import { Heading } from '@/components/shared/ui'; 2 | import { capitalizeOnlyFirstLetter } from '@/lib/utils'; 3 | import { MdOutlineCheck } from 'react-icons/md'; 4 | 5 | const statuses = ['PENDING', 'INPROGRESS', 'SHIPPED', 'COMPLETED']; 6 | 7 | const status: { [key: string]: number } = { 8 | PENDING: 0, 9 | INPROGRESS: 1, 10 | SHIPPED: 2, 11 | COMPLETED: 3, 12 | }; 13 | 14 | function OrderProgress({ orderStatus }: { orderStatus: string }) { 15 | const currentStatusIndex = status[orderStatus]; 16 | 17 | return ( 18 |
19 | Order Status 20 |
21 |
22 | {statuses.map((status, index) => ( 23 | <> 24 |
25 |
28 | 29 |
30 |
31 | {capitalizeOnlyFirstLetter(status)} 32 |
33 |
34 |
37 | 38 | ))} 39 |
40 |
41 |
42 | ); 43 | } 44 | 45 | export default OrderProgress; 46 | -------------------------------------------------------------------------------- /src/components/protected/account/PrivateHeader.tsx: -------------------------------------------------------------------------------- 1 | import HeaderDropdown from '../../shared/ui/HeaderDropdown'; 2 | 3 | function PrivateHeader() { 4 | return ( 5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 | ); 13 | } 14 | 15 | export default PrivateHeader; 16 | -------------------------------------------------------------------------------- /src/components/protected/account/ReviewsList.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from '@/lib/helpers'; 2 | import { fetchReviewsByUser } from '@/lib/services/review'; 3 | import { ShoppingBag } from 'lucide-react'; 4 | import ReviewShowCard from './ReviewShowCard'; 5 | 6 | async function ReviewsList() { 7 | const user = await currentUser(); 8 | const reviews = await fetchReviewsByUser(user?.id as string); 9 | 10 | return ( 11 |
12 | {reviews && reviews.length > 0 ? ( 13 |
14 | {reviews.map((review) => ( 15 | 16 | ))} 17 |
18 | ) : ( 19 |
20 | 21 |

22 | You haven't written any reviews yet. 23 |

24 |

25 | Once you make a purchase, you can share your thoughts here. 26 |

27 |
28 | )} 29 |
30 | ); 31 | } 32 | 33 | export default ReviewsList; 34 | -------------------------------------------------------------------------------- /src/components/protected/dashboard/CategoriesTable.tsx: -------------------------------------------------------------------------------- 1 | import { CustomTableHead } from '@/components/shared/ui'; 2 | import { Table, TableBody, TableHeader, TableRow } from '@/components/ui/table'; 3 | import { fetchCategories } from '@/lib/services/category'; 4 | import CategoryTableRow from './CategoryTableRow'; 5 | import { Suspense } from 'react'; 6 | 7 | const categoryTableHeads = [ 8 | { value: 'id', label: 'ID' }, 9 | { value: 'name', label: 'Order No' }, 10 | { value: 'isActive', label: 'Is Active' }, 11 | { value: 'created_at', label: 'Created', sortable: true }, 12 | { value: 'updated_at', label: 'Updated', sortable: true }, 13 | ]; 14 | 15 | async function CategoriesTable() { 16 | const categories = await fetchCategories(); 17 | 18 | return ( 19 |
20 | 21 | 22 | 23 | {categoryTableHeads.map((head, index) => ( 24 | 25 | 26 | 27 | ))} 28 | 29 | 30 | 31 | {categories?.map((order, index) => ( 32 | 33 | ))} 34 | 35 |
36 |
37 | ); 38 | } 39 | 40 | export default CategoriesTable; 41 | -------------------------------------------------------------------------------- /src/components/protected/dashboard/CategoryTableRow.tsx: -------------------------------------------------------------------------------- 1 | import { TableCell, TableRow } from '@/components/ui/table'; 2 | import { formatDateSecondary } from '@/lib/utils'; 3 | import { CategoryForSidebar } from '@/types'; 4 | 5 | type CategoryTableRowProps = { 6 | category: CategoryForSidebar; 7 | index: number; 8 | }; 9 | 10 | function CategoryTableRow({ category, index }: CategoryTableRowProps) { 11 | return ( 12 | 13 | {index + 1} 14 | {category.name} 15 | {/* TODO: Active column. */} 16 | {category.isActive ? 'yes' : 'no'} 17 | {formatDateSecondary(category.created_at)} 18 | {formatDateSecondary(category.updated_at)} 19 | 20 | ); 21 | } 22 | 23 | export default CategoryTableRow; 24 | -------------------------------------------------------------------------------- /src/components/shared/ui/Box.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Box({ children, className }: { children: React.ReactNode; className?: string }) { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | } 10 | 11 | export default Box; 12 | -------------------------------------------------------------------------------- /src/components/shared/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import Link from 'next/link'; 3 | import { ComponentPropsWithoutRef } from 'react'; 4 | 5 | type AnchorProps = { 6 | el: 'anchor'; 7 | children: React.ReactNode | string; 8 | className?: string; 9 | color?: string; 10 | } & ComponentPropsWithoutRef; 11 | 12 | type ButtonProps = { 13 | el: 'button'; 14 | children: React.ReactNode | string; 15 | className?: string; 16 | color?: string; 17 | } & ComponentPropsWithoutRef<'button'>; 18 | 19 | const colors: { [index: string]: string } = { 20 | blue: 'bg-blue-custom-1 active:bg-blue-custom-2', 21 | transparent: 'bg-transparent active:bg-gray-50 border border-orange-1 text-orange-1', 22 | orange: 'bg-orange-1 text-white active:bg-orange-400', 23 | white: 'bg-white active:bg-gray-100 text-gray-custom-1', 24 | }; 25 | 26 | function Button({ el, children, className, color, ...props }: ButtonProps | AnchorProps) { 27 | if (el === 'anchor') { 28 | return ( 29 | 36 | {children} 37 | 38 | ); 39 | } 40 | 41 | return ( 42 | 51 | ); 52 | } 53 | 54 | export default Button; 55 | -------------------------------------------------------------------------------- /src/components/shared/ui/Container.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Container({ 4 | children, 5 | className, 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }) { 10 | return
{children}
; 11 | } 12 | 13 | export default Container; 14 | -------------------------------------------------------------------------------- /src/components/shared/ui/CustomTableHead.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useSearchParams, useRouter } from 'next/navigation'; 4 | 5 | import { TableHead } from '../../ui/table'; 6 | 7 | import { RxCaretSort, RxCaretUp, RxCaretDown } from 'react-icons/rx'; 8 | 9 | type CustomTableHead = { 10 | tableHead: { 11 | value: string; 12 | label: string; 13 | sortable?: boolean; 14 | }; 15 | }; 16 | 17 | function CustomTableHead({ tableHead }: CustomTableHead) { 18 | const searchParams = useSearchParams(); 19 | const router = useRouter(); 20 | const path = usePathname(); 21 | 22 | const handleClick = (head: string) => { 23 | if (['image', 'id', ' '].includes(head)) return; 24 | const params = new URLSearchParams(searchParams); 25 | const currentSort = params.get('sort'); 26 | const currentOrder = params.get('order'); 27 | 28 | const column = head.toLowerCase(); 29 | 30 | if (currentSort === column) { 31 | params.set('order', currentOrder === 'asc' ? 'desc' : 'asc'); 32 | } else { 33 | params.set('sort', column); 34 | params.set('order', 'asc'); 35 | } 36 | 37 | router.replace(`${path}?${params.toString()}`); 38 | }; 39 | 40 | const isAsc = 41 | searchParams.get('order') === 'asc' && 42 | searchParams.get('sort') === tableHead.value.toLowerCase(); 43 | 44 | const isDesc = 45 | searchParams.get('order') === 'desc' && 46 | searchParams.get('sort') === tableHead.value.toLowerCase(); 47 | 48 | return ( 49 | 50 | 62 | 63 | ); 64 | } 65 | 66 | export default CustomTableHead; 67 | -------------------------------------------------------------------------------- /src/components/shared/ui/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { format } from 'date-fns'; 4 | import { CalendarIcon } from 'lucide-react'; 5 | import { DateRange } from 'react-day-picker'; 6 | 7 | import { Calendar } from '@/components/ui/calendar'; 8 | import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; 9 | 10 | type DateRangePickerProps = { 11 | dateRange: DateRange | undefined; 12 | setDateRange: (range: DateRange | undefined) => void; 13 | }; 14 | 15 | function DatePicker({ dateRange, setDateRange }: DateRangePickerProps) { 16 | return ( 17 | 18 | 19 | 36 | 37 | 38 | 46 | 47 | 48 | ); 49 | } 50 | export default DatePicker; 51 | -------------------------------------------------------------------------------- /src/components/shared/ui/EducationalSiteDisclaimerBanner .tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AlertTriangle, X } from 'lucide-react'; 4 | import { useState, useEffect } from 'react'; 5 | import { createPortal } from 'react-dom'; 6 | 7 | function EducationalSiteDisclaimerBanner() { 8 | const [open, setOpen] = useState(true); 9 | const [portalContainer, setPortalContainer] = useState(null); 10 | 11 | useEffect(() => { 12 | setPortalContainer(document.body); 13 | }, []); 14 | 15 | if (!open || !portalContainer) return null; 16 | 17 | return createPortal( 18 |
22 | 29 |
30 | 31 |

Warning

32 |
33 |

34 | This website is not a real e-commerce platform. Any information or features 35 | presented here are not representative of actual transactions or services. 36 |

37 |
, 38 | portalContainer, 39 | ); 40 | } 41 | 42 | export default EducationalSiteDisclaimerBanner; 43 | -------------------------------------------------------------------------------- /src/components/shared/ui/EmptyTableBody.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCaption, 5 | TableCell, 6 | TableHeader, 7 | TableRow, 8 | } from '@/components/ui/table'; 9 | import { Skeleton } from '../../ui/skeleton'; 10 | 11 | export default function EmptyTableBody({ 12 | rows, 13 | cols, 14 | tableHeaders, 15 | }: { 16 | rows: number; 17 | cols: number; 18 | tableHeaders?: string[]; 19 | }) { 20 | return ( 21 | 22 | A list of your recent products. 23 | 24 | 25 | {tableHeaders?.map((col, index) => {col})} 26 | 27 | 28 | 29 | {Array.from({ length: rows }).map((_, index) => ( 30 | 31 | {Array.from({ length: cols }).map((_, index) => ( 32 | 33 | 34 | 35 | ))} 36 | 37 | ))} 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/shared/ui/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | function ErrorCard() { 2 | return ( 3 |
4 | Something went wrong!! Go back.. 5 |
6 | ); 7 | } 8 | 9 | export default ErrorCard; 10 | -------------------------------------------------------------------------------- /src/components/shared/ui/Filter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 4 | 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from '@/components/ui/dropdown-menu'; 11 | 12 | type Filter = { 13 | label: string; 14 | value: string; 15 | }; 16 | 17 | type FilterByProps = { 18 | filterName: string; 19 | filters: Filter[]; 20 | label: React.ReactNode; 21 | }; 22 | 23 | function Filter({ filterName, filters, label }: FilterByProps) { 24 | const searchParams = useSearchParams(); 25 | const currentFilters = searchParams.getAll(filterName); 26 | const pathname = usePathname(); 27 | const { replace } = useRouter(); 28 | 29 | const handleFilter = (query: string) => { 30 | const params = new URLSearchParams(searchParams); 31 | if (currentFilters.includes(query)) { 32 | params.delete(filterName); 33 | currentFilters 34 | .filter((v) => v !== query) 35 | .forEach((v) => params.append(filterName, v)); 36 | } else { 37 | params.append(filterName, query); 38 | } 39 | 40 | replace(`${pathname}?${params.toString()}`); 41 | }; 42 | 43 | return ( 44 | 45 | 46 | {label} 47 | 48 | 49 | {filters.map((filter) => ( 50 | handleFilter(filter.value)} 53 | className="cursor-pointer" 54 | > 55 | {filter.label} 56 |

{currentFilters.includes(filter.value) && '✓'}

57 |
58 | ))} 59 |
60 |
61 | ); 62 | } 63 | 64 | export default Filter; 65 | -------------------------------------------------------------------------------- /src/components/shared/ui/Form.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import React, { ComponentPropsWithRef, forwardRef } from 'react'; 3 | 4 | type FormProps = { 5 | children: React.ReactNode; 6 | className?: string; 7 | } & ComponentPropsWithRef<'form'>; 8 | 9 | const Form = forwardRef( 10 | ({ children, className, ...props }, ref) => { 11 | return ( 12 |
23 | {children} 24 |
25 | ); 26 | }, 27 | ); 28 | 29 | Form.displayName = 'Form'; 30 | 31 | export default Form; 32 | -------------------------------------------------------------------------------- /src/components/shared/ui/FormError.tsx: -------------------------------------------------------------------------------- 1 | import { MdGppBad } from 'react-icons/md'; 2 | 3 | function FormError({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | 7 | {children} 8 |
9 | ); 10 | } 11 | 12 | export default FormError; 13 | -------------------------------------------------------------------------------- /src/components/shared/ui/FormSelectInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | SelectContent, 4 | SelectGroup, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from '@/components/ui/select'; 9 | import { capitalizeOnlyFirstLetter } from '@/lib/utils'; 10 | 11 | export type SelectItem = { 12 | id: string; 13 | name: string; 14 | }; 15 | 16 | type FormSelectInputProps = { 17 | selectItems: SelectItem[]; 18 | label: string; 19 | onSelectChange: (value: string) => void; 20 | defaultValue?: string; 21 | value?: string; 22 | disabled?: boolean; 23 | }; 24 | 25 | function FormSelectInput({ 26 | selectItems, 27 | label, 28 | onSelectChange, 29 | defaultValue, 30 | value, 31 | disabled, 32 | }: FormSelectInputProps) { 33 | const handleValueChange = (newValue: string) => { 34 | if (newValue === value) { 35 | onSelectChange(''); 36 | } else { 37 | onSelectChange(newValue); 38 | } 39 | }; 40 | 41 | return ( 42 |
43 | 44 | 62 |
63 | ); 64 | } 65 | 66 | export default FormSelectInput; 67 | -------------------------------------------------------------------------------- /src/components/shared/ui/FormSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { MdGppGood } from 'react-icons/md'; 2 | 3 | function FormSuccess({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 | 7 | {children} 8 |
9 | ); 10 | } 11 | 12 | export default FormSuccess; 13 | -------------------------------------------------------------------------------- /src/components/shared/ui/Heading.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Heading({ 4 | children, 5 | type, 6 | className, 7 | }: { 8 | children: string | React.ReactNode; 9 | type: string; 10 | className?: string; 11 | }) { 12 | if (type === 'heading-1') 13 | return

{children}

; 14 | 15 | if (type === 'heading-2') 16 | return

{children}

; 17 | 18 | if (type === 'heading-3') 19 | return

{children}

; 20 | 21 | if (type === 'heading-4') 22 | return

{children}

; 23 | 24 | if (type === 'heading-5') 25 | return ( 26 |

27 | {children} 28 |

29 | ); 30 | 31 | if (type === 'heading-6') 32 | return

{children}

; 33 | } 34 | 35 | export default Heading; 36 | -------------------------------------------------------------------------------- /src/components/shared/ui/Input.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormStatus } from 'react-dom'; 4 | import { ComponentPropsWithoutRef } from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | import FormError from './FormError'; 8 | 9 | type InputProps = { 10 | label?: string; 11 | id?: string; 12 | view?: string; 13 | className?: string; 14 | error?: string[]; 15 | } & ComponentPropsWithoutRef<'input'>; 16 | 17 | function Input({ label, id, view, className, error, ...props }: InputProps) { 18 | const { pending } = useFormStatus(); 19 | const isRadio = props.type === 'radio'; 20 | 21 | return ( 22 | 56 | ); 57 | } 58 | 59 | export default Input; 60 | -------------------------------------------------------------------------------- /src/components/shared/ui/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | import logoImg from '/public/logo.png'; 5 | 6 | function Logo() { 7 | return ( 8 |
9 | 10 | Logo 11 | 12 |
13 | ); 14 | } 15 | 16 | export default Logo; 17 | -------------------------------------------------------------------------------- /src/components/shared/ui/Map.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import dynamic from 'next/dynamic'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | import MiniSpinner from './MiniSpinner'; 7 | 8 | const ClientSideMap = dynamic(() => import('./ClientSideMap'), { 9 | ssr: false, 10 | loading: () => , 11 | }); 12 | 13 | function Map() { 14 | const [isMounted, setIsMounted] = useState(false); 15 | 16 | useEffect(() => { 17 | setIsMounted(true); 18 | }, []); 19 | 20 | if (!isMounted) { 21 | return null; 22 | } 23 | 24 | return ; 25 | } 26 | 27 | export default Map; 28 | -------------------------------------------------------------------------------- /src/components/shared/ui/MiniSpinner.tsx: -------------------------------------------------------------------------------- 1 | function MiniSpinner() { 2 | return ( 3 |
4 | 20 |
21 | ); 22 | } 23 | 24 | export default MiniSpinner; 25 | -------------------------------------------------------------------------------- /src/components/shared/ui/MultiSelect.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | interface MultiSelectProps { 6 | attrId: string; 7 | options: string[]; 8 | value: string; 9 | onChange: (newValue: string[]) => void; 10 | } 11 | 12 | const MultiSelect = ({ attrId, options, value, onChange }: MultiSelectProps) => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | const selectedValues = value.split(',').filter(Boolean); 15 | 16 | const toggleOption = (option: string) => { 17 | const newSelectedValues = selectedValues.includes(option) 18 | ? selectedValues.filter((v) => v !== option) 19 | : [...selectedValues, option]; 20 | onChange(newSelectedValues); 21 | }; 22 | 23 | return ( 24 |
25 |
setIsOpen(!isOpen)} 28 | > 29 | {selectedValues.length > 0 ? selectedValues.join(', ') : 'Select options'} 30 |
31 | {isOpen && ( 32 |
33 | {options.map((option) => ( 34 |
toggleOption(option)} 40 | > 41 | {option} 42 |
43 | ))} 44 |
45 | )} 46 |
47 | ); 48 | }; 49 | 50 | export default MultiSelect; 51 | -------------------------------------------------------------------------------- /src/components/shared/ui/PieChart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ResponsiveContainer, PieChart as PieeChart, Pie, Cell } from 'recharts'; 4 | 5 | type PieChartPropsType = { 6 | data: { 7 | name: string; 8 | value: number; 9 | color: string; 10 | }[]; 11 | }; 12 | 13 | export default function PieChart({ data }: PieChartPropsType) { 14 | return ( 15 |
16 | 17 | 18 | 19 | {data?.map((entry, index) => ( 20 | 21 | ))} 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/shared/ui/ProductPriceDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { calculatePriceWithDiscounts, roundToTwoDecimals } from '@/lib/utils'; 2 | 3 | type ProductPriceDisplayProps = { 4 | product: any; 5 | quantity?: number; 6 | showTotalSavings?: boolean; 7 | showOldPrice?: boolean; 8 | className?: string; 9 | }; 10 | 11 | function ProductPriceDisplay({ 12 | product, 13 | quantity = 1, 14 | showOldPrice = true, 15 | showTotalSavings = true, 16 | className, 17 | }: ProductPriceDisplayProps) { 18 | const { finalPrice, oldPrice, discountPercentage } = calculatePriceWithDiscounts( 19 | product, 20 | quantity, 21 | ); 22 | 23 | const hasDiscount = discountPercentage > 0; 24 | 25 | return ( 26 |
27 | {hasDiscount && showOldPrice && oldPrice !== finalPrice && ( 28 |
29 | 30 | ${roundToTwoDecimals(oldPrice)} 31 | 32 | {discountPercentage > 0 && ( 33 | %{discountPercentage} off 34 | )} 35 |
36 | )} 37 |

38 | ${roundToTwoDecimals(finalPrice)} 39 |

40 | 41 | {discountPercentage > 0 && showTotalSavings && ( 42 |

43 | You save: ${roundToTwoDecimals(product.price - finalPrice)} 44 |

45 | )} 46 |
47 | ); 48 | } 49 | 50 | export default ProductPriceDisplay; 51 | -------------------------------------------------------------------------------- /src/components/shared/ui/Rating.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FaStar } from 'react-icons/fa'; 4 | 5 | type RatingProps = { 6 | el?: string; 7 | value: number; 8 | editable: boolean; 9 | size: number; 10 | onValueChange?: (value: number) => void; 11 | starHw?: { h: string; w: string }; 12 | }; 13 | 14 | function Rating({ 15 | el = 'Star', 16 | value, 17 | editable, 18 | size, 19 | onValueChange, 20 | starHw, 21 | }: RatingProps) { 22 | const handleClick = (index: number) => { 23 | if (editable && onValueChange) { 24 | onValueChange(index); 25 | } 26 | }; 27 | 28 | const getTooltipText = () => { 29 | // if (value === -1) return 'No rating'; 30 | return `${value.toFixed(1)}/${size} ${el}${value !== 1 ? 's' : ''}`; 31 | }; 32 | 33 | 34 | return ( 35 |
36 | {value !== -1 && ( 37 |
38 | {Array.from({ length: size }, (_, index) => ( 39 | handleClick(index)}> 40 | 46 | 47 | ))} 48 |
49 | )} 50 |
51 | ); 52 | } 53 | 54 | export default Rating; 55 | -------------------------------------------------------------------------------- /src/components/shared/ui/Search.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useSearchParams, useRouter } from 'next/navigation'; 4 | import { KeyboardEvent, useState } from 'react'; 5 | 6 | import Input from './Input'; 7 | import { RxCross1 } from 'react-icons/rx'; 8 | 9 | function Search({ placeholder }: { placeholder: string }) { 10 | const router = useRouter(); 11 | const pathname = usePathname(); 12 | const searchParams = useSearchParams(); 13 | const [searchTerm, setSearchTerm] = useState(''); 14 | const [displayTerm, setDisplayTerm] = useState(''); 15 | 16 | const handleKeyDown = (e: KeyboardEvent) => { 17 | if (e.key === 'Enter') { 18 | if (searchTerm) { 19 | const params = new URLSearchParams(searchParams); 20 | setDisplayTerm(searchTerm); 21 | 22 | params.set('search', searchTerm); 23 | 24 | router.push(`${pathname}?${params.toString()}`); 25 | } 26 | } 27 | }; 28 | 29 | const handleClick = () => { 30 | setSearchTerm(''); 31 | setDisplayTerm(''); 32 | 33 | const params = new URLSearchParams(searchParams); 34 | params.delete('search'); 35 | 36 | router.push(`${pathname}?${params.toString()}`); 37 | }; 38 | 39 | return ( 40 |
41 | setSearchTerm(e.target.value)} 45 | placeholder={placeholder} 46 | /> 47 | {displayTerm && ( 48 | 55 | )} 56 |
57 | ); 58 | } 59 | 60 | export default Search; 61 | -------------------------------------------------------------------------------- /src/components/shared/ui/SearchLayout.tsx: -------------------------------------------------------------------------------- 1 | import ProductSidebar from '@/components/main/products/ProductSidebar'; 2 | import { SortBy } from '@/components/shared/ui'; 3 | import Container from '@/components/shared/ui/Container'; 4 | import { fetchCategorybyProduct } from '@/lib/services/category'; 5 | import { Suspense } from 'react'; 6 | 7 | type LayoutProps = { 8 | children: React.ReactNode; 9 | params?: { 10 | [key: string]: string | string[]; 11 | }; 12 | searchParams: { 13 | [key: string]: string | string[]; 14 | }; 15 | }; 16 | 17 | const sortOptions = [ 18 | { label: 'Newest', value: 'newest' }, 19 | { label: 'Price: Low to High', value: 'price_asc' }, 20 | { label: 'Price: High to Low', value: 'price_desc' }, 21 | { label: 'Most Reviews', value: 'review' }, 22 | ]; 23 | 24 | function SearchLayout({ children, searchParams }: LayoutProps) { 25 | const searchQuery = searchParams.q; 26 | 27 | return ( 28 |
29 | {/* TODO: SLIDER */} 30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 | 45 | 46 | {children} 47 | 48 |
49 |
50 | ); 51 | } 52 | 53 | export default SearchLayout; 54 | -------------------------------------------------------------------------------- /src/components/shared/ui/Seperator.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Seperator({ className }: { className?: string }) { 4 | return
; 5 | } 6 | 7 | export default Seperator; 8 | -------------------------------------------------------------------------------- /src/components/shared/ui/SideNavigation.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { usePathname } from 'next/navigation'; 5 | 6 | import Logo from './Logo'; 7 | 8 | type NavLink = { 9 | name: string; 10 | href: string; 11 | icon: React.ReactNode; 12 | }; 13 | 14 | type SideNavigationProps = { 15 | navItems: NavLink[]; 16 | theme?: 'light' | 'dark'; 17 | }; 18 | 19 | const textColors = { 20 | light: 'text-gray-900', 21 | dark: 'text-white', 22 | }; 23 | 24 | const bgColors = { 25 | light: 'bg-white', 26 | dark: 'bg-dark-blue-1', 27 | }; 28 | 29 | const hoverColors = { 30 | light: 'hover:text-orange-1', 31 | dark: 'hover:text-blue-custom-1', 32 | }; 33 | 34 | const activeTextColors = { 35 | light: 'text-orange-1', 36 | dark: 'text-blue-custom-1', 37 | }; 38 | 39 | const activeBgColors = { 40 | light: 'bg-gray-50', 41 | dark: 'bg-dark-blue-2', 42 | }; 43 | 44 | function SideNavigation({ navItems, theme = 'light' }: SideNavigationProps) { 45 | const pathname = usePathname(); 46 | 47 | let textClass = textColors[theme]; 48 | let bgClass = bgColors[theme]; 49 | let hoverClass = hoverColors[theme]; 50 | let activeTextClass = activeTextColors[theme]; 51 | let activeBgClass = activeBgColors[theme]; 52 | 53 | return ( 54 | 74 | ); 75 | } 76 | 77 | export default SideNavigation; 78 | -------------------------------------------------------------------------------- /src/components/shared/ui/SimpleLineChart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | CartesianGrid, 5 | Legend, 6 | Line, 7 | LineChart, 8 | ResponsiveContainer, 9 | Tooltip, 10 | XAxis, 11 | YAxis, 12 | } from 'recharts'; 13 | 14 | type DataPoint = { 15 | [key: string]: string | number; 16 | }; 17 | 18 | type SimpleLineChartProps = { 19 | data: DataPoint[]; 20 | xAxisDataKey: string; 21 | lines: { 22 | dataKey: string; 23 | stroke: string; 24 | }[]; 25 | }; 26 | 27 | function SimpleLineChart({ data, xAxisDataKey, lines }: SimpleLineChartProps) { 28 | return ( 29 | 30 | 39 | 40 | 41 | 42 | 43 | 44 | {lines.map((line, index) => ( 45 | 52 | ))} 53 | 54 | 55 | ); 56 | } 57 | 58 | export default SimpleLineChart; 59 | -------------------------------------------------------------------------------- /src/components/shared/ui/Socials.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FcGoogle } from 'react-icons/fc'; 4 | import { FaGithub } from 'react-icons/fa'; 5 | import { signIn } from 'next-auth/react'; 6 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; 7 | 8 | function Socials() { 9 | const onClick = (provider: 'google' | 'github') => { 10 | signIn(provider, { 11 | callbackUrl: DEFAULT_LOGIN_REDIRECT, 12 | }); 13 | }; 14 | 15 | return ( 16 |
17 | 27 | 37 |
38 | ); 39 | } 40 | 41 | export default Socials; 42 | -------------------------------------------------------------------------------- /src/components/shared/ui/SortBy.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname, useRouter, useSearchParams } from 'next/navigation'; 4 | import React from 'react'; 5 | 6 | import { ChevronDown } from 'lucide-react'; 7 | 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from '@/components/ui/dropdown-menu'; 14 | 15 | type SortOption = { 16 | label: string; 17 | value: string; 18 | }; 19 | 20 | type SortByProps = { 21 | options: SortOption[]; 22 | label?: React.ReactNode; 23 | }; 24 | 25 | function SortBy({ options, label = 'Sort By' }: SortByProps) { 26 | const searchParams = useSearchParams(); 27 | const pathname = usePathname(); 28 | const router = useRouter(); 29 | 30 | const currentSort = searchParams.get('sort'); 31 | 32 | const handleSort = (value: string) => { 33 | const params = new URLSearchParams(searchParams); 34 | params.set('sort', value); 35 | 36 | const newUrl = `${pathname}?${params.toString()}`; 37 | 38 | router.push(newUrl); 39 | }; 40 | 41 | const getCurrentSortLabel = () => { 42 | const option = options.find((opt) => opt.value === currentSort); 43 | return option ? option.label : 'Sorting'; 44 | }; 45 | 46 | return ( 47 | 48 | 49 | {getCurrentSortLabel()} 50 | 51 | 52 | 53 | {options.map((option) => ( 54 | handleSort(option.value)} 57 | className="flex cursor-pointer items-center justify-between gap-2" 58 | > 59 |

{option.label}

60 | 61 |

{currentSort === option.value && }

62 |
63 | ))} 64 |
65 |
66 | ); 67 | } 68 | 69 | export default SortBy; 70 | -------------------------------------------------------------------------------- /src/components/shared/ui/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useFormStatus } from 'react-dom'; 4 | 5 | import MiniSpinner from './MiniSpinner'; 6 | import Button from './Button'; 7 | import { cn } from '@/lib/utils'; 8 | 9 | export default function SubmitButton({ 10 | children, 11 | className, 12 | noValue, 13 | }: { 14 | children: string | React.ReactNode; 15 | className?: string; 16 | noValue?: boolean; 17 | }) { 18 | const { pending } = useFormStatus(); 19 | 20 | return ( 21 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/shared/ui/TextArea.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ComponentPropsWithoutRef } from 'react'; 4 | import { useFormStatus } from 'react-dom'; 5 | import FormError from './FormError'; 6 | 7 | type TextAreaProps = { 8 | label: string; 9 | error?: string[]; 10 | } & ComponentPropsWithoutRef<'textarea'>; 11 | 12 | function TextArea({ children, id, label, error, ...props }: TextAreaProps) { 13 | const { pending } = useFormStatus(); 14 | 15 | return ( 16 |
17 | 18 |