├── app ├── _components │ ├── ui │ │ ├── CustomUploader.tsx │ │ ├── ErrorMessage.tsx │ │ ├── PrintButton.tsx │ │ ├── MyLink.tsx │ │ ├── LogoutButton.tsx │ │ ├── ImagesSlider.tsx │ │ ├── SubmitButton.tsx │ │ ├── LanguageSwitcher.tsx │ │ ├── IconButton.tsx │ │ ├── ChangeLanguage.tsx │ │ ├── ModalImage.tsx │ │ ├── Switch.tsx │ │ ├── Selectors.tsx │ │ ├── Menu.tsx │ │ ├── ToggleTheme.tsx │ │ ├── User.tsx │ │ ├── Modal.tsx │ │ ├── Button.tsx │ │ ├── Filter.tsx │ │ └── Marquee.tsx │ ├── marketking │ │ ├── Banner.tsx │ │ ├── CategorySelection.tsx │ │ ├── ProductInfo.tsx │ │ ├── CategoryHeader.tsx │ │ ├── FeaturedProductsSkeleton.tsx │ │ ├── FilterSheet.tsx │ │ ├── PlaceholderBanner.tsx │ │ ├── ProductInfoLayout.tsx │ │ ├── BannerSkeleton.tsx │ │ ├── FavButton.tsx │ │ ├── Footer.tsx │ │ ├── FavoriteProductsList.tsx │ │ ├── AllProducts.tsx │ │ ├── Phones.tsx │ │ ├── Laptops.tsx │ │ ├── Watches.tsx │ │ ├── IncAndDecProductItem.tsx │ │ ├── RelatedProducts.tsx │ │ ├── FeaturedProducts.tsx │ │ ├── LoginFirst.tsx │ │ ├── FavoriteProductsListSkeleton.tsx │ │ ├── Auth.tsx │ │ ├── CategoryPreviews.tsx │ │ ├── LoginForm.tsx │ │ ├── ProductInfoSkeleton.tsx │ │ ├── ImageSlider.tsx │ │ ├── CreateAccountForm.tsx │ │ ├── EditProfileForm.tsx │ │ └── WhishList.tsx │ ├── theme-provider.tsx │ ├── Toaster.tsx │ ├── header │ │ ├── NavLinks.tsx │ │ ├── NavLink.tsx │ │ ├── Header.tsx │ │ └── Logo.tsx │ └── dashboard │ │ ├── StatusItem.tsx │ │ ├── GraphCard.tsx │ │ ├── Chart.tsx │ │ ├── PaginationOrderTable.tsx │ │ ├── StatusList.tsx │ │ ├── RecentSalesCard.tsx │ │ ├── BannerTable.tsx │ │ └── CreateBanner.tsx ├── [locale] │ ├── icon.png │ ├── (marketking) │ │ ├── loading.tsx │ │ ├── products │ │ │ ├── layout.tsx │ │ │ ├── all │ │ │ │ └── page.tsx │ │ │ ├── laptops │ │ │ │ └── page.tsx │ │ │ ├── phones │ │ │ │ └── page.tsx │ │ │ └── watches │ │ │ │ └── page.tsx │ │ ├── auth │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ ├── sign-up │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── page.tsx │ │ ├── layout.tsx │ │ ├── payment │ │ │ ├── cancel │ │ │ │ └── page.tsx │ │ │ └── success │ │ │ │ └── page.tsx │ │ ├── edit-profile │ │ │ └── page.tsx │ │ ├── product │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── favorite-products │ │ │ └── page.tsx │ │ └── customer-order │ │ │ └── page.tsx │ ├── dashboard │ │ ├── loading.tsx │ │ ├── page.tsx │ │ ├── banner │ │ │ ├── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ └── [id] │ │ │ │ └── delete │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── products │ │ │ ├── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ └── [id] │ │ │ │ ├── page.tsx │ │ │ │ └── delete │ │ │ │ └── page.tsx │ │ └── orders │ │ │ └── page.tsx │ ├── error.tsx │ └── layout.tsx ├── _lib │ ├── stripe.ts │ ├── uploadthing.ts │ └── db.ts ├── api │ ├── uploadthing │ │ ├── route.ts │ │ └── core.ts │ ├── logout │ │ └── route.ts │ └── stripe │ │ └── route.ts ├── fonts.ts ├── _hooks │ ├── useTranslate.ts │ ├── useClickOutside.ts │ └── useElementsForm.ts ├── _utils │ ├── getUser.ts │ ├── consistent.ts │ ├── helpers.ts │ └── types.ts └── _actions │ ├── deleteBanner.ts │ ├── deleteProduct.ts │ ├── deleteItemCart.ts │ ├── increaseProductItem.ts │ ├── decreaseProductItem.ts │ ├── fetchMoreProducts.ts │ ├── createBanner.ts │ ├── editProfile.ts │ ├── loginAccount.ts │ ├── createAndUpdateCart.ts │ ├── checkout.ts │ ├── createProduct.ts │ ├── editProduct.ts │ ├── toggleFavProduct.ts │ └── createAccount.ts ├── .eslintrc.json ├── public ├── logo.png ├── unknownUser.jpg ├── auth-preview.jpg ├── laptops-preview.jpg ├── phones-preview.jpg ├── project-preview.png ├── watches-prview.webp └── placeholder-banner.png ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20240722170420_add_favorite_products │ │ └── migration.sql └── schema.prisma ├── postcss.config.mjs ├── lib └── utils.ts ├── middleware.ts ├── components.json ├── i18n.ts ├── next.config.mjs ├── .gitignore ├── tsconfig.json ├── package.json ├── components └── ui │ ├── avatar.tsx │ └── button.tsx ├── README.md └── tailwind.config.ts /app/_components/ui/CustomUploader.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/logo.png -------------------------------------------------------------------------------- /app/[locale]/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/app/[locale]/icon.png -------------------------------------------------------------------------------- /public/unknownUser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/unknownUser.jpg -------------------------------------------------------------------------------- /public/auth-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/auth-preview.jpg -------------------------------------------------------------------------------- /public/laptops-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/laptops-preview.jpg -------------------------------------------------------------------------------- /public/phones-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/phones-preview.jpg -------------------------------------------------------------------------------- /public/project-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/project-preview.png -------------------------------------------------------------------------------- /public/watches-prview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/watches-prview.webp -------------------------------------------------------------------------------- /public/placeholder-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fares-ahmedd/marketKing-ecommerce/HEAD/public/placeholder-banner.png -------------------------------------------------------------------------------- /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 = "postgresql" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/_lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_API_KEY as string, { 4 | apiVersion: "2024-06-20", 5 | typescript: true, 6 | }); 7 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createRouteHandler({ 7 | router: ourFileRouter, 8 | }); 9 | -------------------------------------------------------------------------------- /app/_components/marketking/Banner.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import BannerSlider from "./BannerSlider"; 3 | 4 | async function Banner() { 5 | const banners = await prisma?.banner?.findMany(); 6 | 7 | return ; 8 | } 9 | 10 | export default Banner; 11 | -------------------------------------------------------------------------------- /app/_lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateUploadButton, 3 | generateUploadDropzone, 4 | } from "@uploadthing/react"; 5 | import { OurFileRouter } from "../api/uploadthing/core"; 6 | 7 | export const UploadButton = generateUploadButton(); 8 | export const UploadDropzone = generateUploadDropzone(); 9 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/loading.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 |
4 | 5 |
6 | ); 7 | }; 8 | 9 | export default Loader; 10 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | const Loader = () => { 2 | return ( 3 |
4 | 5 |
6 | ); 7 | }; 8 | 9 | export default Loader; 10 | -------------------------------------------------------------------------------- /app/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Roboto, Noto_Kufi_Arabic } from "next/font/google"; 2 | 3 | export const roboto = Roboto({ 4 | weight: ["400", "700"], 5 | subsets: ["latin"], 6 | display: "swap", 7 | }); 8 | 9 | export const NotoKufiArabic = Noto_Kufi_Arabic({ 10 | subsets: ["arabic"], 11 | weight: ["400", "700"], 12 | display: "swap", 13 | }); 14 | -------------------------------------------------------------------------------- /app/_components/marketking/CategorySelection.tsx: -------------------------------------------------------------------------------- 1 | import CategoryHeader from "./CategoryHeader"; 2 | import CategoryPreviews from "./CategoryPreviews"; 3 | 4 | function CategorySelection() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | 13 | export default CategorySelection; 14 | -------------------------------------------------------------------------------- /app/_components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /app/_hooks/useTranslate.ts: -------------------------------------------------------------------------------- 1 | import { useLocale, useTranslations } from "next-intl"; 2 | 3 | export function useTranslate() { 4 | const t = useTranslations("Index"); 5 | const locale = useLocale(); 6 | 7 | const isArabic = locale === "ar"; 8 | return { 9 | t: (value: string, params?: Record) => t(value, params), 10 | isArabic, 11 | lang: locale, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import createMiddleware from "next-intl/middleware"; 2 | 3 | export default createMiddleware({ 4 | // A list of all locales that are supported 5 | locales: ["en", "ar"], 6 | 7 | // Used when no locale matches 8 | defaultLocale: "en", 9 | }); 10 | 11 | export const config = { 12 | // Match only internationalized pathnames 13 | matcher: ["/", "/(ar|en)/:path*"], 14 | }; 15 | -------------------------------------------------------------------------------- /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": "app/[locale]/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /app/api/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { revalidatePath } from "next/cache"; 2 | import { cookies } from "next/headers"; 3 | 4 | export async function POST(_: any) { 5 | // Clear the user_info cookie 6 | cookies().delete("user_info"); 7 | 8 | revalidatePath("/", "layout"); 9 | return new Response(JSON.stringify({ message: "Logged out successfully" }), { 10 | status: 200, 11 | headers: { "Content-Type": "application/json" }, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /app/_lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | declare const globalThis: { 8 | prismaGlobal: ReturnType; 9 | } & typeof global; 10 | 11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton(); 12 | 13 | export default prisma; 14 | 15 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma; 16 | -------------------------------------------------------------------------------- /i18n.ts: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation"; 2 | import { getRequestConfig } from "next-intl/server"; 3 | 4 | // Can be imported from a shared config 5 | const locales = ["en", "ar"]; 6 | 7 | export default getRequestConfig(async ({ locale }) => { 8 | // Validate that the incoming `locale` parameter is valid 9 | if (!locales.includes(locale as any)) notFound(); 10 | 11 | return { 12 | messages: (await import(`./messages/${locale}.json`)).default, 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /app/_components/ui/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { PiWarningOctagonBold } from "react-icons/pi"; 2 | 3 | function ErrorMessage({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
10 | {children} 11 |
12 | ); 13 | } 14 | 15 | export default ErrorMessage; 16 | -------------------------------------------------------------------------------- /app/_components/ui/PrintButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import Button from "./Button"; 4 | 5 | function PrintButton() { 6 | const { t } = useTranslate(); 7 | 8 | const handlePrint = () => { 9 | window.print(); 10 | }; 11 | return ( 12 |
13 | 16 |
17 | ); 18 | } 19 | 20 | export default PrintButton; 21 | -------------------------------------------------------------------------------- /app/_components/Toaster.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from "react-hot-toast"; 2 | 3 | export default function ToasterProvider() { 4 | return ( 5 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/_utils/getUser.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | import prisma from "../_lib/db"; 3 | 4 | export async function getUser() { 5 | const userInfoCookie = cookies().get("user_info"); 6 | let user = null; 7 | 8 | if (userInfoCookie) { 9 | const userInfo = JSON.parse(userInfoCookie.value); 10 | user = await prisma?.user?.findUnique({ 11 | where: { email: userInfo.email, id: userInfo.userId }, 12 | include: { favoriteProducts: { include: { product: true } } }, 13 | }); 14 | } 15 | 16 | return user; 17 | } 18 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from "next-intl/plugin"; 2 | 3 | const withNextIntl = createNextIntlPlugin(); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | images: { 8 | remotePatterns: [ 9 | { protocol: "https", hostname: "utfs.io", port: "", pathname: "/**" }, 10 | { 11 | protocol: "https", 12 | hostname: "avatar.vercel.sh", 13 | port: "", 14 | pathname: "/**", 15 | }, 16 | ], 17 | }, 18 | }; 19 | 20 | export default withNextIntl(nextConfig); 21 | -------------------------------------------------------------------------------- /.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 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/products/layout.tsx: -------------------------------------------------------------------------------- 1 | import Filter from "@/app/_components/ui/Filter"; 2 | import { unstable_setRequestLocale } from "next-intl/server"; 3 | function layout({ 4 | children, 5 | params: { locale }, 6 | }: { 7 | children: React.ReactNode; 8 | params: { locale: string }; 9 | }) { 10 | unstable_setRequestLocale(locale); 11 | 12 | return ( 13 |
14 | 15 | 16 | {children} 17 |
18 | ); 19 | } 20 | 21 | export default layout; 22 | -------------------------------------------------------------------------------- /app/_actions/deleteBanner.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import prisma from "../_lib/db"; 5 | import { getTranslate } from "../_utils/helpers"; 6 | 7 | export async function deleteBanner(formData: FormData) { 8 | const bannerId = formData.get("bannerId") as string; 9 | 10 | const { isArabic } = await getTranslate(); 11 | 12 | await prisma.banner.delete({ 13 | where: { id: bannerId }, 14 | }); 15 | 16 | if (isArabic) { 17 | redirect("/ar/dashboard/banner"); 18 | } else { 19 | redirect("/en/dashboard/banner"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/_actions/deleteProduct.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import prisma from "../_lib/db"; 5 | import { getTranslate } from "../_utils/helpers"; 6 | 7 | export async function deleteProduct(formData: FormData) { 8 | const productId = formData.get("productId") as string; 9 | 10 | const { isArabic } = await getTranslate(); 11 | 12 | await prisma.product.delete({ 13 | where: { id: productId }, 14 | }); 15 | 16 | if (isArabic) { 17 | redirect("/ar/dashboard/products"); 18 | } else { 19 | redirect("/en/dashboard/products"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/_components/header/NavLinks.tsx: -------------------------------------------------------------------------------- 1 | import { dashboardNavLinks, homeNavLinks } from "@/app/_utils/helpers"; 2 | import NavLink from "./NavLink"; 3 | 4 | type Props = { 5 | isDashboard?: boolean; 6 | }; 7 | 8 | function NavLinks({ isDashboard = false }: Props) { 9 | const navLinks = isDashboard ? dashboardNavLinks : homeNavLinks; 10 | return ( 11 | 18 | ); 19 | } 20 | 21 | export default NavLinks; 22 | -------------------------------------------------------------------------------- /app/_components/marketking/ProductInfo.tsx: -------------------------------------------------------------------------------- 1 | import { IUserIncludeFavorites } from "@/app/_utils/types"; 2 | import { Product } from "@prisma/client"; 3 | import ImageSlider from "./ImageSlider"; 4 | import ProductPreview from "./ProductPreview"; 5 | 6 | function ProductInfo({ 7 | product, 8 | user, 9 | }: { 10 | product: Product; 11 | user: IUserIncludeFavorites; 12 | }) { 13 | return ( 14 |
15 | 16 | 17 | 18 |
19 | ); 20 | } 21 | 22 | export default ProductInfo; 23 | -------------------------------------------------------------------------------- /app/_components/ui/MyLink.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | 5 | const MyLink = React.forwardRef< 6 | HTMLAnchorElement, 7 | { 8 | children: React.ReactNode; 9 | href: string; 10 | className?: string; 11 | [key: string]: any; 12 | } 13 | >(({ children, href, className, ...props }, ref) => { 14 | const { lang } = useTranslate(); 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }); 21 | 22 | MyLink.displayName = "MyLink"; 23 | 24 | export default MyLink; 25 | -------------------------------------------------------------------------------- /app/_components/marketking/CategoryHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import MyLink from "../ui/MyLink"; 3 | 4 | function CategoryHeader() { 5 | const { isArabic, t } = useTranslate(); 6 | return ( 7 |
8 |

{t("Shop By")}

9 | 13 | {t("Browse All")}{" "} 14 | {isArabic ? "←" : "→"} 15 | 16 |
17 | ); 18 | } 19 | 20 | export default CategoryHeader; 21 | -------------------------------------------------------------------------------- /app/_components/marketking/FeaturedProductsSkeleton.tsx: -------------------------------------------------------------------------------- 1 | function FeaturedProductsSkeleton() { 2 | return ( 3 |
4 |
5 |
    6 | {Array.from({ length: 4 }, (_, index) => ( 7 |
  • 11 | ))} 12 |
13 |
14 | ); 15 | } 16 | 17 | export default FeaturedProductsSkeleton; 18 | -------------------------------------------------------------------------------- /app/_components/marketking/FilterSheet.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Sheet, 3 | SheetContent, 4 | SheetDescription, 5 | SheetHeader, 6 | SheetTitle, 7 | SheetTrigger, 8 | } from "@/components/ui/sheet"; 9 | import { FaFilter } from "react-icons/fa"; 10 | import IconButton from "../ui/IconButton"; 11 | import Filter from "../ui/Filter"; 12 | 13 | function FilterSheet() { 14 | return ( 15 | 16 | 17 | 18 | Filter 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export default FilterSheet; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | ".next/types/**/*.ts", 29 | "middleware.ts" 30 | ], 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /app/_components/header/NavLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { usePathname } from "next/navigation"; 4 | import MyLink from "../ui/MyLink"; 5 | 6 | function NavLink({ link }: { link: { href: string; label: string } }) { 7 | const pathname = usePathname(); 8 | const { t, lang } = useTranslate(); 9 | 10 | const isActive = pathname === `/${lang}${link.href === "/" ? "" : link.href}`; 11 | 12 | return ( 13 |
  • 18 | {t(link.label)} 19 |
  • 20 | ); 21 | } 22 | 23 | export default NavLink; 24 | -------------------------------------------------------------------------------- /app/_components/marketking/PlaceholderBanner.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import placeholderBanner from "@/public/placeholder-banner.png"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | 5 | function PlaceholderBanner() { 6 | const { t } = useTranslate(); 7 | return ( 8 |
    9 | Banner Placeholder 15 |

    16 | {t("Placeholder Banner")} 17 |

    18 |
    19 | ); 20 | } 21 | 22 | export default PlaceholderBanner; 23 | -------------------------------------------------------------------------------- /app/_actions/deleteItemCart.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | 6 | export async function deleteItemCart({ 7 | userId, 8 | itemId, 9 | }: { 10 | userId: string; 11 | itemId: string; 12 | }) { 13 | const cart = await prisma.cart.findUnique({ 14 | where: { userId: userId }, 15 | include: { items: true }, 16 | }); 17 | 18 | if (!cart) { 19 | throw new Error("Cart not found for this user"); 20 | } 21 | 22 | const cartItem = cart.items.find((item) => item.id === itemId); 23 | 24 | if (!cartItem) { 25 | throw new Error("Cart item not found in this user's cart"); 26 | } 27 | 28 | await prisma.cartItem.delete({ 29 | where: { id: itemId }, 30 | }); 31 | 32 | revalidatePath("/", "layout"); 33 | } 34 | -------------------------------------------------------------------------------- /app/_components/marketking/ProductInfoLayout.tsx: -------------------------------------------------------------------------------- 1 | import FeaturedProducts from "@/app/_components/marketking/FeaturedProducts"; 2 | import RelatedProducts from "@/app/_components/marketking/RelatedProducts"; 3 | import prisma from "@/app/_lib/db"; 4 | import { getUser } from "@/app/_utils/getUser"; 5 | import { notFound } from "next/navigation"; 6 | import ProductInfo from "./ProductInfo"; 7 | 8 | async function ProductInfoLayout({ id }: { id: string }) { 9 | const product: any = await prisma.product.findUnique({ where: { id } }); 10 | const user: any = await getUser(); 11 | 12 | if (!product) notFound(); 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default ProductInfoLayout; 23 | -------------------------------------------------------------------------------- /app/_utils/consistent.ts: -------------------------------------------------------------------------------- 1 | import laptopsPreview from "@/public/laptops-preview.jpg"; 2 | import phonesPreview from "@/public/phones-preview.jpg"; 3 | import watchesPreview from "@/public/watches-prview.webp"; 4 | 5 | export const ADMIN_EMAIL = "faresahmed00001111@gmail.com"; 6 | export const LOCALES = ["en", "ar"]; 7 | 8 | export const CATEGORIES = [ 9 | { 10 | href: "/products/phones", 11 | src: phonesPreview, 12 | alt: "Phones Products", 13 | title: "Phones", 14 | span: "row-span-full", 15 | }, 16 | { 17 | href: "/products/watches", 18 | src: watchesPreview, 19 | alt: "watches Products", 20 | title: "Watches", 21 | span: null, 22 | }, 23 | { 24 | href: "/products/laptops", 25 | src: laptopsPreview, 26 | alt: "laptops Products", 27 | title: "Laptops", 28 | span: null, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import LoginForm from "@/app/_components/marketking/LoginForm"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 4 | 5 | export async function generateMetadata({ 6 | params: { locale }, 7 | }: { 8 | params: { locale: string }; 9 | }) { 10 | const t = await getTranslations({ locale, namespace: "metadata" }); 11 | 12 | return { 13 | title: `${t("Login")}`, 14 | }; 15 | } 16 | 17 | function LoginPage({ params: { locale } }: { params: { locale: string } }) { 18 | unstable_setRequestLocale(locale); 19 | 20 | const { t } = useTranslate(); 21 | return ( 22 | <> 23 |

    {t("Login")}

    24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default LoginPage; 31 | -------------------------------------------------------------------------------- /app/_actions/increaseProductItem.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | 6 | export async function increaseProductItem({ 7 | itemId, 8 | userId, 9 | }: { 10 | itemId: string; 11 | userId: string; 12 | }) { 13 | const cart = await prisma.cart.findUnique({ 14 | where: { userId: userId }, 15 | include: { items: true }, 16 | }); 17 | 18 | if (!cart) { 19 | throw new Error("Cart not found for this user"); 20 | } 21 | 22 | const cartItem = cart.items.find((item) => item.id === itemId); 23 | 24 | if (!cartItem) { 25 | throw new Error("Cart item not found in this user's cart"); 26 | } 27 | 28 | await prisma.cartItem.update({ 29 | where: { id: cartItem.id }, 30 | data: { quantity: cartItem.quantity + 1 }, 31 | }); 32 | 33 | revalidatePath("/", "layout"); 34 | } 35 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/auth/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import CreateAccountForm from "@/app/_components/marketking/CreateAccountForm"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 4 | 5 | export async function generateMetadata({ 6 | params: { locale }, 7 | }: { 8 | params: { locale: string }; 9 | }) { 10 | const t = await getTranslations({ locale, namespace: "metadata" }); 11 | 12 | return { 13 | title: `${t("Sign Up")}`, 14 | }; 15 | } 16 | 17 | function SignUpPage({ params: { locale } }: { params: { locale: string } }) { 18 | unstable_setRequestLocale(locale); 19 | 20 | const { t } = useTranslate(); 21 | return ( 22 | <> 23 |

    {t("Sign Up")}

    24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default SignUpPage; 31 | -------------------------------------------------------------------------------- /app/_components/marketking/BannerSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function BannerSkeleton() { 4 | return ( 5 |
    6 |
    7 | 8 |
    9 |
    10 |
    11 |
    12 | 13 |
      14 | {Array.from({ length: 3 }, (_, index) => ( 15 |
    • 19 | ))} 20 |
    21 |
    22 | ); 23 | } 24 | 25 | export default BannerSkeleton; 26 | -------------------------------------------------------------------------------- /app/_components/marketking/FavButton.tsx: -------------------------------------------------------------------------------- 1 | import { useFormStatus } from "react-dom"; 2 | import { FaRegHeart, FaHeart } from "react-icons/fa"; 3 | 4 | function FavButton({ 5 | className, 6 | isFav = false, 7 | onClick, 8 | }: { 9 | className?: string; 10 | isFav?: boolean; 11 | onClick?: () => void; 12 | }) { 13 | const { pending } = useFormStatus(); 14 | 15 | return ( 16 | 29 | ); 30 | } 31 | 32 | export default FavButton; 33 | -------------------------------------------------------------------------------- /app/_components/marketking/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { FaGithub } from "react-icons/fa"; 2 | import IconButton from "../ui/IconButton"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | 5 | function Footer() { 6 | const { t } = useTranslate(); 7 | return ( 8 | 27 | ); 28 | } 29 | 30 | export default Footer; 31 | -------------------------------------------------------------------------------- /app/_components/ui/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; 5 | import { useTranslate } from "@/app/_hooks/useTranslate"; 6 | 7 | export default function LogoutButton() { 8 | const router = useRouter(); 9 | const { t } = useTranslate(); 10 | const handleLogout = async () => { 11 | try { 12 | const response = await fetch("/api/logout", { method: "POST" }); 13 | 14 | if (!response.ok) { 15 | throw new Error("Logout failed"); 16 | } 17 | 18 | router.refresh(); 19 | } catch (error) { 20 | console.error("Logout error:", error); 21 | } 22 | }; 23 | 24 | return ( 25 | 29 | {t("Logout")} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/_components/marketking/FavoriteProductsList.tsx: -------------------------------------------------------------------------------- 1 | import ProductList from "./ProductList"; 2 | import { getUser } from "@/app/_utils/getUser"; 3 | import { getTranslate } from "@/app/_utils/helpers"; 4 | import { redirect } from "next/navigation"; 5 | async function FavoriteProductsList() { 6 | const user: any = await getUser(); 7 | const { isArabic, t } = await getTranslate(); 8 | if (!user) redirect(isArabic ? "/ar" : "/en"); 9 | 10 | const favoriteProducts: any = user?.favoriteProducts.map((fav: any) => ({ 11 | ...fav.product, 12 | })); 13 | 14 | return ( 15 | <> 16 | {favoriteProducts[0]?.id ? ( 17 | 18 | ) : ( 19 |

    20 | {t("no favorite products")} 21 |

    22 | )} 23 | 24 | ); 25 | } 26 | 27 | export default FavoriteProductsList; 28 | -------------------------------------------------------------------------------- /app/_components/marketking/AllProducts.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import ProductList from "./ProductList"; 3 | import { getUser } from "@/app/_utils/getUser"; 4 | 5 | async function AllProducts({ 6 | searchParams, 7 | }: { 8 | searchParams: { 9 | "sort-price": string; 10 | "filter-price": string; 11 | }; 12 | }) { 13 | const sortPrice = searchParams["sort-price"] || "all"; 14 | const filterPrice = searchParams["filter-price"] || "all"; 15 | const allProducts = await prisma.product.findMany({ 16 | orderBy: { createdAt: "desc" }, 17 | }); 18 | const totalProducts = await prisma.product.count(); 19 | const user: any = await getUser(); 20 | 21 | return ( 22 | 29 | ); 30 | } 31 | 32 | export default AllProducts; 33 | -------------------------------------------------------------------------------- /app/_components/marketking/Phones.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import ProductList from "./ProductList"; 3 | import { getUser } from "@/app/_utils/getUser"; 4 | 5 | async function Phones({ 6 | searchParams, 7 | }: { 8 | searchParams: { 9 | "sort-price": string; 10 | "filter-price": string; 11 | }; 12 | }) { 13 | const sortPrice = searchParams["sort-price"] || "all"; 14 | const filterPrice = searchParams["filter-price"] || "all"; 15 | const phonesProducts = await prisma.product.findMany({ 16 | where: { category: "phones" }, 17 | orderBy: { createdAt: "desc" }, 18 | }); 19 | 20 | const totalProducts = phonesProducts?.length; 21 | const user: any = await getUser(); 22 | 23 | return ( 24 | 32 | ); 33 | } 34 | 35 | export default Phones; 36 | -------------------------------------------------------------------------------- /app/_components/marketking/Laptops.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import ProductList from "./ProductList"; 3 | import { getUser } from "@/app/_utils/getUser"; 4 | 5 | async function Laptops({ 6 | searchParams, 7 | }: { 8 | searchParams: { 9 | "sort-price": string; 10 | "filter-price": string; 11 | }; 12 | }) { 13 | const sortPrice = searchParams["sort-price"] || "all"; 14 | const filterPrice = searchParams["filter-price"] || "all"; 15 | const laptopsProducts = await prisma.product.findMany({ 16 | where: { category: "laptops" }, 17 | orderBy: { createdAt: "desc" }, 18 | }); 19 | 20 | const totalProducts = laptopsProducts?.length; 21 | const user: any = await getUser(); 22 | 23 | return ( 24 | 32 | ); 33 | } 34 | 35 | export default Laptops; 36 | -------------------------------------------------------------------------------- /app/_components/marketking/Watches.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import ProductList from "./ProductList"; 3 | import { getUser } from "@/app/_utils/getUser"; 4 | 5 | async function Watches({ 6 | searchParams, 7 | }: { 8 | searchParams: { 9 | "sort-price": string; 10 | "filter-price": string; 11 | }; 12 | }) { 13 | const sortPrice = searchParams["sort-price"] || "all"; 14 | const filterPrice = searchParams["filter-price"] || "all"; 15 | const watchesProducts = await prisma.product.findMany({ 16 | where: { category: "watches" }, 17 | orderBy: { createdAt: "desc" }, 18 | }); 19 | 20 | const totalProducts = watchesProducts?.length; 21 | const user: any = await getUser(); 22 | 23 | return ( 24 | 32 | ); 33 | } 34 | 35 | export default Watches; 36 | -------------------------------------------------------------------------------- /app/_hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, RefObject } from "react"; 2 | 3 | type Handler = (event: MouseEvent | TouchEvent) => void; 4 | 5 | function useClickOutside( 6 | refs: RefObject[], 7 | handler: Handler 8 | ): void { 9 | useEffect(() => { 10 | const listener = (event: MouseEvent | TouchEvent) => { 11 | // Check if the click is outside all provided refs 12 | const isOutside = refs.every((ref) => { 13 | const el = ref.current; 14 | return !el || !el.contains((event.target as Node) || null); 15 | }); 16 | 17 | if (isOutside) { 18 | handler(event); 19 | } 20 | }; 21 | 22 | document.addEventListener("mousedown", listener); 23 | document.addEventListener("touchstart", listener); 24 | 25 | return () => { 26 | document.removeEventListener("mousedown", listener); 27 | document.removeEventListener("touchstart", listener); 28 | }; 29 | }, [refs, handler]); 30 | } 31 | 32 | export default useClickOutside; 33 | -------------------------------------------------------------------------------- /app/_components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion, useMotionValueEvent, useScroll } from "framer-motion"; 3 | import { useState } from "react"; 4 | 5 | function Header({ children }: { children: React.ReactNode }) { 6 | const [hidden, setHidden] = useState(false); 7 | const { scrollY } = useScroll(); 8 | 9 | useMotionValueEvent(scrollY, "change", (latest) => { 10 | const prev = scrollY.getPrevious() || 0; 11 | if (latest > prev && latest > 150) { 12 | setHidden(true); 13 | } else { 14 | setHidden(false); 15 | } 16 | }); 17 | 18 | return ( 19 | 28 |
    {children}
    29 |
    30 | ); 31 | } 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/page.tsx: -------------------------------------------------------------------------------- 1 | import Banner from "@/app/_components/marketking/Banner"; 2 | import BannerSkeleton from "@/app/_components/marketking/BannerSkeleton"; 3 | import CategorySelection from "@/app/_components/marketking/CategorySelection"; 4 | import FeaturedProducts from "@/app/_components/marketking/FeaturedProducts"; 5 | import FeaturedProductsSkeleton from "@/app/_components/marketking/FeaturedProductsSkeleton"; 6 | import { unstable_setRequestLocale } from "next-intl/server"; 7 | import { Suspense } from "react"; 8 | 9 | export default function HomePage({ 10 | params: { locale }, 11 | }: { 12 | params: { locale: string }; 13 | }) { 14 | unstable_setRequestLocale(locale); 15 | return ( 16 |
    17 | }> 18 | 19 | 20 | 21 | 22 | 23 | }> 24 | 25 | 26 |
    27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/_components/dashboard/StatusItem.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | 3 | interface Props { 4 | title: string; 5 | icon: React.ReactNode; 6 | statusValue: string | number; 7 | labelNumber?: number; 8 | label: string; 9 | } 10 | 11 | function StatusItem({ 12 | title, 13 | icon, 14 | statusValue, 15 | label, 16 | labelNumber = 0, 17 | }: Props) { 18 | const { t } = useTranslate(); 19 | return ( 20 |
  • 21 |

    22 | {t(title)}{" "} 23 | {icon} 24 |

    25 |

    {statusValue}

    26 | 31 |
  • 32 | ); 33 | } 34 | 35 | export default StatusItem; 36 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/app/_components/header/Header"; 2 | import Logo from "@/app/_components/header/Logo"; 3 | import NavLinks from "@/app/_components/header/NavLinks"; 4 | import Auth from "@/app/_components/marketking/Auth"; 5 | import BottomNavigation from "@/app/_components/marketking/BottomNavigation."; 6 | import Footer from "@/app/_components/marketking/Footer"; 7 | import { unstable_setRequestLocale } from "next-intl/server"; 8 | 9 | export default function MarkKingLayout({ 10 | children, 11 | params: { locale }, 12 | }: { 13 | children: React.ReactNode; 14 | params: { locale: string }; 15 | }) { 16 | unstable_setRequestLocale(locale); 17 | 18 | return ( 19 | <> 20 |
    21 | 22 | 23 | 24 |
    25 |
    26 | {children} 27 |
    28 |
    29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/_actions/decreaseProductItem.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | 6 | export async function decreaseProductItem({ 7 | itemId, 8 | userId, 9 | }: { 10 | itemId: string; 11 | userId: string; 12 | }) { 13 | const cart = await prisma.cart.findUnique({ 14 | where: { userId: userId }, 15 | include: { items: true }, 16 | }); 17 | 18 | if (!cart) { 19 | throw new Error("Cart not found for this user"); 20 | } 21 | 22 | const cartItem = cart.items.find((item) => item.id === itemId); 23 | 24 | if (!cartItem) { 25 | throw new Error("Cart item not found in this user's cart"); 26 | } 27 | 28 | if (cartItem.quantity > 1) { 29 | await prisma.cartItem.update({ 30 | where: { id: cartItem.id }, 31 | data: { quantity: cartItem.quantity - 1 }, 32 | }); 33 | } 34 | 35 | if (cartItem.quantity === 1) { 36 | await prisma.cartItem.delete({ 37 | where: { id: cartItem.id }, 38 | }); 39 | } 40 | revalidatePath("/", "layout"); 41 | } 42 | -------------------------------------------------------------------------------- /app/_components/ui/ImagesSlider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Carousel, 3 | CarouselContent, 4 | CarouselItem, 5 | CarouselNext, 6 | CarouselPrevious, 7 | } from "@/components/ui/carousel"; 8 | import Image from "next/image"; 9 | type ImagesProp = { 10 | item: { images: string[]; name: string }; 11 | }; 12 | function ImagesSlider({ item }: ImagesProp) { 13 | return ( 14 | 15 | 16 | {item.images.map((image) => ( 17 | 18 |
    19 | {`${item.name}`} 25 |
    26 |
    27 | ))} 28 |
    29 | 30 | 31 |
    32 | ); 33 | } 34 | 35 | export default ImagesSlider; 36 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 2 | import GraphCard from "@/app/_components/dashboard/GraphCard"; 3 | import RecentSalesCard from "@/app/_components/dashboard/RecentSalesCard"; 4 | import StatusList from "@/app/_components/dashboard/StatusList"; 5 | import prisma from "@/app/_lib/db"; 6 | 7 | export async function generateMetadata({ 8 | params: { locale }, 9 | }: { 10 | params: { locale: string }; 11 | }) { 12 | const t = await getTranslations({ locale, namespace: "metadata" }); 13 | 14 | return { 15 | title: `${t("Dashboard")}`, 16 | }; 17 | } 18 | 19 | function Dashboard({ params: { locale } }: { params: { locale: string } }) { 20 | unstable_setRequestLocale(locale); 21 | return ( 22 |
    23 | 24 |
    25 | 26 | 27 |
    28 |
    29 | ); 30 | } 31 | 32 | export default Dashboard; 33 | -------------------------------------------------------------------------------- /app/_components/header/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import { FaCrown } from "react-icons/fa"; 3 | import MyLink from "../ui/MyLink"; 4 | 5 | interface LogoProps { 6 | isLink?: boolean; 7 | } 8 | 9 | function Logo({ isLink = true }: LogoProps) { 10 | const { t, isArabic } = useTranslate(); 11 | 12 | const logoContent = ( 13 |

    18 | {t("Market")} 19 | 24 | {isArabic && " "} 25 | {t("King")} 26 | 27 | 32 |

    33 | ); 34 | 35 | return isLink ? {logoContent} : logoContent; 36 | } 37 | 38 | export default Logo; 39 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import { unstable_setRequestLocale } from "next-intl/server"; 2 | import authPreview from "@/public/auth-preview.jpg"; 3 | import logo from "@/public/logo.png"; 4 | import Image from "next/image"; 5 | function layout({ 6 | children, 7 | params: { locale }, 8 | }: { 9 | children: React.ReactNode; 10 | params: { locale: string }; 11 | }) { 12 | unstable_setRequestLocale(locale); 13 | 14 | return ( 15 |
    16 |
    17 | Logo 18 | {children} 19 |
    20 |
    21 | Autonation Preview 27 |
    28 |
    29 |
    30 | ); 31 | } 32 | 33 | export default layout; 34 | -------------------------------------------------------------------------------- /app/_components/ui/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useFormStatus } from "react-dom"; 3 | import Button from "./Button"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | 6 | interface SubmitButtonProps 7 | extends React.ButtonHTMLAttributes { 8 | children: React.ReactNode; 9 | iconOnly?: boolean; 10 | beforeContent?: React.ReactNode; 11 | afterContent?: React.ReactNode; 12 | size?: "sm" | "md" | "lg"; 13 | active?: boolean; 14 | disabled?: boolean; 15 | variant?: "primary" | "secondary"; 16 | className?: string; 17 | color?: "primary" | "black" | "white" | "info" | "warning" | "error"; 18 | } 19 | 20 | function SubmitButton({ children, ...props }: SubmitButtonProps) { 21 | const { pending } = useFormStatus(); 22 | const { t, isArabic } = useTranslate(); 23 | return ( 24 | 33 | ); 34 | } 35 | 36 | export default SubmitButton; 37 | -------------------------------------------------------------------------------- /app/_actions/fetchMoreProducts.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import prisma from "@/app/_lib/db"; 4 | 5 | export async function fetchMoreProducts(skip: number) { 6 | const products = await prisma.product.findMany({ 7 | skip, 8 | take: 6, 9 | orderBy: { createdAt: "desc" }, 10 | }); 11 | 12 | return products; 13 | } 14 | 15 | export async function fetchMorePhones(skip: number) { 16 | const products = await prisma.product.findMany({ 17 | skip, 18 | take: 6, 19 | where: { category: "phones" }, 20 | orderBy: { createdAt: "desc" }, 21 | }); 22 | 23 | return products; 24 | } 25 | 26 | export async function fetchMoreWatches(skip: number) { 27 | const products = await prisma.product.findMany({ 28 | skip, 29 | take: 6, 30 | where: { category: "watches" }, 31 | orderBy: { createdAt: "desc" }, 32 | }); 33 | 34 | return products; 35 | } 36 | export async function fetchMoreLaptops(skip: number) { 37 | const products = await prisma.product.findMany({ 38 | skip, 39 | take: 6, 40 | where: { category: "laptops" }, 41 | orderBy: { createdAt: "desc" }, 42 | }); 43 | 44 | return products; 45 | } 46 | -------------------------------------------------------------------------------- /app/[locale]/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Button from "@/app/_components/ui/Button"; 4 | import MyLink from "@/app/_components/ui/MyLink"; 5 | import { useTranslate } from "@/app/_hooks/useTranslate"; 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | import logo from "@/public/logo.png"; 9 | interface ErrorComponentProps { 10 | error: Error; 11 | reset: () => void; 12 | } 13 | 14 | export default function Error({ error, reset }: ErrorComponentProps) { 15 | const { t } = useTranslate(); 16 | return ( 17 |
    18 | App Logo 19 |

    Something went wrong

    20 |

    {error.message}

    21 | 22 |
    23 | 26 | 29 |
    30 |
    31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /app/_components/dashboard/GraphCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import Chart from "./Chart"; 3 | import { getTranslate } from "@/app/_utils/helpers"; 4 | import prisma from "@/app/_lib/db"; 5 | 6 | async function GraphCard() { 7 | const now = new Date(); 8 | const sevenDaysAge = new Date(); 9 | 10 | sevenDaysAge.setDate(now.getDate() - 7); 11 | 12 | const orders = await prisma.order.findMany({ 13 | where: { 14 | createdAt: { 15 | gte: sevenDaysAge, 16 | }, 17 | }, 18 | select: { 19 | amount: true, 20 | createdAt: true, 21 | }, 22 | orderBy: { createdAt: "asc" }, 23 | }); 24 | 25 | const result = orders.map((item) => ({ 26 | date: new Intl.DateTimeFormat("en-US").format(item.createdAt), 27 | revenue: item.amount / 100, 28 | })); 29 | 30 | const { t } = await getTranslate(); 31 | 32 | return ( 33 |
    34 |

    {t("Transitions")}

    35 |

    {t("Transitions label")}

    36 | 37 | 38 |
    39 | ); 40 | } 41 | 42 | export default GraphCard; 43 | -------------------------------------------------------------------------------- /app/_components/marketking/IncAndDecProductItem.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useFormStatus } from "react-dom"; 3 | import Button from "../ui/Button"; 4 | import { increaseProductItem } from "@/app/_actions/increaseProductItem"; 5 | import { decreaseProductItem } from "@/app/_actions/decreaseProductItem"; 6 | 7 | function IncAndDecProductItem({ 8 | quantity, 9 | itemId, 10 | userId, 11 | }: { 12 | quantity: number; 13 | itemId: string; 14 | userId: string; 15 | }) { 16 | return ( 17 |
    18 |
    19 | + 20 |
    21 | {quantity} 22 |
    23 | - 24 |
    25 |
    26 | ); 27 | } 28 | 29 | export default IncAndDecProductItem; 30 | 31 | function SubmitButton({ children }: { children: React.ReactNode }) { 32 | const { pending } = useFormStatus(); 33 | 34 | return ( 35 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/_actions/createBanner.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import prisma from "../_lib/db"; 4 | import { getTranslate } from "../_utils/helpers"; 5 | import { BannerErrors } from "../_utils/types"; 6 | import { revalidatePath } from "next/cache"; 7 | 8 | export async function createBanner(_: any, formData: FormData) { 9 | const { isArabic } = await getTranslate(); 10 | const banner = formData.get("banner") as string; 11 | let image = formData.get("image") as string; 12 | 13 | let errors: BannerErrors = {}; 14 | 15 | if (!banner || banner.trim().length === 0) { 16 | errors.banner = "banner error message"; 17 | } 18 | 19 | if (!image || image.trim().length === 0) { 20 | errors.image = "image error message"; 21 | } 22 | 23 | if (Object.keys(errors).length > 0) { 24 | return errors; 25 | } 26 | 27 | try { 28 | await prisma.banner.create({ 29 | data: { 30 | title: banner, 31 | imageString: image, 32 | }, 33 | }); 34 | if (isArabic) { 35 | revalidatePath("/ar/dashboard", "layout"); 36 | } else { 37 | revalidatePath("/en/dashboard", "layout"); 38 | } 39 | 40 | return { success: true }; 41 | } catch { 42 | return { success: false }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/_components/ui/LanguageSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 | 6 | export default function LanguageSwitcher() { 7 | const router = useRouter(); 8 | const pathname = usePathname(); 9 | const searchParams = useSearchParams(); 10 | const { t, isArabic } = useTranslate(); 11 | const changeLocale = (locale: string) => { 12 | const segments = pathname.split("/"); 13 | segments[1] = locale; 14 | const newPathname = segments.join("/"); 15 | const queryString = searchParams.toString(); 16 | const url = queryString ? `${newPathname}?${queryString}` : newPathname; 17 | router.push(url); 18 | }; 19 | 20 | return ( 21 | <> 22 | {isArabic ? ( 23 | 29 | ) : ( 30 | 36 | )} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /app/_components/marketking/RelatedProducts.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import { getUser } from "@/app/_utils/getUser"; 3 | import { getTranslate } from "@/app/_utils/helpers"; 4 | import { Category } from "@prisma/client"; 5 | import React from "react"; 6 | import Marquee from "../ui/Marquee"; 7 | 8 | async function RelatedProducts({ type }: { type: Category }) { 9 | const items = await prisma.product.findMany({ 10 | where: { category: type ?? "laptops" }, 11 | select: { 12 | id: true, 13 | name: true, 14 | images: true, 15 | price: true, 16 | discount: true, 17 | }, 18 | orderBy: { 19 | createdAt: "desc", 20 | }, 21 | take: 10, 22 | }); 23 | const user = await getUser(); 24 | const userDetails = await prisma.user.findUnique({ 25 | where: { 26 | id: user?.id ?? "", 27 | }, 28 | include: { 29 | favoriteProducts: { select: { productId: true } }, 30 | }, 31 | }); 32 | 33 | const { t } = await getTranslate(); 34 | return ( 35 |
    36 |

    {t("Related Products")}

    37 | 38 | 39 | ); 40 | } 41 | 42 | export default RelatedProducts; 43 | -------------------------------------------------------------------------------- /app/_components/ui/IconButton.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | children: React.ReactNode; 3 | className?: string; 4 | isClient?: boolean; 5 | isClickable?: boolean; 6 | onClick?: () => void; 7 | } 8 | const IconButton = ({ 9 | children, 10 | onClick, 11 | className, 12 | isClient = false, 13 | isClickable = true, 14 | }: Props) => { 15 | return ( 16 | <> 17 | {isClient ? ( 18 | 27 | {children} 28 | 29 | ) : ( 30 | 39 | {children} 40 | 41 | )} 42 | 43 | ); 44 | }; 45 | 46 | export default IconButton; 47 | -------------------------------------------------------------------------------- /app/_components/marketking/FeaturedProducts.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import { getTranslate } from "@/app/_utils/helpers"; 3 | import { getUser } from "@/app/_utils/getUser"; 4 | import MarqueeProducts from "../ui/Marquee"; 5 | 6 | const getFeaturedProducts = async () => { 7 | const items = await prisma.product.findMany({ 8 | where: { isFeatured: true }, 9 | select: { 10 | id: true, 11 | name: true, 12 | images: true, 13 | price: true, 14 | discount: true, 15 | }, 16 | orderBy: { 17 | createdAt: "desc", 18 | }, 19 | }); 20 | 21 | return items; 22 | }; 23 | 24 | async function FeaturedProducts() { 25 | const items = await getFeaturedProducts(); 26 | const user = await getUser(); 27 | const userDetails = await prisma.user.findUnique({ 28 | where: { 29 | id: user?.id ?? "", 30 | }, 31 | include: { 32 | favoriteProducts: { select: { productId: true } }, 33 | }, 34 | }); 35 | 36 | const { t } = await getTranslate(); 37 | return ( 38 |
    39 |

    {t("Featured Items")}

    40 | 41 |
    42 | ); 43 | } 44 | 45 | export default FeaturedProducts; 46 | -------------------------------------------------------------------------------- /app/_components/marketking/LoginFirst.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Modal from "../ui/Modal"; 3 | 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import Button from "../ui/Button"; 6 | import MyLink from "../ui/MyLink"; 7 | 8 | function LoginFirst({ children }: { children: React.ReactNode }) { 9 | const { t } = useTranslate(); 10 | return ( 11 | 12 | {children} 13 | 14 | {() => ( 15 | <> 16 |

    {t("login message")}

    17 | 18 |
    19 | 23 | {t("Login")} 24 | 25 | 26 | 30 | {t("Sign Up")} 31 | 32 |
    33 | 34 | )} 35 |
    36 |
    37 | ); 38 | } 39 | 40 | export default LoginFirst; 41 | -------------------------------------------------------------------------------- /app/_components/dashboard/Chart.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | ResponsiveContainer, 4 | LineChart, 5 | CartesianGrid, 6 | XAxis, 7 | YAxis, 8 | Tooltip, 9 | Legend, 10 | Line, 11 | } from "recharts"; 12 | const aggregateData = (data: any) => { 13 | const aggregated = data.reduce((acc: any, curr: any) => { 14 | if (acc[curr.date]) { 15 | acc[curr.date] += curr.revenue; 16 | } else { 17 | acc[curr.date] = curr.revenue; 18 | } 19 | return acc; 20 | }, {}); 21 | 22 | return Object.keys(aggregated).map((date) => ({ 23 | date, 24 | revenue: aggregated[date], 25 | })); 26 | }; 27 | 28 | function Chart({ result }: { result: { date: string; revenue: number }[] }) { 29 | const processedData = aggregateData(result); 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export default Chart; 50 | -------------------------------------------------------------------------------- /app/_components/ui/ChangeLanguage.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DropdownMenu, 3 | DropdownMenuContent, 4 | DropdownMenuItem, 5 | DropdownMenuLabel, 6 | DropdownMenuSeparator, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import LanguageSwitcher from "../ui/LanguageSwitcher"; 10 | import { useTranslate } from "@/app/_hooks/useTranslate"; 11 | import { TbWorld } from "react-icons/tb"; 12 | import IconButton from "./IconButton"; 13 | function ChangeLanguage({ isDashboard = false }: { isDashboard?: boolean }) { 14 | const { t, isArabic } = useTranslate(); 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {t("Choose Your Language")} 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | export default ChangeLanguage; 38 | -------------------------------------------------------------------------------- /app/_actions/editProfile.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | import { getTranslate } from "../_utils/helpers"; 6 | import { EditProfileErrors } from "../_utils/types"; 7 | import { redirect } from "next/navigation"; 8 | 9 | export async function editProfile(_: any, formData: FormData) { 10 | const { isArabic } = await getTranslate(); 11 | const firstName = formData.get("firstName") as string; 12 | const lastName = formData.get("lastName") as string; 13 | const userId = formData.get("userId") as string; 14 | const image = formData.get("image") as string; 15 | let errors: EditProfileErrors = {}; 16 | 17 | if (!firstName || firstName.trim().length < 3) { 18 | errors.firstName = "firstName error message"; 19 | } 20 | if (!lastName || lastName.trim().length < 3) { 21 | errors.lastName = "lastName error message"; 22 | } 23 | if (!image || image.trim().length === 0) { 24 | errors.lastName = "image error message"; 25 | } 26 | 27 | if (Object.keys(errors).length > 0) { 28 | return errors; 29 | } 30 | 31 | await prisma.user.update({ 32 | where: { id: userId ?? "" }, 33 | data: { 34 | firstName, 35 | lastName, 36 | profileImage: image, 37 | }, 38 | }); 39 | 40 | revalidatePath("/", "layout"); 41 | 42 | redirect(isArabic ? "/ar" : "/en"); 43 | } 44 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/banner/page.tsx: -------------------------------------------------------------------------------- 1 | import BannerTable from "@/app/_components/dashboard/BannerTable"; 2 | import Button from "@/app/_components/ui/Button"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 6 | import { IoMdAddCircleOutline } from "react-icons/io"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("Banner")}`, 17 | }; 18 | } 19 | 20 | function BannerPage({ params: { locale } }: { params: { locale: string } }) { 21 | unstable_setRequestLocale(locale); 22 | const { t } = useTranslate(); 23 | 24 | return ( 25 |
    26 | 38 | 39 |
    40 | ); 41 | } 42 | 43 | export default BannerPage; 44 | -------------------------------------------------------------------------------- /app/_components/marketking/FavoriteProductsListSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function FavoriteProductsListSkeleton() { 4 | return ( 5 |
      6 | {Array.from({ length: 6 }, (_, index) => ( 7 |
    • 11 |
      12 | 13 |
      14 | 15 |

      16 |

      17 | 18 |
      19 |
      20 |
      21 |
      22 | 23 |
      24 |
    • 25 | ))} 26 |
    27 | ); 28 | } 29 | 30 | export default FavoriteProductsListSkeleton; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "market-king", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.16.2", 14 | "@radix-ui/react-avatar": "^1.1.0", 15 | "@radix-ui/react-dialog": "^1.1.1", 16 | "@radix-ui/react-dropdown-menu": "^2.1.1", 17 | "@radix-ui/react-slot": "^1.1.0", 18 | "@uploadthing/react": "^6.7.2", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.1", 21 | "embla-carousel-react": "^8.1.7", 22 | "framer-motion": "^11.2.13", 23 | "next": "14.2.4", 24 | "next-intl": "^3.15.5", 25 | "next-themes": "^0.3.0", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "react-fast-marquee": "^1.6.5", 29 | "react-hot-toast": "^2.4.1", 30 | "react-icons": "^5.2.1", 31 | "recharts": "^2.12.7", 32 | "sharp": "^0.33.4", 33 | "stripe": "^16.6.0", 34 | "tailwind-merge": "^2.4.0", 35 | "uploadthing": "^6.13.2" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20", 39 | "@types/react": "^18", 40 | "@types/react-dom": "^18", 41 | "eslint": "^8", 42 | "eslint-config-next": "14.2.4", 43 | "postcss": "^8", 44 | "prisma": "^5.16.2", 45 | "tailwindcss": "^3.4.1", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/_utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import { getLocale, getTranslations } from "next-intl/server"; 2 | 3 | export async function getTranslate() { 4 | const t = await getTranslations("Index"); 5 | const locale = await getLocale(); 6 | const isArabic = locale === "ar"; 7 | 8 | return { t, isArabic }; 9 | } 10 | 11 | export async function formatDate(date: Date) { 12 | const { isArabic } = await getTranslate(); 13 | return new Intl.DateTimeFormat(isArabic ? "ar-EG" : "en-US", { 14 | year: "numeric", 15 | month: "short", 16 | day: "numeric", 17 | hour: "numeric", 18 | minute: "numeric", 19 | hour12: true, 20 | }).format(date); 21 | } 22 | 23 | export function newPrice(price: number, discount: number) { 24 | return discount ? price - discount : price; 25 | } 26 | 27 | export function oldPrice(price: number, discount: number) { 28 | return discount ? price : null; 29 | } 30 | 31 | export const homeNavLinks = [ 32 | { label: "Home", href: "/" }, 33 | { label: "All Products", href: "/products/all" }, 34 | { label: "Phones", href: "/products/phones" }, 35 | { label: "Laptops", href: "/products/laptops" }, 36 | { label: "Watches", href: "/products/watches" }, 37 | ]; 38 | export const dashboardNavLinks = [ 39 | { label: "Dashboard", href: "/dashboard" }, 40 | { label: "Orders", href: "/dashboard/orders" }, 41 | { label: "Products", href: "/dashboard/products" }, 42 | { label: "Banner Picture", href: "/dashboard/banner" }, 43 | ]; 44 | -------------------------------------------------------------------------------- /app/api/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import { stripe } from "@/app/_lib/stripe"; 3 | import { headers } from "next/headers"; 4 | 5 | export async function POST(req: Request) { 6 | const body = await req.text(); 7 | 8 | const signature = headers().get("Stripe-Signature") as string; 9 | 10 | let event; 11 | 12 | try { 13 | event = stripe.webhooks.constructEvent( 14 | body, 15 | signature, 16 | process.env.STRIPE_SECRET_WEBHOOK as string 17 | ); 18 | } catch (error: unknown) { 19 | return new Response("Webhook Error", { status: 400 }); 20 | } 21 | 22 | switch (event.type) { 23 | case "checkout.session.completed": { 24 | const session = event.data.object; 25 | 26 | await prisma.order.create({ 27 | data: { 28 | amount: session.amount_total as number, 29 | status: session.status as string, 30 | userId: session.metadata?.userId, 31 | }, 32 | }); 33 | 34 | const userCart = await prisma.cart.findUnique({ 35 | where: { 36 | userId: session.metadata?.userId ?? "", 37 | }, 38 | }); 39 | 40 | await prisma.cartItem.deleteMany({ 41 | where: { 42 | cartId: userCart?.id ?? "", 43 | }, 44 | }); 45 | 46 | break; 47 | } 48 | default: { 49 | console.log("Unhandled Event"); 50 | } 51 | } 52 | 53 | return new Response(null, { status: 200 }); 54 | } 55 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import User from "@/app/_components/ui/User"; 2 | import Header from "@/app/_components/header/Header"; 3 | import NavLinks from "@/app/_components/header/NavLinks"; 4 | import ChangeLanguage from "@/app/_components/ui/ChangeLanguage"; 5 | import Menu from "@/app/_components/ui/Menu"; 6 | import { unstable_setRequestLocale } from "next-intl/server"; 7 | import { getUser } from "@/app/_utils/getUser"; 8 | import { redirect } from "next/navigation"; 9 | import { getTranslate } from "@/app/_utils/helpers"; 10 | import ToggleTheme from "@/app/_components/ui/ToggleTheme"; 11 | 12 | export default async function DashBoardLayout({ 13 | children, 14 | params: { locale }, 15 | }: { 16 | children: React.ReactNode; 17 | params: { locale: string }; 18 | }) { 19 | unstable_setRequestLocale(locale); 20 | 21 | const user = await getUser(); 22 | const { isArabic } = await getTranslate(); 23 | if (user?.email !== "faresahmed00001111@gmail.com") { 24 | redirect(isArabic ? "/ar" : "/en"); 25 | } 26 | return ( 27 |
    28 |
    29 |
    30 | 31 | 32 |
    33 | 34 |
    35 | 36 | 37 | 38 | 39 |
    40 |
    41 | 42 | {children} 43 |
    44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/_hooks/useElementsForm.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import { ProductErrors } from "../_utils/types"; 5 | 6 | export default function useElementsForm(state: ProductErrors | undefined) { 7 | const productEl = useRef(null); 8 | const descriptionEl = useRef(null); 9 | const priceEl = useRef(null); 10 | const discountEl = useRef(null); 11 | const categoryEl = useRef(null); 12 | const statusEl = useRef(null); 13 | 14 | useEffect(() => { 15 | if (categoryEl.current && state?.category) { 16 | return categoryEl.current.focus(); 17 | } 18 | if (productEl?.current && state?.product) { 19 | return productEl.current.focus(); 20 | } 21 | 22 | if (descriptionEl.current && state?.description) { 23 | return descriptionEl.current.focus(); 24 | } 25 | if (priceEl.current && state?.price) { 26 | return priceEl.current.focus(); 27 | } 28 | if (discountEl.current && state?.discount) { 29 | return discountEl.current.focus(); 30 | } 31 | if (statusEl.current && state?.status) { 32 | setTimeout(() => { 33 | return statusEl.current?.focus(); 34 | }, 0); 35 | } 36 | }, [state, productEl, descriptionEl, priceEl, statusEl, categoryEl]); 37 | 38 | return { 39 | productEl, 40 | descriptionEl, 41 | priceEl, 42 | statusEl, 43 | categoryEl, 44 | discountEl, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /app/_components/marketking/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslate } from "@/app/_utils/helpers"; 2 | import Button from "../ui/Button"; 3 | 4 | import { getUser } from "@/app/_utils/getUser"; 5 | import ChangeLanguage from "../ui/ChangeLanguage"; 6 | import MyLink from "../ui/MyLink"; 7 | import ToggleTheme from "../ui/ToggleTheme"; 8 | import User from "../ui/User"; 9 | import ShoppingCart from "./ShoppingCart"; 10 | import WhishList from "./WhishList"; 11 | 12 | async function Auth() { 13 | const { t } = await getTranslate(); 14 | const user: any = await getUser(); 15 | 16 | 17 | return ( 18 |
    19 | 20 | 21 | {user ? ( 22 | <> 23 | 24 | 25 | 26 | 27 | ) : ( 28 |
    29 | 37 | 38 | 46 |
    47 | )} 48 |
    49 | ); 50 | } 51 | export default Auth; 52 | -------------------------------------------------------------------------------- /app/_components/dashboard/PaginationOrderTable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { useRouter, usePathname } from "next/navigation"; 5 | 6 | function OrderTablePagination({ 7 | currentPage, 8 | totalPages, 9 | }: { 10 | currentPage: number; 11 | totalPages: number; 12 | }) { 13 | const router = useRouter(); 14 | const pathname = usePathname(); 15 | const { t } = useTranslate(); 16 | const handlePageChange = (newPage: number) => { 17 | router.push(`${pathname}?page=${newPage}`); 18 | }; 19 | 20 | return ( 21 |
    22 | 31 | 32 | {t("Page")} {currentPage} {t("of")} {totalPages} 33 | 34 | 45 |
    46 | ); 47 | } 48 | 49 | export default OrderTablePagination; 50 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/payment/cancel/page.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@/app/_components/ui/Button"; 2 | import MyLink from "@/app/_components/ui/MyLink"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { MdCancel } from "react-icons/md"; 5 | 6 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("Cancel Payment")}`, 17 | }; 18 | } 19 | 20 | function CancelPaymentPage({ 21 | params: { locale }, 22 | }: { 23 | params: { locale: string }; 24 | }) { 25 | unstable_setRequestLocale(locale); 26 | const { t } = useTranslate(); 27 | 28 | return ( 29 |
    30 |
    31 | 32 | 33 |

    {t("Payment Cancelled")}

    34 | 35 |

    36 | {t("payment cancel message")}{" "} 37 |

    38 | 39 | 42 |
    43 |
    44 | ); 45 | } 46 | 47 | export default CancelPaymentPage; 48 | -------------------------------------------------------------------------------- /app/_components/ui/ModalImage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { AnimatePresence, motion } from "framer-motion"; 3 | import { useState } from "react"; 4 | 5 | function ModalImage({ 6 | image, 7 | className, 8 | modalId, 9 | isInTable = false, 10 | }: { 11 | image: React.ReactElement; 12 | className: string; 13 | modalId: string; 14 | isInTable?: boolean; 15 | }) { 16 | const [open, setOpen] = useState(""); 17 | 18 | return ( 19 | <> 20 | {isInTable ? ( 21 | setOpen(modalId)} 25 | layoutId={modalId} 26 | > 27 | {image} 28 | 29 | ) : ( 30 | setOpen(modalId)} 34 | layoutId={modalId} 35 | > 36 | {image} 37 | 38 | )} 39 | 40 | {open && ( 41 | setOpen("")} 46 | > 47 |
    {image}
    48 |
    49 | )} 50 |
    51 | 52 | ); 53 | } 54 | 55 | export default ModalImage; 56 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/payment/success/page.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@/app/_components/ui/Button"; 2 | import MyLink from "@/app/_components/ui/MyLink"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { BsFillCartCheckFill } from "react-icons/bs"; 5 | 6 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("Success Payment")}`, 17 | }; 18 | } 19 | 20 | function CancelPaymentPage({ 21 | params: { locale }, 22 | }: { 23 | params: { locale: string }; 24 | }) { 25 | unstable_setRequestLocale(locale); 26 | const { t } = useTranslate(); 27 | 28 | return ( 29 |
    30 |
    31 | 32 | 33 |

    {t("Payment Success")}

    34 | 35 |

    36 | {t("payment Success message")}{" "} 37 |

    38 | 39 | 42 |
    43 |
    44 | ); 45 | } 46 | 47 | export default CancelPaymentPage; 48 | -------------------------------------------------------------------------------- /app/_actions/loginAccount.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | import { LoginErrors } from "../_utils/types"; 6 | import { cookies } from "next/headers"; 7 | import { redirect } from "next/navigation"; 8 | import { getTranslate } from "../_utils/helpers"; 9 | 10 | export async function loginAccount(_: any, formData: FormData) { 11 | const { isArabic } = await getTranslate(); 12 | 13 | const email = formData.get("email") as string; 14 | const password = formData.get("password") as string; 15 | 16 | let errors: LoginErrors = {}; 17 | 18 | if (!email || email.trim().length === 0) { 19 | errors.email = "email error message"; 20 | } 21 | 22 | if (!password || password.trim().length === 0) { 23 | errors.password = "password error message"; 24 | } 25 | 26 | if (Object.keys(errors).length > 0) { 27 | return errors; 28 | } 29 | 30 | const existEmail = await prisma.user.findUnique({ 31 | where: { email, password }, 32 | }); 33 | 34 | if (existEmail) { 35 | revalidatePath("/"); 36 | 37 | cookies().set( 38 | "user_info", 39 | JSON.stringify({ 40 | email: existEmail.email, 41 | userId: existEmail.id, 42 | }), 43 | { 44 | httpOnly: true, 45 | secure: process.env.NODE_ENV === "production", 46 | sameSite: "lax", 47 | maxAge: 60 * 60 * 24 * 7, // 1 week 48 | path: "/", 49 | } 50 | ); 51 | 52 | if (isArabic) { 53 | redirect("/ar"); 54 | } else { 55 | redirect("/en"); 56 | } 57 | } 58 | 59 | return { success: false }; 60 | } 61 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /app/_components/ui/Switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { ProductType } from "@/app/_utils/types"; 5 | import React from "react"; 6 | import toast from "react-hot-toast"; 7 | 8 | interface SwitchProps { 9 | checked: boolean; 10 | onChange: (checked: boolean) => void; 11 | disabled?: boolean; 12 | product?: ProductType; 13 | } 14 | 15 | const Switch: React.FC = ({ 16 | checked, 17 | onChange, 18 | disabled = false, 19 | product, 20 | }) => { 21 | const { t } = useTranslate(); 22 | function handleChange(e: React.ChangeEvent) { 23 | onChange(e.target.checked); 24 | if (e.target.checked) { 25 | toast.success(t("Featured Mode")); 26 | } else { 27 | toast.error(t("Featured Mode disabled")); 28 | } 29 | } 30 | 31 | return ( 32 |
    33 | 43 | 52 |
    53 | ); 54 | }; 55 | 56 | export default Switch; 57 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/banner/create/page.tsx: -------------------------------------------------------------------------------- 1 | import CreateBanner from "@/app/_components/dashboard/CreateBanner"; 2 | import IconButton from "@/app/_components/ui/IconButton"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 6 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("Create Banner")}`, 17 | }; 18 | } 19 | 20 | function BannerCreatePage({ 21 | params: { locale }, 22 | }: { 23 | params: { locale: string }; 24 | }) { 25 | unstable_setRequestLocale(locale); 26 | const { t, isArabic } = useTranslate(); 27 | 28 | return ( 29 |
    30 |
    31 | 32 | 33 | {isArabic ? : } 34 | 35 | 36 |

    {t("New Banner")}

    37 |
    38 |
    39 |

    {t("Banner Details")}

    40 |

    41 | {t("Banner Details title")} 42 |

    43 | 44 | 45 |
    46 |
    47 | ); 48 | } 49 | 50 | export default BannerCreatePage; 51 | -------------------------------------------------------------------------------- /app/_actions/createAndUpdateCart.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { Product } from "@prisma/client"; 4 | import prisma from "../_lib/db"; 5 | import { revalidatePath } from "next/cache"; 6 | import { getTranslate } from "../_utils/helpers"; 7 | 8 | export async function createAndUpdateCart({ 9 | userId, 10 | product, 11 | quantity, 12 | }: { 13 | userId: string; 14 | product: Product; 15 | quantity: number; 16 | }) { 17 | // const { isArabic } = await getTranslate(); 18 | let cart = await prisma.cart.findUnique({ 19 | where: { userId: userId }, 20 | include: { items: true }, 21 | }); 22 | 23 | if (!cart) { 24 | cart = await prisma.cart.create({ 25 | data: { userId: userId }, 26 | include: { items: true }, 27 | }); 28 | } 29 | 30 | const existingItem = cart.items.find((item) => item.productId === product.id); 31 | 32 | if (existingItem) { 33 | // await prisma.cartItem.update({ 34 | // where: { id: existingItem.id }, 35 | // data: { quantity: existingItem.quantity + quantity }, 36 | // }); 37 | return { isExist: "exist", name: existingItem.name }; 38 | } else { 39 | const createdCartItem = await prisma.cartItem.create({ 40 | data: { 41 | cartId: cart.id, 42 | productId: product.id, 43 | quantity: quantity, 44 | price: product.price, 45 | discount: product.discount, 46 | imageString: product.images[0], 47 | name: product.name, 48 | }, 49 | }); 50 | revalidatePath("/", "layout"); 51 | 52 | return { 53 | isExist: "created", 54 | name: createdCartItem.name, 55 | quantity: createdCartItem.quantity, 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/edit-profile/page.tsx: -------------------------------------------------------------------------------- 1 | import EditProfileForm from "@/app/_components/marketking/EditProfileForm"; 2 | import IconButton from "@/app/_components/ui/IconButton"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import { getUser } from "@/app/_utils/getUser"; 5 | import { getTranslate } from "@/app/_utils/helpers"; 6 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 7 | import { redirect } from "next/navigation"; 8 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 9 | 10 | export async function generateMetadata({ 11 | params: { locale }, 12 | }: { 13 | params: { locale: string }; 14 | }) { 15 | const t = await getTranslations({ locale, namespace: "metadata" }); 16 | 17 | return { 18 | title: `${t("Edit Profile")}`, 19 | }; 20 | } 21 | async function EditProfilePage({ 22 | params: { locale }, 23 | }: { 24 | params: { locale: string }; 25 | }) { 26 | unstable_setRequestLocale(locale); 27 | 28 | const user: any = await getUser(); 29 | const { t, isArabic } = await getTranslate(); 30 | 31 | if (!user) redirect(isArabic ? "/ar" : "/en"); 32 | return ( 33 |
    34 |
    35 | 36 | 37 | {isArabic ? : } 38 | 39 | 40 |

    {t("Edit Profile")}

    41 |
    42 | 43 |
    44 | 45 |
    46 |
    47 | ); 48 | } 49 | 50 | export default EditProfilePage; 51 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/product/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import ProductInfoLayout from "@/app/_components/marketking/ProductInfoLayout"; 2 | import ProductInfoSkeleton from "@/app/_components/marketking/ProductInfoSkeleton"; 3 | import IconButton from "@/app/_components/ui/IconButton"; 4 | import MyLink from "@/app/_components/ui/MyLink"; 5 | import { useTranslate } from "@/app/_hooks/useTranslate"; 6 | 7 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 8 | import { Suspense } from "react"; 9 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 10 | 11 | export async function generateMetadata({ 12 | params: { locale }, 13 | }: { 14 | params: { locale: string }; 15 | }) { 16 | const t = await getTranslations({ locale, namespace: "metadata" }); 17 | 18 | return { 19 | title: `${t("Product Details")}`, 20 | }; 21 | } 22 | 23 | function ProductDetailsPage({ 24 | params: { id, locale }, 25 | }: { 26 | params: { id: string; locale: string }; 27 | }) { 28 | unstable_setRequestLocale(locale); 29 | 30 | const { t, isArabic } = useTranslate(); 31 | 32 | return ( 33 |
    34 |
    35 | 36 | 37 | {isArabic ? : } 38 | 39 | 40 |

    {t("Product Details")}

    41 |
    42 | 43 | }> 44 | 45 | 46 |
    47 | ); 48 | } 49 | 50 | export default ProductDetailsPage; 51 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/products/page.tsx: -------------------------------------------------------------------------------- 1 | import OrderTable from "@/app/_components/dashboard/OrderTable"; 2 | import Button from "@/app/_components/ui/Button"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 6 | import { IoMdAddCircleOutline } from "react-icons/io"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("Products")}`, 17 | }; 18 | } 19 | 20 | function ProductsPage({ 21 | params: { locale }, 22 | searchParams, 23 | }: { 24 | params: { locale: string }; 25 | searchParams: { page: string }; 26 | }) { 27 | unstable_setRequestLocale(locale); 28 | 29 | const { t } = useTranslate(); 30 | return ( 31 |
    32 | 44 |
    45 |

    {t("Products")}

    46 |

    {t("Products title")}

    47 | 48 |
    49 |
    50 | ); 51 | } 52 | 53 | export default ProductsPage; 54 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/products/create/page.tsx: -------------------------------------------------------------------------------- 1 | import CreateAndEditProductForm from "@/app/_components/dashboard/CreateAndEditProductForm"; 2 | import IconButton from "@/app/_components/ui/IconButton"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 6 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 7 | 8 | export async function generateMetadata({ 9 | params: { locale }, 10 | }: { 11 | params: { locale: string }; 12 | }) { 13 | const t = await getTranslations({ locale, namespace: "metadata" }); 14 | 15 | return { 16 | title: `${t("New Product")}`, 17 | }; 18 | } 19 | 20 | function ProductCreateRoute({ 21 | params: { locale }, 22 | }: { 23 | params: { locale: string }; 24 | }) { 25 | unstable_setRequestLocale(locale); 26 | 27 | const { t, isArabic } = useTranslate(); 28 | return ( 29 |
    30 |
    31 | 32 | 33 | {isArabic ? : } 34 | 35 | 36 |

    {t("New Product")}

    37 |
    38 | 39 |
    40 |

    {t("Banner Details")}

    41 |

    42 | {t("Banner Details title")} 43 |

    44 | 45 | 46 |
    47 |
    48 | ); 49 | } 50 | 51 | export default ProductCreateRoute; 52 | -------------------------------------------------------------------------------- /app/_components/ui/Selectors.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { ProductType } from "@/app/_utils/types"; 4 | import { useState } from "react"; 5 | 6 | function Selectors({ 7 | chooses, 8 | categoryEl, 9 | product, 10 | }: { 11 | chooses: string[]; 12 | categoryEl: React.RefObject; 13 | product?: ProductType; 14 | }) { 15 | function defaultActive() { 16 | if (product?.category === "phones") { 17 | return 0; 18 | } 19 | if (product?.category === "watches") { 20 | return 1; 21 | } 22 | if (product?.category === "laptops") { 23 | return 2; 24 | } 25 | return undefined; 26 | } 27 | const { t } = useTranslate(); 28 | 29 | const [isActive, setIsActive] = useState(defaultActive); 30 | return ( 31 |
      32 | {chooses.map((choose, index) => ( 33 |
    • setIsActive(index)} 41 | > 42 | 50 | {t(choose)} 51 |
    • 52 | ))} 53 |
    54 | ); 55 | } 56 | 57 | export default Selectors; 58 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/favorite-products/page.tsx: -------------------------------------------------------------------------------- 1 | import FavoriteProductsList from "@/app/_components/marketking/FavoriteProductsList"; 2 | import FavoriteProductsListSkeleton from "@/app/_components/marketking/FavoriteProductsListSkeleton"; 3 | import IconButton from "@/app/_components/ui/IconButton"; 4 | import MyLink from "@/app/_components/ui/MyLink"; 5 | import { useTranslate } from "@/app/_hooks/useTranslate"; 6 | import { getUser } from "@/app/_utils/getUser"; 7 | import { getTranslate } from "@/app/_utils/helpers"; 8 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 9 | import { redirect } from "next/navigation"; 10 | import { Suspense } from "react"; 11 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 12 | 13 | export async function generateMetadata({ 14 | params: { locale }, 15 | }: { 16 | params: { locale: string }; 17 | }) { 18 | const t = await getTranslations({ locale, namespace: "metadata" }); 19 | 20 | return { 21 | title: `${t("Favorite Products")}`, 22 | }; 23 | } 24 | function FavoriteProductsPage({ 25 | params: { locale }, 26 | }: { 27 | params: { locale: string }; 28 | }) { 29 | unstable_setRequestLocale(locale); 30 | const { t, isArabic } = useTranslate(); 31 | 32 | return ( 33 |
    34 |
    35 | 36 | 37 | {isArabic ? : } 38 | 39 | 40 |

    {t("Favorite Products")}

    41 |
    42 | 43 | }> 44 | 45 | 46 |
    47 | ); 48 | } 49 | 50 | export default FavoriteProductsPage; 51 | -------------------------------------------------------------------------------- /app/_components/dashboard/StatusList.tsx: -------------------------------------------------------------------------------- 1 | import { AiFillDollarCircle } from "react-icons/ai"; 2 | import { FcSalesPerformance } from "react-icons/fc"; 3 | import StatusItem from "@/app/_components/dashboard/StatusItem"; 4 | import { FaUsers } from "react-icons/fa"; 5 | import prisma from "@/app/_lib/db"; 6 | import { FaCartArrowDown } from "react-icons/fa6"; 7 | 8 | async function StatusList() { 9 | const [users, products, orders] = await Promise.all([ 10 | prisma.user.findMany({ 11 | select: { 12 | id: true, 13 | }, 14 | }), 15 | prisma.product.findMany({ 16 | select: { 17 | id: true, 18 | }, 19 | }), 20 | 21 | prisma.order.findMany({ 22 | select: { 23 | amount: true, 24 | }, 25 | }), 26 | ]); 27 | 28 | const totalAmount = orders.reduce((acc, cur) => acc + cur.amount, 0); 29 | 30 | return ( 31 |
      32 | } 34 | title="Total Revenue" 35 | label="basedOnChanges" 36 | statusValue={`$${new Intl.NumberFormat("en-US").format( 37 | totalAmount / 100 38 | )}`} 39 | /> 40 | } 42 | title="Total Sales" 43 | label="Total Sales label" 44 | statusValue={`+${orders.length}`} 45 | /> 46 | } 48 | title="Total Products" 49 | label="Total Products label" 50 | statusValue={`${products.length}`} 51 | /> 52 | } 54 | title="Total Users" 55 | label="Total Users label" 56 | statusValue={`+${users.length}`} 57 | /> 58 |
    59 | ); 60 | } 61 | 62 | export default StatusList; 63 | -------------------------------------------------------------------------------- /app/_utils/types.ts: -------------------------------------------------------------------------------- 1 | import { Category, ProductStatus, User } from "@prisma/client"; 2 | 3 | export interface ProductType { 4 | name: string; 5 | description: string; 6 | price: number; 7 | images: string[]; 8 | status: ProductStatus; 9 | category: Category; 10 | isFeatured: boolean; 11 | id: string; 12 | discount: number; 13 | } 14 | 15 | export interface ProductErrors { 16 | product?: string; 17 | description?: string; 18 | price?: string; 19 | featured?: boolean; 20 | status?: string; 21 | discount?: string; 22 | image?: string; 23 | category?: string; 24 | success?: boolean; 25 | } 26 | 27 | export interface BannerErrors { 28 | banner?: string; 29 | image?: string; 30 | success?: boolean; 31 | } 32 | 33 | export interface SignUpErrors { 34 | firstName?: string; 35 | lastName?: string; 36 | email?: string; 37 | password?: string; 38 | confirmPassword?: string; 39 | success?: boolean; 40 | 41 | storeEmail?: string; 42 | storePassword?: string; 43 | storeUserId?: string; 44 | } 45 | 46 | export interface EditProfileErrors { 47 | firstName?: string; 48 | lastName?: string; 49 | image?: string; 50 | success?: boolean; 51 | } 52 | export interface LoginErrors { 53 | email?: string; 54 | password?: string; 55 | success?: boolean; 56 | 57 | storeEmail?: string; 58 | storePassword?: string; 59 | storeUserId?: string; 60 | } 61 | 62 | export type IUserIncludeFavorites = { 63 | id: string; 64 | email: string; 65 | password: string; 66 | firstName: string; 67 | lastName: string; 68 | profileImage: string; 69 | createAt: Date; 70 | favoriteProducts: { 71 | product: { 72 | id: string; 73 | name: string; 74 | description: string; 75 | status: ProductStatus; 76 | price: number; 77 | discount: number; 78 | images: string[]; 79 | category: Category; 80 | isFeatured: boolean; 81 | createdAt: Date; 82 | }; 83 | }[]; 84 | } | null; 85 | -------------------------------------------------------------------------------- /app/_actions/checkout.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { redirect } from "next/navigation"; 3 | import { getUser } from "../_utils/getUser"; 4 | import prisma from "../_lib/db"; 5 | import { stripe } from "../_lib/stripe"; 6 | import Stripe from "stripe"; 7 | import { getTranslate } from "../_utils/helpers"; 8 | 9 | export async function checkout() { 10 | const user = await getUser(); 11 | const { isArabic } = await getTranslate(); 12 | if (!user?.id) { 13 | return redirect("/"); 14 | } 15 | 16 | let cart = await prisma.cart.findUnique({ 17 | where: { userId: user.id }, 18 | include: { 19 | items: true, 20 | }, 21 | }); 22 | 23 | if (cart && cart.items) { 24 | const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = 25 | cart.items.map((item) => ({ 26 | price_data: { 27 | currency: "usd", 28 | unit_amount: (item.price - item.discount) * 100, 29 | product_data: { 30 | name: item.name, 31 | images: [item.imageString], 32 | }, 33 | }, 34 | quantity: item.quantity, 35 | })); 36 | const session = await stripe.checkout.sessions.create({ 37 | mode: "payment", 38 | line_items: lineItems, 39 | success_url: 40 | process.env.NODE_ENV === "development" 41 | ? `http://localhost:3000/${isArabic ? "ar" : "en"}/payment/success` 42 | : `https://market-king-ecommerce.vercel.app/${ 43 | isArabic ? "ar" : "en" 44 | }/payment/success`, 45 | cancel_url: 46 | process.env.NODE_ENV === "development" 47 | ? `http://localhost:3000/${isArabic ? "ar" : "en"}/payment/cancel` 48 | : `https://market-king-ecommerce.vercel.app/${ 49 | isArabic ? "ar" : "en" 50 | }/payment/cancel`, 51 | metadata: { 52 | userId: user.id, 53 | }, 54 | locale: "auto", 55 | }); 56 | 57 | return redirect(session.url as string); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/products/all/page.tsx: -------------------------------------------------------------------------------- 1 | import AllProducts from "@/app/_components/marketking/AllProducts"; 2 | import FavoriteProductsListSkeleton from "@/app/_components/marketking/FavoriteProductsListSkeleton"; 3 | import FilterSheet from "@/app/_components/marketking/FilterSheet"; 4 | import IconButton from "@/app/_components/ui/IconButton"; 5 | import MyLink from "@/app/_components/ui/MyLink"; 6 | import { useTranslate } from "@/app/_hooks/useTranslate"; 7 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 8 | import { Suspense } from "react"; 9 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 10 | 11 | export async function generateMetadata({ 12 | params: { locale }, 13 | }: { 14 | params: { locale: string }; 15 | }) { 16 | const t = await getTranslations({ locale, namespace: "metadata" }); 17 | 18 | return { 19 | title: `${t("All Products")}`, 20 | }; 21 | } 22 | function AllProductsPage({ 23 | params: { locale }, 24 | searchParams, 25 | }: { 26 | params: { locale: string }; 27 | searchParams: { 28 | "sort-price": string; 29 | "filter-price": string; 30 | }; 31 | }) { 32 | unstable_setRequestLocale(locale); 33 | const { t, isArabic } = useTranslate(); 34 | return ( 35 |
    36 |
    37 |
    38 | 39 | 40 | {isArabic ? : } 41 | 42 | 43 |

    {t("All Products")}

    44 |
    45 | 46 |
    47 | 48 | }> 49 | 50 | 51 |
    52 | ); 53 | } 54 | 55 | export default AllProductsPage; 56 | -------------------------------------------------------------------------------- /app/_components/marketking/CategoryPreviews.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import MyLink from "../ui/MyLink"; 3 | import Image from "next/image"; 4 | import { CATEGORIES } from "@/app/_utils/consistent"; 5 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 6 | 7 | function CategoryPreviews() { 8 | const { t, isArabic } = useTranslate(); 9 | 10 | return ( 11 |
    12 | {CATEGORIES.map(({ alt, href, span, src, title }) => ( 13 | 20 | {alt} 28 |
    29 |
    30 |

    {t(title)}

    31 |

    {t("Shop Now")}

    32 |
    33 | 34 | 41 | 42 | ))} 43 |
    44 | ); 45 | } 46 | 47 | export default CategoryPreviews; 48 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/products/laptops/page.tsx: -------------------------------------------------------------------------------- 1 | import AllProducts from "@/app/_components/marketking/AllProducts"; 2 | import FavoriteProductsListSkeleton from "@/app/_components/marketking/FavoriteProductsListSkeleton"; 3 | import FilterSheet from "@/app/_components/marketking/FilterSheet"; 4 | import Laptops from "@/app/_components/marketking/Laptops"; 5 | import IconButton from "@/app/_components/ui/IconButton"; 6 | import MyLink from "@/app/_components/ui/MyLink"; 7 | import { useTranslate } from "@/app/_hooks/useTranslate"; 8 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 9 | import { Suspense } from "react"; 10 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 11 | 12 | export async function generateMetadata({ 13 | params: { locale }, 14 | }: { 15 | params: { locale: string }; 16 | }) { 17 | const t = await getTranslations({ locale, namespace: "metadata" }); 18 | 19 | return { 20 | title: `${t("Laptops")}`, 21 | }; 22 | } 23 | function LaptopsPage({ 24 | params: { locale }, 25 | searchParams, 26 | }: { 27 | params: { locale: string }; 28 | searchParams: { 29 | "sort-price": string; 30 | "filter-price": string; 31 | }; 32 | }) { 33 | unstable_setRequestLocale(locale); 34 | const { t, isArabic } = useTranslate(); 35 | return ( 36 |
    37 |
    38 |
    39 | 40 | 41 | {isArabic ? : } 42 | 43 | 44 |

    {t("Laptops")}

    45 |
    46 | 47 |
    48 | 49 | }> 50 | 51 | 52 |
    53 | ); 54 | } 55 | 56 | export default LaptopsPage; 57 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/products/phones/page.tsx: -------------------------------------------------------------------------------- 1 | import AllProducts from "@/app/_components/marketking/AllProducts"; 2 | import FavoriteProductsListSkeleton from "@/app/_components/marketking/FavoriteProductsListSkeleton"; 3 | import FilterSheet from "@/app/_components/marketking/FilterSheet"; 4 | import Laptops from "@/app/_components/marketking/Laptops"; 5 | import Phones from "@/app/_components/marketking/Phones"; 6 | import IconButton from "@/app/_components/ui/IconButton"; 7 | import MyLink from "@/app/_components/ui/MyLink"; 8 | import { useTranslate } from "@/app/_hooks/useTranslate"; 9 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 10 | import { Suspense } from "react"; 11 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 12 | 13 | export async function generateMetadata({ 14 | params: { locale }, 15 | }: { 16 | params: { locale: string }; 17 | }) { 18 | const t = await getTranslations({ locale, namespace: "metadata" }); 19 | 20 | return { 21 | title: `${t("Phones")}`, 22 | }; 23 | } 24 | function LaptopsPage({ 25 | params: { locale }, 26 | searchParams, 27 | }: { 28 | params: { locale: string }; 29 | searchParams: { 30 | "sort-price": string; 31 | "filter-price": string; 32 | }; 33 | }) { 34 | unstable_setRequestLocale(locale); 35 | const { t, isArabic } = useTranslate(); 36 | return ( 37 |
    38 |
    39 |
    40 | 41 | 42 | {isArabic ? : } 43 | 44 | 45 |

    {t("Phones")}

    46 |
    47 | 48 |
    49 | 50 | }> 51 | 52 | 53 |
    54 | ); 55 | } 56 | 57 | export default LaptopsPage; 58 | -------------------------------------------------------------------------------- /app/_components/dashboard/RecentSalesCard.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import prisma from "@/app/_lib/db"; 3 | import { getTranslate } from "@/app/_utils/helpers"; 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 5 | 6 | async function RecentSalesCard() { 7 | const orders = await prisma.order.findMany({ 8 | select: { 9 | amount: true, 10 | id: true, 11 | User: { 12 | select: { 13 | firstName: true, 14 | lastName: true, 15 | profileImage: true, 16 | email: true, 17 | }, 18 | }, 19 | }, 20 | orderBy: { 21 | createdAt: "desc", 22 | }, 23 | take: 7, 24 | }); 25 | const { t, isArabic } = await getTranslate(); 26 | return ( 27 |
    28 |

    {t("Recent Sales")}

    29 | 30 |
      31 | {orders.map((order) => ( 32 |
    • 33 | 34 | 38 | 39 | 40 |
      41 |

      42 | {order.User?.firstName} {order.User?.lastName} 43 |

      44 |

      {order.User?.email}

      45 |
      46 | 51 | +${Intl.NumberFormat("en-US").format(order.amount / 100)} 52 | 53 |
    • 54 | ))} 55 |
    56 |
    57 | ); 58 | } 59 | 60 | export default RecentSalesCard; 61 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/products/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import CreateAndEditProductForm from "@/app/_components/dashboard/CreateAndEditProductForm"; 2 | import IconButton from "@/app/_components/ui/IconButton"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import prisma from "@/app/_lib/db"; 5 | import { getTranslate } from "@/app/_utils/helpers"; 6 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 7 | import { notFound } from "next/navigation"; 8 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 9 | 10 | export async function generateMetadata({ 11 | params: { locale }, 12 | }: { 13 | params: { locale: string }; 14 | }) { 15 | const t = await getTranslations({ locale, namespace: "metadata" }); 16 | 17 | return { 18 | title: `${t("Edit")}`, 19 | }; 20 | } 21 | 22 | async function getProduct(productId: string) { 23 | const product = await prisma.product.findUnique({ 24 | where: { 25 | id: productId, 26 | }, 27 | }); 28 | 29 | if (!product) { 30 | return notFound(); 31 | } 32 | return product; 33 | } 34 | 35 | export default async function EditRoute({ 36 | params, 37 | }: { 38 | params: { id: string; locale: string }; 39 | }) { 40 | unstable_setRequestLocale(params.locale); 41 | 42 | const product = await getProduct(params.id); 43 | 44 | const { t, isArabic } = await getTranslate(); 45 | return ( 46 |
    47 |
    48 | 49 | 50 | {isArabic ? : } 51 | 52 | 53 |

    {t("Edit Product")}

    54 |
    55 | 56 |
    57 |

    {t("Product Details")}

    58 |

    59 | {t("Product Edit title")} 60 |

    61 | 62 | 63 |
    64 |
    65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /app/_components/ui/Menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; 4 | import { FaBars } from "react-icons/fa"; 5 | import IconButton from "./IconButton"; 6 | import Logo from "../header/Logo"; 7 | import Link from "next/link"; 8 | import { usePathname } from "next/navigation"; 9 | import { useState } from "react"; 10 | import { dashboardNavLinks, homeNavLinks } from "@/app/_utils/helpers"; 11 | 12 | function Menu({ isDashboard = false }: { isDashboard?: boolean }) { 13 | const { isArabic, t, lang } = useTranslate(); 14 | const pathname = usePathname(); 15 | const [open, setOpen] = useState(false); 16 | 17 | const navLinks = isDashboard ? dashboardNavLinks : homeNavLinks; 18 | 19 | const handleLinkClick = () => { 20 | setOpen(false); 21 | }; 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 53 | 54 | 55 | ); 56 | } 57 | 58 | export default Menu; 59 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/products/watches/page.tsx: -------------------------------------------------------------------------------- 1 | import AllProducts from "@/app/_components/marketking/AllProducts"; 2 | import FavoriteProductsListSkeleton from "@/app/_components/marketking/FavoriteProductsListSkeleton"; 3 | import FilterSheet from "@/app/_components/marketking/FilterSheet"; 4 | import Laptops from "@/app/_components/marketking/Laptops"; 5 | import Phones from "@/app/_components/marketking/Phones"; 6 | import Watches from "@/app/_components/marketking/Watches"; 7 | import IconButton from "@/app/_components/ui/IconButton"; 8 | import MyLink from "@/app/_components/ui/MyLink"; 9 | import { useTranslate } from "@/app/_hooks/useTranslate"; 10 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 11 | import { Suspense } from "react"; 12 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 13 | 14 | export async function generateMetadata({ 15 | params: { locale }, 16 | }: { 17 | params: { locale: string }; 18 | }) { 19 | const t = await getTranslations({ locale, namespace: "metadata" }); 20 | 21 | return { 22 | title: `${t("Watches")}`, 23 | }; 24 | } 25 | function LaptopsPage({ 26 | params: { locale }, 27 | searchParams, 28 | }: { 29 | params: { locale: string }; 30 | searchParams: { 31 | "sort-price": string; 32 | "filter-price": string; 33 | }; 34 | }) { 35 | unstable_setRequestLocale(locale); 36 | const { t, isArabic } = useTranslate(); 37 | return ( 38 |
    39 |
    40 |
    41 | 42 | 43 | {isArabic ? : } 44 | 45 | 46 |

    {t("Watches")}

    47 |
    48 | 49 |
    50 | 51 | }> 52 | 53 | 54 |
    55 | ); 56 | } 57 | 58 | export default LaptopsPage; 59 | -------------------------------------------------------------------------------- /app/_components/marketking/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import SubmitButton from "@/app/_components/ui/SubmitButton"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import MyLink from "../ui/MyLink"; 5 | import { useFormState } from "react-dom"; 6 | import { useEffect } from "react"; 7 | import { loginAccount } from "@/app/_actions/loginAccount"; 8 | import toast from "react-hot-toast"; 9 | import ErrorMessage from "../ui/ErrorMessage"; 10 | 11 | function LoginForm() { 12 | const [state, formAction] = useFormState(loginAccount, {}); 13 | const { t } = useTranslate(); 14 | 15 | useEffect(() => { 16 | if (state.success === false) { 17 | toast.error(t("error login email")); 18 | } 19 | }, [state.success, t]); 20 | 21 | return ( 22 |
    23 | 24 | 32 | 33 | 40 | 41 |
      42 |
    • 43 | {state?.email && {t(state.email)}}{" "} 44 |
    • 45 |
    • 46 | {" "} 47 | {state?.password && ( 48 | {t(state.password)} 49 | )}{" "} 50 |
    • 51 |
    52 | 53 | 54 | {t("Login")} 55 | 56 | 57 |
    58 |

    {t("don't have account")}

    {" "} 59 | 63 | {t("Sign Up Now")} 64 | 65 |
    66 |
    67 | ); 68 | } 69 | 70 | export default LoginForm; 71 | -------------------------------------------------------------------------------- /prisma/migrations/20240722170420_add_favorite_products/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ProductStatus" AS ENUM ('draft', 'published', 'archived'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "Category" AS ENUM ('laptops', 'phones', 'watches'); 6 | 7 | -- CreateTable 8 | CREATE TABLE "User" ( 9 | "id" TEXT NOT NULL, 10 | "email" TEXT NOT NULL, 11 | "firstName" TEXT NOT NULL, 12 | "lastName" TEXT NOT NULL, 13 | "profileImage" TEXT NOT NULL, 14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | 16 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateTable 20 | CREATE TABLE "Product" ( 21 | "id" TEXT NOT NULL, 22 | "name" TEXT NOT NULL, 23 | "description" TEXT NOT NULL, 24 | "status" "ProductStatus" NOT NULL, 25 | "price" INTEGER NOT NULL, 26 | "images" TEXT[], 27 | "category" "Category" NOT NULL, 28 | "isFeatured" BOOLEAN NOT NULL DEFAULT false, 29 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | 31 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id") 32 | ); 33 | 34 | -- CreateTable 35 | CREATE TABLE "FavoriteProduct" ( 36 | "id" TEXT NOT NULL, 37 | "userId" TEXT NOT NULL, 38 | "productId" TEXT NOT NULL, 39 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 40 | 41 | CONSTRAINT "FavoriteProduct_pkey" PRIMARY KEY ("id") 42 | ); 43 | 44 | -- CreateTable 45 | CREATE TABLE "Banner" ( 46 | "id" TEXT NOT NULL, 47 | "title" TEXT NOT NULL, 48 | "imageString" TEXT NOT NULL, 49 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 50 | 51 | CONSTRAINT "Banner_pkey" PRIMARY KEY ("id") 52 | ); 53 | 54 | -- CreateIndex 55 | CREATE UNIQUE INDEX "User_id_key" ON "User"("id"); 56 | 57 | -- CreateIndex 58 | CREATE UNIQUE INDEX "FavoriteProduct_userId_productId_key" ON "FavoriteProduct"("userId", "productId"); 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "FavoriteProduct" ADD CONSTRAINT "FavoriteProduct_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | 63 | -- AddForeignKey 64 | ALTER TABLE "FavoriteProduct" ADD CONSTRAINT "FavoriteProduct_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 65 | -------------------------------------------------------------------------------- /app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from "@/app/_utils/getUser"; 2 | import { ADMIN_EMAIL } from "@/app/_utils/consistent"; 3 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 4 | import { UploadThingError } from "uploadthing/server"; 5 | const f = createUploadthing(); 6 | 7 | // FileRouter for your app, can contain multiple FileRoutes 8 | export const ourFileRouter = { 9 | // Define as many FileRoutes as you like, each with a unique routeSlug 10 | imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 10 } }) 11 | // Set permissions and file types for this FileRoute 12 | .middleware(async ({ req }) => { 13 | const user = await getUser(); 14 | // If you throw, the user will not be able to upload 15 | if (!user || user.email !== ADMIN_EMAIL) 16 | throw new UploadThingError("Unauthorized"); 17 | 18 | // Whatever is returned here is accessible in onUploadComplete as `metadata` 19 | return { userId: user.id }; 20 | }) 21 | .onUploadComplete(async ({ metadata, file }) => { 22 | console.log("Upload complete for userId:", metadata.userId); 23 | 24 | console.log("file url", file.url); 25 | 26 | // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback 27 | return { uploadedBy: metadata.userId }; 28 | }), 29 | 30 | bannerImageRoute: f({ image: { maxFileSize: "4MB", maxFileCount: 1 } }) 31 | // Set permissions and file types for this FileRoute 32 | .middleware(async ({ req }) => { 33 | const user = await getUser(); 34 | // If you throw, the user will not be able to upload 35 | if (!user) throw new UploadThingError("Unauthorized"); 36 | 37 | // Whatever is returned here is accessible in onUploadComplete as `metadata` 38 | return { userId: user.id }; 39 | }) 40 | .onUploadComplete(async ({ metadata, file }) => { 41 | console.log("Upload complete for userId:", metadata.userId); 42 | 43 | console.log("file url", file.url); 44 | 45 | // !!! Whatever is returned here is sent to the clientside `onClientUploadComplete` callback 46 | return { uploadedBy: metadata.userId }; 47 | }), 48 | } satisfies FileRouter; 49 | 50 | export type OurFileRouter = typeof ourFileRouter; 51 | -------------------------------------------------------------------------------- /app/_components/marketking/ProductInfoSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { FaStar } from "react-icons/fa"; 2 | 3 | function ProductInfoSkeleton() { 4 | return ( 5 | <> 6 |
    7 |
    8 |
    9 |
    10 | {Array.from({ length: 5 }, (_, index) => ( 11 |
    15 | ))} 16 |
    17 |
    18 | 19 |
    20 |
    21 |

    22 |
    23 |
    24 | 25 |

    26 | 27 |
      28 | {Array.from({ length: 5 }, (_, index) => ( 29 |
    1. 33 | 34 |
    2. 35 | ))} 36 |
    37 | 38 |

    39 |

    40 |

    41 | 42 |
    43 | 44 |
    45 |
    46 |
    47 | 48 | ); 49 | } 50 | 51 | export default ProductInfoSkeleton; 52 | -------------------------------------------------------------------------------- /app/_components/marketking/ImageSlider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import Image from "next/image"; 3 | import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io"; 4 | import { useTranslate } from "@/app/_hooks/useTranslate"; 5 | import { useState } from "react"; 6 | 7 | type Props = { 8 | images: string[]; 9 | }; 10 | 11 | function ImageSlider({ images }: Props) { 12 | const [mainIndex, setMainIndex] = useState(0); 13 | const { isArabic } = useTranslate(); 14 | 15 | function handlePrev() { 16 | setMainIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1)); 17 | } 18 | function handleNext() { 19 | setMainIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1)); 20 | } 21 | return ( 22 |
    23 |
    24 | Product img 31 | 32 |
    33 | 39 | 40 | 46 |
    47 |
    48 |
    49 | {images.map((image, index) => ( 50 |
    51 | Product Img setMainIndex(index)} 59 | /> 60 |
    61 | ))} 62 |
    63 |
    64 | ); 65 | } 66 | 67 | export default ImageSlider; 68 | -------------------------------------------------------------------------------- /app/_actions/createProduct.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { ProductStatus } from "@prisma/client"; 4 | import { revalidatePath } from "next/cache"; 5 | import prisma from "../_lib/db"; 6 | import { ProductErrors } from "../_utils/types"; 7 | 8 | export async function createProduct(_: any, formData: FormData) { 9 | const isArabic = formData.get("isArabic"); 10 | const product = formData.get("product") as string; 11 | const description = formData.get("description") as string; 12 | const price = formData.get("price"); 13 | const discount = formData.get("discount"); 14 | const category = formData.get("category") as string; 15 | const featured = formData.get("featured") === "on"; 16 | const status = formData.get("status") as ProductStatus; 17 | let images = formData.get("images") as string | string[]; 18 | let errors: ProductErrors = {}; 19 | 20 | if (!product || product.trim().length === 0) { 21 | errors.product = "product error message"; 22 | } 23 | if (!category || category.trim().length === 0) { 24 | errors.category = "category error message"; 25 | } 26 | if (!description || description.trim().length === 0) { 27 | errors.description = "description error message"; 28 | } 29 | if (!price || Number(price) <= 0) { 30 | errors.price = "price error message"; 31 | } 32 | if (Number(discount) > Number(price)) { 33 | errors.discount = "discount error message"; 34 | } 35 | if (!images || images.length === 0) { 36 | errors.image = "image error message"; 37 | } 38 | if (!status || status.trim().length === 0) { 39 | errors.status = "status error message"; 40 | } 41 | 42 | if (Object.keys(errors).length > 0) { 43 | return errors; 44 | } 45 | 46 | if (category !== "laptops" && category !== "watches" && category !== "phones") 47 | return; 48 | images = typeof images === "string" ? images.split(",") : [""]; 49 | 50 | try { 51 | await prisma.product.create({ 52 | data: { 53 | name: product, 54 | description, 55 | status, 56 | price: Number(price), 57 | images, 58 | category, 59 | isFeatured: featured, 60 | discount: Number(discount) < 0 ? 0 : Number(discount), 61 | }, 62 | }); 63 | if (isArabic) { 64 | revalidatePath("/ar/dashboard", "layout"); 65 | } else { 66 | revalidatePath("/en/dashboard", "layout"); 67 | } 68 | return { success: true }; 69 | } catch { 70 | return { success: false }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/_components/ui/ToggleTheme.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuSeparator, 8 | DropdownMenuTrigger, 9 | } from "@/components/ui/dropdown-menu"; 10 | import IconButton from "./IconButton"; 11 | import { useTheme } from "next-themes"; 12 | import { 13 | MdOutlineDarkMode, 14 | MdOutlineLightMode, 15 | MdDevices, 16 | } from "react-icons/md"; 17 | 18 | function ToggleTheme({ isDashboard = false }: { isDashboard?: boolean }) { 19 | const { setTheme, theme, systemTheme } = useTheme(); 20 | 21 | const { t, isArabic } = useTranslate(); 22 | const currentTheme = theme === "system" ? systemTheme : theme; 23 | 24 | return ( 25 | 26 | 27 | 31 | {currentTheme === "dark" ? ( 32 | 33 | ) : ( 34 | 35 | )} 36 | 37 | 38 | 39 | setTheme("light")} 46 | > 47 | {t("Light Mode")} 48 | 49 | 50 | 51 | 52 | setTheme("dark")} 59 | > 60 | {t("Dark Mode")} 61 | 62 | 63 | 64 | setTheme("system")} 71 | > 72 | {t("System")} 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export default ToggleTheme; 80 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/banner/[id]/delete/page.tsx: -------------------------------------------------------------------------------- 1 | import { deleteBanner } from "@/app/_actions/deleteBanner"; 2 | import Button from "@/app/_components/ui/Button"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import SubmitButton from "@/app/_components/ui/SubmitButton"; 5 | import prisma from "@/app/_lib/db"; 6 | import { getTranslate } from "@/app/_utils/helpers"; 7 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 8 | import Image from "next/image"; 9 | import { notFound } from "next/navigation"; 10 | 11 | export async function generateMetadata({ 12 | params: { locale }, 13 | }: { 14 | params: { locale: string }; 15 | }) { 16 | const t = await getTranslations({ locale, namespace: "metadata" }); 17 | 18 | return { 19 | title: `${t("Delete Banner")}`, 20 | }; 21 | } 22 | async function page({ 23 | params: { id, locale }, 24 | }: { 25 | params: { id: string; locale: string }; 26 | }) { 27 | unstable_setRequestLocale(locale); 28 | 29 | const banner = await prisma.banner.findUnique({ 30 | where: { 31 | id, 32 | }, 33 | }); 34 | const { t } = await getTranslate(); 35 | 36 | if (!banner) { 37 | return notFound(); 38 | } 39 | return ( 40 |
    41 |
    42 |
    43 | {t("delete title banner")}{" "} 44 | ({banner.title}) 45 |
    46 |

    47 | {t("delete title desc banner")} 48 |

    49 | {banner.title} 57 |
    58 | 61 |
    62 | 63 | 64 | {t("Yes")} 65 | 66 |
    67 |
    68 |
    69 |
    70 | ); 71 | } 72 | 73 | export default page; 74 | -------------------------------------------------------------------------------- /app/_actions/editProduct.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { ProductStatus } from "@prisma/client"; 4 | import { revalidatePath } from "next/cache"; 5 | import prisma from "../_lib/db"; 6 | import { ProductErrors } from "../_utils/types"; 7 | 8 | export async function editProduct(_: any, formData: FormData) { 9 | const productId = formData.get("productId") as string; 10 | 11 | const isArabic = formData.get("isArabic"); 12 | const product = formData.get("product") as string; 13 | const discount = formData.get("discount"); 14 | 15 | const description = formData.get("description") as string; 16 | const price = formData.get("price"); 17 | const category = formData.get("category") as string; 18 | const featured = formData.get("featured") === "on"; 19 | const status = formData.get("status") as ProductStatus; 20 | let images = formData.get("images") as string | string[]; 21 | 22 | let errors: ProductErrors = {}; 23 | 24 | if (Number(discount) > Number(price)) { 25 | errors.discount = "discount error message"; 26 | } 27 | if (!product || product.trim().length === 0) { 28 | errors.product = "product error message"; 29 | } 30 | if (!category || category.trim().length === 0) { 31 | errors.category = "category error message"; 32 | } 33 | if (!description || description.trim().length === 0) { 34 | errors.description = "description error message"; 35 | } 36 | if (!price || Number(price) <= 0) { 37 | errors.price = "price error message"; 38 | } 39 | if (!images || images.length === 0) { 40 | errors.image = "image error message"; 41 | } 42 | if (!status || status.trim().length === 0) { 43 | errors.status = "status error message"; 44 | } 45 | 46 | if (Object.keys(errors).length > 0) { 47 | return errors; 48 | } 49 | 50 | if (category !== "laptops" && category !== "watches" && category !== "phones") 51 | return; 52 | images = typeof images === "string" ? images.split(",") : [""]; 53 | 54 | try { 55 | await prisma.product.update({ 56 | where: { 57 | id: productId, 58 | }, 59 | data: { 60 | name: product, 61 | description, 62 | status, 63 | price: Number(price), 64 | images, 65 | category, 66 | isFeatured: featured, 67 | discount: Number(discount) < 0 ? 0 : Number(discount), 68 | }, 69 | }); 70 | 71 | if (isArabic) { 72 | revalidatePath("/ar/dashboard", "layout"); 73 | } else { 74 | revalidatePath("/en/dashboard", "layout"); 75 | } 76 | 77 | return { success: true }; 78 | } catch { 79 | return { success: false }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/_components/ui/User.tsx: -------------------------------------------------------------------------------- 1 | import { getUser } from "@/app/_utils/getUser"; 2 | import { getTranslate } from "@/app/_utils/helpers"; 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuLabel, 9 | DropdownMenuSeparator, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu"; 12 | import unknown from "@/public/unknownUser.jpg"; 13 | import LogoutButton from "./LogoutButton"; 14 | import MyLink from "./MyLink"; 15 | 16 | async function User() { 17 | const user = await getUser(); 18 | const { t } = await getTranslate(); 19 | return ( 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 |

    32 | {user?.firstName} {user?.lastName} 33 |

    34 |

    37 | {user?.email} 38 |

    39 |
    40 | 41 | 42 | {user?.email === "faresahmed00001111@gmail.com" && ( 43 | <> 44 | 45 | {t("Home")} 46 | 47 | 48 | 49 | 50 | {t("Dashboard")} 51 | 52 | 53 | )} 54 | 55 | 56 | 57 | {t("My purchases")} 58 | 59 | 60 | 61 | 62 | {t("Edit Profile")} 63 | 64 | 65 | 66 | 67 |
    68 |
    69 | ); 70 | } 71 | 72 | export default User; 73 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/products/[id]/delete/page.tsx: -------------------------------------------------------------------------------- 1 | import { deleteProduct } from "@/app/_actions/deleteProduct"; 2 | import Button from "@/app/_components/ui/Button"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import SubmitButton from "@/app/_components/ui/SubmitButton"; 5 | import prisma from "@/app/_lib/db"; 6 | import { getTranslate } from "@/app/_utils/helpers"; 7 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 8 | import Image from "next/image"; 9 | import { notFound } from "next/navigation"; 10 | 11 | export async function generateMetadata({ 12 | params: { locale }, 13 | }: { 14 | params: { locale: string }; 15 | }) { 16 | const t = await getTranslations({ locale, namespace: "metadata" }); 17 | 18 | return { 19 | title: `${t("Delete")}`, 20 | }; 21 | } 22 | 23 | async function getProduct(productId: string) { 24 | const product = await prisma.product.findUnique({ 25 | where: { 26 | id: productId, 27 | }, 28 | }); 29 | 30 | if (!product) { 31 | return notFound(); 32 | } 33 | return product; 34 | } 35 | 36 | async function page({ 37 | params: { id, locale }, 38 | }: { 39 | params: { id: string; locale: string }; 40 | }) { 41 | unstable_setRequestLocale(locale); 42 | 43 | const product = await getProduct(id); 44 | 45 | const { t } = await getTranslate(); 46 | return ( 47 |
    48 |
    49 |
    50 | {t("delete title")}{" "} 51 | ({product.name}) 52 |
    53 |

    54 | {t("delete title desc")} 55 |

    56 | {product.name} 64 |
    65 | 68 |
    69 | 70 | 71 | {t("Yes")} 72 | 73 |
    74 |
    75 |
    76 |
    77 | ); 78 | } 79 | 80 | export default page; 81 | -------------------------------------------------------------------------------- /app/_actions/toggleFavProduct.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; 6 | 7 | type User = 8 | | ({ 9 | favoriteProducts: { 10 | productId: string; 11 | }[]; 12 | } & { 13 | id: string; 14 | email: string; 15 | password: string; 16 | firstName: string; 17 | lastName: string; 18 | profileImage: string; 19 | createdAt: Date; 20 | }) 21 | | null; 22 | 23 | export async function toggleFavProduct({ 24 | user, 25 | productId, 26 | }: { 27 | user: User; 28 | productId: string; 29 | }) { 30 | if (!user) { 31 | return { success: false, message: "User not authenticated" }; 32 | } 33 | 34 | try { 35 | const existingFavorite = await prisma.favoriteProduct.findUnique({ 36 | where: { 37 | userId_productId: { 38 | userId: user.id, 39 | productId: productId, 40 | }, 41 | }, 42 | }); 43 | 44 | if (!existingFavorite) { 45 | const favProduct = await prisma.favoriteProduct.upsert({ 46 | where: { 47 | userId_productId: { 48 | userId: user.id, 49 | productId: productId, 50 | }, 51 | }, 52 | update: {}, 53 | create: { productId, userId: user.id }, 54 | select: { product: true }, 55 | }); 56 | 57 | revalidatePath("/", "layout"); 58 | return { success: true, favProduct: favProduct.product.name }; 59 | } else { 60 | // If the favorite exists, delete it 61 | try { 62 | const favProduct = await prisma.favoriteProduct.delete({ 63 | where: { 64 | userId_productId: { 65 | userId: user.id, 66 | productId: productId, 67 | }, 68 | }, 69 | select: { product: true }, 70 | }); 71 | 72 | revalidatePath("/", "layout"); 73 | return { success: false, favProduct: favProduct.product.name }; 74 | } catch (deleteError) { 75 | if ( 76 | deleteError instanceof PrismaClientKnownRequestError && 77 | deleteError.code === "P2025" 78 | ) { 79 | // Record was already deleted, treat as success 80 | revalidatePath("/", "layout"); 81 | return { success: false, message: "Favorite already removed" }; 82 | } 83 | throw deleteError; 84 | } 85 | } 86 | } catch (error) { 87 | console.error("Error toggling favorite:", error); 88 | return { 89 | success: false, 90 | message: "An error occurred while updating favorite", 91 | }; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@/app/_components/theme-provider"; 2 | import type { Metadata } from "next"; 3 | import { NextIntlClientProvider } from "next-intl"; 4 | import { NextSSRPlugin } from "@uploadthing/react/next-ssr-plugin"; 5 | 6 | import { 7 | getMessages, 8 | getTranslations, 9 | unstable_setRequestLocale, 10 | } from "next-intl/server"; 11 | import { NotoKufiArabic, roboto } from "../fonts"; 12 | import "./globals.css"; 13 | import ToasterProvider from "../_components/Toaster"; 14 | import { extractRouterConfig } from "uploadthing/server"; 15 | import { ourFileRouter } from "../api/uploadthing/core"; 16 | import { LOCALES } from "../_utils/consistent"; 17 | 18 | export function generateStaticParams() { 19 | return LOCALES.map((locale) => ({ locale })); 20 | } 21 | 22 | export async function generateMetadata({ 23 | params: { locale }, 24 | }: { 25 | params: { locale: string }; 26 | }): Promise { 27 | const t = await getTranslations({ locale, namespace: "metadata" }); 28 | 29 | return { 30 | metadataBase: new URL("https://market-king-ecommerce.vercel.app"), 31 | 32 | title: { 33 | template: `%s | ${t("title")}`, 34 | default: `${t("homeTitle")} | ${t("title")}`, 35 | }, 36 | description: 37 | "MarketKing: Secure e-commerce platform featuring user authentication, Stripe payments, and an admin dashboard", 38 | openGraph: { 39 | title: "MarketKing", 40 | description: 41 | "Online marketplace solution 'MarketKing' with user login, Stripe integration, and management interface", 42 | images: ["/project-preview.png"], 43 | url: "https://market-king-ecommerce.vercel.app/", 44 | type: "website", 45 | }, 46 | }; 47 | } 48 | 49 | export default async function LocaleLayout({ 50 | children, 51 | params: { locale }, 52 | }: { 53 | children: React.ReactNode; 54 | params: { locale: string }; 55 | }) { 56 | unstable_setRequestLocale(locale); 57 | 58 | const messages = await getMessages(); 59 | 60 | const dir = locale === "ar" ? "rtl" : "ltr"; 61 | 62 | return ( 63 | 64 | 69 | 70 | 71 | 72 | 78 | {children} 79 | 80 | 81 | 82 | 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /app/_actions/createAccount.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import prisma from "../_lib/db"; 5 | import { SignUpErrors } from "../_utils/types"; 6 | import { cookies } from "next/headers"; 7 | import { redirect } from "next/navigation"; 8 | import { getTranslate } from "../_utils/helpers"; 9 | // todo: REGEX 10 | const MAIL_REGEX = /^[a-z0-9]+@[a-z0-9]+\.(?!$)[a-z]{1,}(?:\.[a-z]{1,})*$/; 11 | const PASSWORD_REGEX = /^.{8,}$/; 12 | 13 | export async function createAccount(_: any, formData: FormData) { 14 | const { isArabic } = await getTranslate(); 15 | const firstName = formData.get("first-name") as string; 16 | const lastName = formData.get("last-name") as string; 17 | const email = formData.get("email") as string; 18 | const password = formData.get("password") as string; 19 | const confirmPassword = formData.get("confirmPassword") as string; 20 | 21 | let errors: SignUpErrors = {}; 22 | 23 | if (!firstName || firstName.trim().length === 0) { 24 | errors.firstName = "firstName error message"; 25 | } 26 | if (!lastName || lastName.trim().length === 0) { 27 | errors.lastName = "lastName error message"; 28 | } 29 | 30 | if (!MAIL_REGEX.test(email) || email.trim().length === 0) { 31 | errors.email = "email error message"; 32 | } 33 | 34 | if (!PASSWORD_REGEX.test(password) || password.trim().length === 0) { 35 | errors.password = "password error message"; 36 | } 37 | 38 | if (password !== confirmPassword) { 39 | errors.confirmPassword = "confirmPassword error message"; 40 | } 41 | 42 | if (Object.keys(errors).length > 0) { 43 | return errors; 44 | } 45 | const emails = await prisma.user.findMany({ select: { email: true } }); 46 | 47 | const isEmailExist = emails.find((item) => item.email === email); 48 | 49 | if (isEmailExist) { 50 | errors.success = false; 51 | 52 | return errors; 53 | } 54 | 55 | const createdUser = await prisma.user.create({ 56 | data: { 57 | email, 58 | firstName, 59 | password, 60 | lastName, 61 | profileImage: `https://avatar.vercel.sh/${email}.svg?text=${firstName.charAt( 62 | 0 63 | )}${lastName.charAt(0)}`, 64 | }, 65 | }); 66 | 67 | if (createdUser) { 68 | revalidatePath("/"); 69 | 70 | cookies().set( 71 | "user_info", 72 | JSON.stringify({ 73 | email: createdUser.email, 74 | userId: createdUser.id, 75 | }), 76 | { 77 | httpOnly: true, 78 | secure: process.env.NODE_ENV === "production", 79 | sameSite: "lax", 80 | maxAge: 60 * 60 * 24 * 7, // 1 week 81 | path: "/", 82 | } 83 | ); 84 | 85 | if (isArabic) { 86 | redirect("/ar"); 87 | } else { 88 | redirect("/en"); 89 | } 90 | } 91 | 92 | return { success: false }; 93 | } 94 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id @unique @default(uuid()) 18 | email String @unique 19 | password String 20 | firstName String 21 | lastName String 22 | profileImage String 23 | cart Cart? 24 | favoriteProducts FavoriteProduct[] 25 | orders Order[] 26 | createdAt DateTime @default(now()) 27 | } 28 | 29 | model Cart { 30 | id String @id @default(uuid()) 31 | user User @relation(fields: [userId], references: [id]) 32 | userId String @unique 33 | items CartItem[] 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @updatedAt 36 | } 37 | model CartItem { 38 | id String @id @default(uuid()) 39 | cart Cart @relation(fields: [cartId], references: [id]) 40 | cartId String 41 | productId String 42 | quantity Int 43 | price Int 44 | discount Int 45 | imageString String 46 | name String 47 | createdAt DateTime @default(now()) 48 | updatedAt DateTime @updatedAt 49 | 50 | @@unique([cartId, productId]) 51 | } 52 | model Product { 53 | id String @id @default(uuid()) 54 | name String 55 | description String 56 | status ProductStatus 57 | price Int 58 | discount Int 59 | images String[] 60 | category Category 61 | isFeatured Boolean @default(false) 62 | favoritedBy FavoriteProduct[] 63 | createdAt DateTime @default(now()) 64 | } 65 | 66 | model FavoriteProduct { 67 | id String @id @default(uuid()) 68 | user User @relation(fields: [userId], references: [id]) 69 | userId String 70 | product Product @relation(fields: [productId], references: [id]) 71 | productId String 72 | createdAt DateTime @default(now()) 73 | 74 | @@unique([userId, productId]) 75 | } 76 | 77 | 78 | model Banner { 79 | id String @id @default(uuid()) 80 | title String 81 | imageString String 82 | 83 | createdAt DateTime @default(now()) 84 | } 85 | 86 | model Order { 87 | id String @id @default(uuid()) 88 | status String @default("pending") 89 | amount Int 90 | 91 | User User? @relation(fields: [userId], references: [id]) 92 | userId String? 93 | 94 | createdAt DateTime @default(now()) 95 | } 96 | 97 | enum ProductStatus { 98 | draft 99 | published 100 | archived 101 | } 102 | enum Category { 103 | laptops 104 | phones 105 | watches 106 | } -------------------------------------------------------------------------------- /app/_components/ui/Modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import useClickOutside from "@/app/_hooks/useClickOutside"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import React, { 5 | ReactNode, 6 | createContext, 7 | useContext, 8 | useRef, 9 | useState, 10 | } from "react"; 11 | import { createPortal } from "react-dom"; 12 | 13 | interface ModalContextType { 14 | openId: string; 15 | close: () => void; 16 | open: (id: string) => void; 17 | } 18 | 19 | const ModalContext = createContext(undefined); 20 | 21 | interface ModalProps { 22 | children: ReactNode; 23 | } 24 | 25 | function Modal({ children }: ModalProps) { 26 | const [openId, setOpenId] = useState(""); 27 | const close = () => setOpenId(""); 28 | const open = (id: string) => setOpenId(id); 29 | 30 | return ( 31 | 32 |
    {children}
    33 |
    34 | ); 35 | } 36 | 37 | interface OpenModalProps { 38 | id: string; 39 | isFull?: boolean; 40 | children: React.ReactNode; 41 | } 42 | 43 | function useModalContext() { 44 | const context = useContext(ModalContext); 45 | if (!context) { 46 | throw new Error("Modal context must be used within a Modal provider"); 47 | } 48 | return context; 49 | } 50 | 51 | function OpenModal({ id, children, isFull }: OpenModalProps) { 52 | const { t } = useTranslate(); 53 | const { open } = useModalContext(); 54 | function handleClick() { 55 | open(id); 56 | } 57 | 58 | return ( 59 | 66 | {children} 67 | 68 | ); 69 | } 70 | 71 | interface ContentProps { 72 | id: string; 73 | children: (props: { close: () => void }) => React.ReactNode; 74 | } 75 | 76 | function Content({ id, children }: ContentProps) { 77 | const { openId, close } = useModalContext(); 78 | const elementRef = useRef(null); 79 | 80 | useClickOutside([elementRef], () => { 81 | close(); 82 | }); 83 | 84 | if (openId !== id) return null; 85 | 86 | return createPortal( 87 | e.stopPropagation()} 90 | > 91 |
    95 | {children({ close })} 96 |
    97 |
    , 98 | document.body 99 | ); 100 | } 101 | 102 | Modal.OpenModal = OpenModal; 103 | Modal.Content = Content; 104 | 105 | export default Modal; 106 | -------------------------------------------------------------------------------- /app/_components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import { Slot } from "@radix-ui/react-slot"; 4 | 5 | type ButtonProps = { 6 | children: React.ReactNode; 7 | iconOnly?: boolean; 8 | beforeContent?: React.ReactNode; 9 | afterContent?: React.ReactNode; 10 | size?: "sm" | "md" | "lg"; 11 | active?: boolean; 12 | disabled?: boolean; 13 | variant?: "primary" | "secondary"; 14 | className?: string; 15 | color?: "primary" | "black" | "white" | "info" | "warning" | "error"; 16 | asChild?: boolean; 17 | } & React.ButtonHTMLAttributes; 18 | 19 | const Button = React.forwardRef( 20 | ( 21 | { 22 | active, 23 | disabled, 24 | className = "", 25 | iconOnly, 26 | beforeContent, 27 | afterContent, 28 | size, 29 | variant = "primary", 30 | color = "primary", 31 | children, 32 | asChild = false, 33 | ...props 34 | }, 35 | ref 36 | ) => { 37 | const Comp = asChild ? Slot : "button"; 38 | 39 | const baseStyles = 40 | "transition-all duration-250 flex items-center justify-center border-none font-medium cursor-pointer rounded-lg gap-1 "; 41 | const sizeStyles = { 42 | sm: "py-1 px-2 text-sm", 43 | md: "py-2 px-4 text-base", 44 | lg: "py-3 px-6 text-lg", 45 | }; 46 | const colorStyles = { 47 | primary: 48 | "bg-primary-bg-color hover:bg-primary-color-hover disabled:bg-green-700 text-black dark:text-white", 49 | black: 50 | "bg-black hover:bg-black/80 disabled:bg-stone-700 text-white dark:hover:bg-black/50", 51 | white: 52 | "bg-white hover:bg-white/50 disabled:bg-stone-700 text-black dark:hover:bg-white/80", 53 | info: "bg-blue-500 text-white border-blue-400 hover:bg-blue-600 active:bg-blue-700 disabled:bg-blue-200 disabled:text-stone-600", 54 | warning: 55 | "bg-yellow-500 text-white border-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 disabled:bg-yellow-300", 56 | error: 57 | "bg-red-500 dark:bg-red-600 text-white border-red-500 dark:border-red-600 hover:bg-red-600 dark:hover:bg-red-700 active:bg-red-700 dark:active:bg-red-800 disabled:bg-red-300 dark:disabled:bg-red-400", 58 | }; 59 | const variantStyles = { 60 | primary: "", 61 | secondary: "border-2", 62 | }; 63 | const activeStyles = active ? "shadow-inner" : ""; 64 | const iconOnlyStyles = iconOnly ? "p-1 aspect-square" : ""; 65 | const disabledStyles = disabled ? "opacity-70 cursor-not-allowed" : ""; 66 | 67 | const content = ( 68 | <> 69 | {beforeContent} 70 | {children} 71 | {afterContent} 72 | 73 | ); 74 | 75 | return ( 76 | 92 | {asChild ? React.Children.only(children) : content} 93 | 94 | ); 95 | } 96 | ); 97 | 98 | Button.displayName = "Button"; 99 | 100 | export default Button; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

          

    2 | 3 |
    4 | MarketKing logo 5 |
    6 |
    7 | 8 |

    MarketKing

    9 |

    My e-commerce web app, built using Next.js, is fully responsive and includes an admin dashboard, Stripe payment integration, and user authentication (login and sign-up pages). The app supports multiple languages (English and Arabic) and features a toggle for theme mode, offering an improved user experience and enhanced performance. 10 |  live demo , (If you see an error message or run into an issue, please create bug report. thank you)

    11 | 12 | --- 13 | 14 |
    15 | MarketKing app 16 |
    17 |
    18 | MarketKing app 19 |
    20 |
    21 | MarketKing app 22 |
    23 |
    24 | MarketKing app 25 |
    26 |
    27 | 28 | --- 29 | 30 | ## Primary technologies employed 31 | 32 | NextJs Technique    Tailwind Technique    Prisma Technique    Stripe Technique    Typescript Technique    PostgreSQL Technique    Shadcn Technique 33 | 34 | ## Lighthouse project performance 35 | 36 |
    37 | Lighthouse Performance 38 |
    39 | 40 | 41 | ## Building and running on localhost 42 | 43 | First install dependencies: 44 | 45 | ```sh 46 | npm install 47 | ``` 48 | 49 | To run in hot module reloading mode: 50 | 51 | ```sh 52 | npm run dev 53 | ``` 54 | 55 | To create a production build: 56 | 57 | ```sh 58 | npm run build 59 | ``` 60 | 61 | ## App User Experience 62 | (note) If your internet speed is slow, please be patient as the GIF may take some time to download 63 |
    64 | App user experience 65 |
    66 | 67 | --- 68 | 69 |
    70 | App user experience 71 |
    72 | -------------------------------------------------------------------------------- /app/_components/ui/Filter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { usePathname, useRouter, useSearchParams } from "next/navigation"; 4 | 5 | const sortArr = [ 6 | { label: "All", name: "all" }, 7 | { label: "Lowest", name: "lowest" }, 8 | { label: "Highest", name: "highest" }, 9 | ]; 10 | 11 | const filterArr = [ 12 | { label: "All", name: "all" }, 13 | { label: "less-then-200", name: "less-then-200" }, 14 | { label: "between-200-500", name: "between-200-500" }, 15 | { label: "between-1000-5000", name: "between-1000-5000" }, 16 | { label: "more-then-5000", name: "more-then-5000" }, 17 | ]; 18 | 19 | function Filter({ isMedium = false }: { isMedium?: boolean }) { 20 | const { t } = useTranslate(); 21 | const searchParams = useSearchParams(); 22 | const router = useRouter(); 23 | const pathname = usePathname(); 24 | const activeSort = searchParams.get("sort-price") || "all"; 25 | const activeFilterPrice = searchParams.get("filter-price") || "all"; 26 | 27 | const handleSort = (filter: string) => { 28 | const params = new URLSearchParams(searchParams); 29 | params.set("sort-price", filter); 30 | router.replace(`${pathname}?${params.toString()}`, { scroll: false }); 31 | }; 32 | 33 | const handleFilterPrice = (filter: string) => { 34 | const params = new URLSearchParams(searchParams); 35 | params.set("filter-price", filter); 36 | router.replace(`${pathname}?${params.toString()}`, { scroll: false }); 37 | }; 38 | 39 | return ( 40 | 99 | ); 100 | } 101 | 102 | export default Filter; 103 | -------------------------------------------------------------------------------- /app/_components/marketking/CreateAccountForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import SubmitButton from "@/app/_components/ui/SubmitButton"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import MyLink from "../ui/MyLink"; 5 | import { useFormState } from "react-dom"; 6 | import { createAccount } from "@/app/_actions/createAccount"; 7 | import ErrorMessage from "../ui/ErrorMessage"; 8 | import { useEffect } from "react"; 9 | import toast from "react-hot-toast"; 10 | 11 | function CreateAccountForm() { 12 | const [state, formAction] = useFormState(createAccount, {}); 13 | const { t } = useTranslate(); 14 | 15 | useEffect(() => { 16 | if (state.success === false) { 17 | toast.error(t("error create email")); 18 | } 19 | }, [state.success, t]); 20 | return ( 21 |
    22 |
    23 |
    24 | 25 | 33 |
    34 |
    35 | 36 | 43 |
    44 |
    45 | 46 | 53 | 54 | 63 | 64 | {" "} 73 |
      74 |
    • 75 | {state?.firstName && ( 76 | {t(state.firstName)} 77 | )} 78 |
    • 79 |
    • 80 | {state?.lastName && {t(state.lastName)}} 81 |
    • 82 |
    • 83 | {state?.email && {t(state.email)}}{" "} 84 |
    • 85 |
    • 86 | {" "} 87 | {state?.password && ( 88 | {t(state.password)} 89 | )}{" "} 90 |
    • 91 |
    • 92 | {" "} 93 | {state?.confirmPassword && ( 94 | {t(state.confirmPassword)} 95 | )}{" "} 96 |
    • 97 |
    98 | 99 | {t("Sign Up")} 100 | 101 |
    102 |

    {t("have account")}

    {" "} 103 | 107 | {t("Login Now")} 108 | 109 |
    110 |
    111 | ); 112 | } 113 | 114 | export default CreateAccountForm; 115 | -------------------------------------------------------------------------------- /app/[locale]/dashboard/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslate } from "@/app/_hooks/useTranslate"; 2 | import prisma from "@/app/_lib/db"; 3 | import { formatDate, getTranslate } from "@/app/_utils/helpers"; 4 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 5 | 6 | export async function generateMetadata({ 7 | params: { locale }, 8 | }: { 9 | params: { locale: string }; 10 | }) { 11 | const t = await getTranslations({ locale, namespace: "metadata" }); 12 | 13 | return { 14 | title: `${t("Orders")}`, 15 | }; 16 | } 17 | 18 | export default async function OrdersPage({ 19 | params: { locale }, 20 | }: { 21 | params: { locale: string }; 22 | }) { 23 | unstable_setRequestLocale(locale); 24 | 25 | const orders = await prisma.order.findMany({ 26 | select: { 27 | amount: true, 28 | createdAt: true, 29 | status: true, 30 | id: true, 31 | User: { 32 | select: { 33 | firstName: true, 34 | email: true, 35 | profileImage: true, 36 | lastName: true, 37 | }, 38 | }, 39 | }, 40 | orderBy: { 41 | createdAt: "desc", 42 | }, 43 | }); 44 | const { t } = await getTranslate(); 45 | return ( 46 |
    47 |
    48 |

    {t("Orders")}

    49 |

    {t("Orders label")}

    50 | 51 | {orders && orders.length > 0 ? ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {orders.map((order) => ( 64 | 68 | 82 | 83 | 84 | 87 | 90 | 91 | ))} 92 | 93 |
    {t("Customer")}{t("Type")}{t("Status")}{t("Date")}{t("Amount")}
    69 | 73 | {order.User?.firstName} {order.User?.lastName} 74 | 75 | 79 | {order.User?.email} 80 | 81 | {t("Order")}{t(order.status)} 85 | {formatDate(order.createdAt)} 86 | 88 | ${new Intl.NumberFormat("en-Us").format(order.amount / 100)} 89 |
    94 | ) : ( 95 |

    96 | {t("not found orders")} 97 |

    98 | )} 99 |
    100 |
    101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /app/_components/ui/Marquee.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { newPrice } from "@/app/_utils/helpers"; 5 | 6 | import FavButton from "../marketking/FavButton"; 7 | import LoginFirst from "../marketking/LoginFirst"; 8 | import Button from "./Button"; 9 | import MyLink from "./MyLink"; 10 | import { toggleFavProduct } from "@/app/_actions/toggleFavProduct"; 11 | import toast from "react-hot-toast"; 12 | import Marquee from "react-fast-marquee"; 13 | import ImagesSlider from "./ImagesSlider"; 14 | 15 | type ItemType = { 16 | id: string; 17 | name: string; 18 | price: number; 19 | discount: number; 20 | images: string[]; 21 | isFav?: boolean; 22 | }; 23 | 24 | type User = 25 | | ({ 26 | favoriteProducts: { 27 | productId: string; 28 | }[]; 29 | } & { 30 | id: string; 31 | email: string; 32 | password: string; 33 | firstName: string; 34 | lastName: string; 35 | profileImage: string; 36 | createdAt: Date; 37 | }) 38 | | null; 39 | 40 | function MarqueeProducts({ 41 | items, 42 | user, 43 | }: { 44 | items: ItemType[]; 45 | user: User | null; 46 | }) { 47 | const { t } = useTranslate(); 48 | 49 | async function favProduct({ 50 | user, 51 | productId, 52 | }: { 53 | user: User; 54 | productId: string; 55 | }) { 56 | const res = await toggleFavProduct({ user, productId }); 57 | 58 | if (res?.success && res?.favProduct) { 59 | toast.success(t("add fav success", { favProduct: res.favProduct })); 60 | } 61 | if (res?.success === false && res?.favProduct) { 62 | toast.success(t("remove fav success", { favProduct: res.favProduct })); 63 | } 64 | } 65 | 66 | if (items.length < 1) 67 | return ( 68 |
    {t("No Featured Products")}
    69 | ); 70 | return ( 71 | 75 | {items?.concat(items)?.map((item, index) => ( 76 |
    80 | 81 | {user ? ( 82 |
    89 | product.productId === item.id 92 | )} 93 | /> 94 | 95 | ) : ( 96 | 97 | 98 | 99 | )} 100 |
    101 |
    105 | {item.name} 106 |
    107 | 108 |
    109 | 113 | ${newPrice(item.price, item.discount)} 114 | 115 | 116 | 119 |
    120 |
    121 |
    122 | ))} 123 |
    124 | ); 125 | } 126 | 127 | export default MarqueeProducts; 128 | -------------------------------------------------------------------------------- /app/_components/marketking/EditProfileForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useTranslate } from "@/app/_hooks/useTranslate"; 3 | import { UploadButton } from "@/app/_lib/uploadthing"; 4 | import { User } from "@prisma/client"; 5 | import Image from "next/image"; 6 | import { useState } from "react"; 7 | import toast from "react-hot-toast"; 8 | import SubmitButton from "../ui/SubmitButton"; 9 | import { useFormState } from "react-dom"; 10 | import { editProfile } from "@/app/_actions/editProfile"; 11 | import ErrorMessage from "../ui/ErrorMessage"; 12 | 13 | function EditProfileForm({ user }: { user: User }) { 14 | const [image, setImage] = useState(user.profileImage); 15 | const [state, formAction] = useFormState(editProfile, {}); 16 | 17 | const { t } = useTranslate(); 18 | 19 | return ( 20 |
    21 | 22 | 29 | 30 |
    31 |
    32 | 35 | 43 |
    44 |
    45 | 48 | 56 |
    57 |
    58 | 59 | 60 |
    61 | { 68 | console.log("uploaded done!"); 69 | 70 | setImage(res[0].url); 71 | toast.success(t("Finish Upload ProfileImage")); 72 | }} 73 | onUploadError={(e) => { 74 | console.log(e); 75 | 76 | toast.error(t("failed Upload ProfileImage")); 77 | }} 78 | /> 79 |
    80 | 81 |
    82 | {`${user.firstName} 89 |
    90 | 91 | 92 | 93 |
      94 |
    • 95 | {state?.firstName && ( 96 | {t(state.firstName)} 97 | )} 98 |
    • 99 |
    • 100 | {state?.lastName && {t(state.lastName)}} 101 |
    • {" "} 102 |
    • {state?.image && {t(state.image)}}
    • 103 |
    104 | 105 | 106 | {t("Edit Profile")} 107 | 108 |
    109 | ); 110 | } 111 | 112 | export default EditProfileForm; 113 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | import { withUt } from "uploadthing/tw"; 3 | 4 | const config = { 5 | darkMode: ["class"], 6 | content: [ 7 | "./pages/**/*.{ts,tsx}", 8 | "./components/**/*.{ts,tsx}", 9 | "./app/**/*.{ts,tsx}", 10 | "./src/**/*.{ts,tsx}", 11 | ], 12 | prefix: "", 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: "2rem", 17 | screens: { 18 | "2xl": "1400px", 19 | }, 20 | }, 21 | screens: { 22 | sm: "640px", 23 | md: "895px", 24 | lg: "1024px", 25 | xl: "1280px", 26 | "2xl": "1536px", 27 | }, 28 | extend: { 29 | colors: { 30 | "main-background": "var(--main-background)", 31 | "sec-background": "var(--second-background)", 32 | "third-background": "var(--third-background)", 33 | "main-text": "var(--main-text)", 34 | "second-text": "var(--second-text)", 35 | "primary-color": "var(--primary-color)", 36 | "primary-bg-color": "var(--primary-bg-color)", 37 | "primary-color-hover": "var(--primary-color-hover)", 38 | "primary--color-hover": "var(--primary-color-hover)", 39 | "hover-button": "var(--hover-button)", 40 | error: "var(--error)", 41 | border: "hsl(var(--border))", 42 | input: "hsl(var(--input))", 43 | ring: "hsl(var(--ring))", 44 | background: "hsl(var(--background))", 45 | foreground: "hsl(var(--foreground))", 46 | primary: { 47 | DEFAULT: "hsl(var(--primary))", 48 | foreground: "hsl(var(--primary-foreground))", 49 | }, 50 | secondary: { 51 | DEFAULT: "hsl(var(--secondary))", 52 | foreground: "hsl(var(--secondary-foreground))", 53 | }, 54 | destructive: { 55 | DEFAULT: "hsl(var(--destructive))", 56 | foreground: "hsl(var(--destructive-foreground))", 57 | }, 58 | muted: { 59 | DEFAULT: "hsl(var(--muted))", 60 | foreground: "hsl(var(--muted-foreground))", 61 | }, 62 | accent: { 63 | DEFAULT: "hsl(var(--accent))", 64 | foreground: "hsl(var(--accent-foreground))", 65 | }, 66 | popover: { 67 | DEFAULT: "hsl(var(--popover))", 68 | foreground: "hsl(var(--popover-foreground))", 69 | }, 70 | card: { 71 | DEFAULT: "hsl(var(--card))", 72 | foreground: "hsl(var(--card-foreground))", 73 | }, 74 | }, 75 | borderRadius: { 76 | lg: "var(--radius)", 77 | md: "calc(var(--radius) - 2px)", 78 | sm: "calc(var(--radius) - 4px)", 79 | }, 80 | keyframes: { 81 | "accordion-down": { 82 | from: { height: "0" }, 83 | to: { height: "var(--radix-accordion-content-height)" }, 84 | }, 85 | "accordion-up": { 86 | from: { height: "var(--radix-accordion-content-height)" }, 87 | to: { height: "0" }, 88 | }, 89 | skeleton: { 90 | "0%": { opacity: "1" }, 91 | "50%": { opacity: "0.4 " }, 92 | "100%": { opacity: "1" }, 93 | }, 94 | rotation: { 95 | from: { transform: "rotate(0deg)" }, 96 | to: { transform: "rotate(360deg)" }, 97 | }, 98 | smooth: { 99 | from: { opacity: "0", transform: "translateY(-20px)" }, 100 | to: { opacity: "1", transform: "translateY(0px)" }, 101 | }, 102 | }, 103 | animation: { 104 | "accordion-down": "accordion-down 0.2s ease-out", 105 | "accordion-up": "accordion-up 0.2s ease-out", 106 | skeleton: "skeleton 1.5s ease-in-out infinite", 107 | smooth: "smooth 1s ease-in-out ", 108 | rotation: "rotation 2s linear infinite ", 109 | }, 110 | }, 111 | }, 112 | corePlugins: { 113 | aspectRatio: false, 114 | }, 115 | } satisfies Config; 116 | 117 | export default withUt(config); 118 | -------------------------------------------------------------------------------- /app/_components/dashboard/BannerTable.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/app/_lib/db"; 2 | import { getTranslate } from "@/app/_utils/helpers"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuLabel, 8 | DropdownMenuSeparator, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | import Image from "next/image"; 12 | import { BsThreeDotsVertical } from "react-icons/bs"; 13 | import IconButton from "../ui/IconButton"; 14 | import ModalImage from "../ui/ModalImage"; 15 | import MyLink from "../ui/MyLink"; 16 | 17 | async function BannerTable() { 18 | const banners = await prisma.banner.findMany({ 19 | orderBy: { 20 | createdAt: "desc", 21 | }, 22 | }); 23 | 24 | const { t, isArabic } = await getTranslate(); 25 | return ( 26 |
    27 |

    {t("Banners")}

    28 |

    {t("Manage Banners")}

    29 | {banners.length <= 0 ? ( 30 |

    {t("No Banner")}

    31 | ) : ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {banners.map((banner) => { 42 | const titleWithCap = 43 | banner.title.charAt(0).toUpperCase() + 44 | banner.title.slice(1).toLowerCase(); 45 | 46 | return ( 47 | 51 | 54 | } 55 | className="w-[95%] h-[100px] relative" 56 | isInTable={true} 57 | modalId={banner.id} 58 | /> 59 | 60 | 92 | 93 | ); 94 | })} 95 | 96 |
    {t("Image")}{t("title")}{t("Actions")}
    {t(titleWithCap)} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | {t("Actions")} 74 | 75 | 76 | 77 | 83 | 86 | {t("Delete")} 87 | 88 | 89 | 90 | 91 |
    97 | )} 98 |
    99 | ); 100 | } 101 | 102 | export default BannerTable; 103 | -------------------------------------------------------------------------------- /app/_components/dashboard/CreateBanner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { createBanner } from "@/app/_actions/createBanner"; 3 | import { useTranslate } from "@/app/_hooks/useTranslate"; 4 | import { UploadButton } from "@/app/_lib/uploadthing"; 5 | import Image from "next/image"; 6 | import { useEffect, useState } from "react"; 7 | import { useFormState } from "react-dom"; 8 | import toast from "react-hot-toast"; 9 | import { IoIosCloseCircle } from "react-icons/io"; 10 | import Button from "../ui/Button"; 11 | import ModalImage from "../ui/ModalImage"; 12 | import SubmitButton from "../ui/SubmitButton"; 13 | import ErrorMessage from "../ui/ErrorMessage"; 14 | import { useRouter } from "next/navigation"; 15 | 16 | function CreateBanner() { 17 | const [image, setImage] = useState([]); 18 | const [state, formAction] = useFormState(createBanner, {}); 19 | const { t, isArabic } = useTranslate(); 20 | const router = useRouter(); 21 | 22 | useEffect(() => { 23 | if (state?.success) { 24 | toast.success(t("create success banner")); 25 | router.push(isArabic ? "/ar/dashboard/banner" : "/en/dashboard/banner"); 26 | } else if (state?.success === false) { 27 | toast.error(t("create failed banner")); 28 | } 29 | }, [state?.success, t, router, isArabic]); 30 | 31 | const handleDeleteImage = (index: number) => { 32 | setImage(image.filter((_: any, i) => i !== index)); 33 | }; 34 | return ( 35 |
    36 | 37 | 44 | {state?.banner && {t(state.banner)}} 45 | 46 | {image.length < 1 && ( 47 |
    48 | { 55 | console.log("uploaded done!"); 56 | 57 | setImage((prevImage) => [...prevImage, ...res.map((r) => r.url)]); 58 | toast.success(t("Finish Upload Banner")); 59 | }} 60 | onUploadError={(e) => { 61 | console.log(e); 62 | 63 | toast.error(t("failed Upload Banner")); 64 | }} 65 | /> 66 |
    67 | )} 68 | 69 | 70 | {image.length > 0 && ( 71 | <> 72 |

    {t("Banner Image")}

    73 |
      74 | {image.map((image, index) => ( 75 | 78 | 90 | Product Img 96 | 97 | } 98 | className="relative min-h-[300px] max-w-[200px] rounded-lg " 99 | modalId={`${index}`} 100 | key={index} 101 | /> 102 | ))} 103 |
    104 | 105 | )} 106 | {state?.image && {t(state.image)}} 107 | 108 | 109 | {t("Create Banner")} 110 | 111 |
    112 | ); 113 | } 114 | 115 | export default CreateBanner; 116 | -------------------------------------------------------------------------------- /app/[locale]/(marketking)/customer-order/page.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@/app/_components/ui/Button"; 2 | import IconButton from "@/app/_components/ui/IconButton"; 3 | import MyLink from "@/app/_components/ui/MyLink"; 4 | import PrintButton from "@/app/_components/ui/PrintButton"; 5 | import prisma from "@/app/_lib/db"; 6 | import { getUser } from "@/app/_utils/getUser"; 7 | import { getTranslate } from "@/app/_utils/helpers"; 8 | import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; 9 | import { IoMdArrowRoundBack, IoMdArrowRoundForward } from "react-icons/io"; 10 | import { PiCurrencyCircleDollarFill } from "react-icons/pi"; 11 | 12 | export async function generateMetadata({ 13 | params: { locale }, 14 | }: { 15 | params: { locale: string }; 16 | }) { 17 | const t = await getTranslations({ locale, namespace: "metadata" }); 18 | 19 | return { 20 | title: `${t("Customer Order")}`, 21 | }; 22 | } 23 | 24 | async function CustomerOrderPage({ 25 | params: { locale }, 26 | }: { 27 | params: { locale: string }; 28 | }) { 29 | unstable_setRequestLocale(locale); 30 | const user = await getUser(); 31 | 32 | const customerOrder = await prisma.order.findMany({ 33 | where: { userId: user?.id ?? "" }, 34 | }); 35 | 36 | const { isArabic, t } = await getTranslate(); 37 | return ( 38 |
    39 |
    40 | 41 | 42 | {isArabic ? : } 43 | 44 | 45 |

    {t("Customer Order")}

    46 |
    47 | 48 | {customerOrder && customerOrder.length > 0 ? ( 49 | <> 50 |
      51 | {customerOrder.map((order) => ( 52 |
    • 56 |
      57 |
      58 | 59 | 60 |
      61 | {" "} 62 | {t("Bill")}#{order.amount} 63 |
      64 |
      65 | 66 | 67 | {t(order.status)} 68 | 69 | 70 |
      71 |
      72 | {t("Date")} 73 |

      74 | {Intl.DateTimeFormat( 75 | isArabic ? "ar-EG" : "en-US", 76 | {} 77 | ).format(order.createdAt)} 78 |

      79 |
      {" "} 80 |
      81 | {t("Total Price")} 82 |

      83 | {new Intl.NumberFormat("en-Us").format( 84 | order.amount / 100 85 | )} 86 |

      87 |
      88 |
      89 | 90 |
      91 | 92 |
      93 |
      94 | {t("Status")} 95 |

      {t(order.status)}

      96 |
      {" "} 97 |
      98 | {t("Reference")} 99 |

      {order.id.slice(0, 5)}

      100 |
      101 |
      102 |
      103 |
    • 104 | ))} 105 |
    106 | 107 | 108 | ) : ( 109 |
    110 |

    {t("No Orders Found")}

    111 | 115 | {t("Shop Now")}{" "} 116 | {isArabic ? "←" : "→"} 117 | 118 |
    119 | )} 120 |
    121 | ); 122 | } 123 | 124 | export default CustomerOrderPage; 125 | -------------------------------------------------------------------------------- /app/_components/marketking/WhishList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import IconButton from "../ui/IconButton"; 3 | import { FaHeart } from "react-icons/fa"; 4 | import { IUserIncludeFavorites } from "@/app/_utils/types"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { useTranslate } from "@/app/_hooks/useTranslate"; 14 | import MyLink from "../ui/MyLink"; 15 | import Image from "next/image"; 16 | import Button from "../ui/Button"; 17 | import { newPrice, oldPrice } from "@/app/_utils/helpers"; 18 | 19 | import { FaHeartBroken } from "react-icons/fa"; 20 | 21 | async function WhishList({ user }: { user: IUserIncludeFavorites }) { 22 | const whishCount = user?.favoriteProducts.length || 0; 23 | const { t, isArabic } = useTranslate(); 24 | return ( 25 | 26 | 27 | 28 | 29 | {whishCount > 0 && ( 30 | 31 | {whishCount} 32 | 33 | )} 34 | 35 | 36 | 40 | 41 | {t("WhishList")} 42 | 43 | 44 | 45 | {user?.favoriteProducts?.length ? ( 46 | <> 47 |
      48 | {user.favoriteProducts.map((fav) => ( 49 |
    • 50 | 56 | 60 |
      61 | {fav.product.name} 67 |
      68 |

      72 | {fav.product.name} 73 |

      74 | 75 | ${newPrice(fav.product.price, fav.product.discount)} 76 | {oldPrice(fav.product.price, fav.product.discount) && ( 77 | 78 | ($ 79 | {oldPrice(fav.product.price, fav.product.discount)}) 80 | 81 | )} 82 | {fav.product.isFeatured && ( 83 | 84 | {t("Featured")} 85 | 86 | )} 87 | 88 |
      89 |
      90 |
    • 91 | ))} 92 |
    93 | 97 | 102 | 103 | 104 | ) : ( 105 | <> 106 | 107 |

    {t("no favorite")}

    108 | 109 | )} 110 |
    111 |
    112 | ); 113 | } 114 | 115 | export default WhishList; 116 | --------------------------------------------------------------------------------