From d84eb88a42d66ab8c4aa6224a93eb2bf3a823f3f Mon Sep 17 00:00:00 2001 From: Shivansh Date: Mon, 4 Nov 2024 02:03:24 +0530 Subject: [PATCH 1/3] global context: multistep form, necessary file structure and protection with middleware for seller and structure of multistep form --- .../app/(routes)/auth/components/otp-form.tsx | 25 ++++- .../auth/components/seller/login-form.tsx | 2 +- .../[sellerId]/components/formControl.tsx | 58 ++++++++++ .../[sellerId]/components/listingDetails.tsx | 85 +++++++++++++++ .../app/seller/[sellerId]/components/main.tsx | 103 ++++++++++++++++++ .../[sellerId]/components/setupListing.tsx | 31 ++++++ .../seller/[sellerId]/components/sidebar.tsx | 83 ++++++++++++++ .../components/sidebarConstants.tsx | 18 +++ scruter-nextjs/app/seller/[sellerId]/page.tsx | 41 +++++++ scruter-nextjs/app/seller/layout.tsx | 21 ++++ .../context/GlobalListingProvider.tsx | 99 +++++++++++++++++ scruter-nextjs/lib/providers.tsx | 3 +- scruter-nextjs/middleware.ts | 31 ++++++ 13 files changed, 594 insertions(+), 6 deletions(-) create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/formControl.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/listingDetails.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/main.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/setupListing.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/sidebar.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/sidebarConstants.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/page.tsx create mode 100644 scruter-nextjs/app/seller/layout.tsx create mode 100644 scruter-nextjs/context/GlobalListingProvider.tsx create mode 100644 scruter-nextjs/middleware.ts diff --git a/scruter-nextjs/app/(routes)/auth/components/otp-form.tsx b/scruter-nextjs/app/(routes)/auth/components/otp-form.tsx index c2d2c120..c896145b 100644 --- a/scruter-nextjs/app/(routes)/auth/components/otp-form.tsx +++ b/scruter-nextjs/app/(routes)/auth/components/otp-form.tsx @@ -19,7 +19,7 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Toaster, toast } from 'react-hot-toast'; -import { signIn } from 'next-auth/react'; +import { signIn, useSession } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { ChevronLeftCircleIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; // use 'next/navigation' for Next.js 13 App Router @@ -44,7 +44,9 @@ export function OtpForm({ ...props }: UserAuthFormProps) { const [isLoading, setIsLoading] = React.useState(false); - const router = useRouter(); // Ensure this is declared outside of useEffect + const [redirectUrl, setRedirectUrl] = React.useState(null); + const { data: session } = useSession(); // Access the session + const router = useRouter(); const form = useForm>({ resolver: zodResolver(FormSchema), @@ -64,14 +66,29 @@ export function OtpForm({ if (!result?.ok) { toast.error('Invalid email or OTP'); } else { - toast.success(`Welcome!`); + toast.success('Welcome!'); + + // Set redirect URL based on user role if (roleType === 'user') { - router.push('/'); // Redirect to home for user + setRedirectUrl('/'); // Redirect to home for user + } else if (roleType === 'seller') { + setRedirectUrl(`/seller/${email}`); // Temporarily set to seller's page } } setIsLoading(false); } + React.useEffect(() => { + if (redirectUrl && session) { + const sellerId = session.user?.id; // Retrieve the sellerId from session + if (sellerId) { + router.push(`/seller/${sellerId}`); // Redirect to seller's page + } else if (redirectUrl === '/') { + router.push(redirectUrl); // Redirect to home for user + } + } + }, [redirectUrl, session, router]); + return (
diff --git a/scruter-nextjs/app/(routes)/auth/components/seller/login-form.tsx b/scruter-nextjs/app/(routes)/auth/components/seller/login-form.tsx index 3b1fb7b6..c1e8344f 100644 --- a/scruter-nextjs/app/(routes)/auth/components/seller/login-form.tsx +++ b/scruter-nextjs/app/(routes)/auth/components/seller/login-form.tsx @@ -87,7 +87,7 @@ export function SellerLoginForm({ )} {otpOpen && ( - + )}
); diff --git a/scruter-nextjs/app/seller/[sellerId]/components/formControl.tsx b/scruter-nextjs/app/seller/[sellerId]/components/formControl.tsx new file mode 100644 index 00000000..44842118 --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/formControl.tsx @@ -0,0 +1,58 @@ +import React from "react"; + +// Define the type for the props +interface FormControlProps { + type: string; + id: string; + label: string; + placeholder: string; + value: string; + onchange: (e: React.ChangeEvent) => void; + valid: boolean; +} + +const FormControl: React.FC = ({ + type, + id, + label, + placeholder, + value, + onchange, + valid, +}) => { + return ( +
+
+
+ +
+ {valid ? null : ( +
+ +
+ )} +
+ +
+ ); +}; + +export default FormControl; diff --git a/scruter-nextjs/app/seller/[sellerId]/components/listingDetails.tsx b/scruter-nextjs/app/seller/[sellerId]/components/listingDetails.tsx new file mode 100644 index 00000000..2f3a5378 --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/listingDetails.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import FormControl from "./formControl"; +import { useGlobalListing } from "@/context/GlobalListingProvider"; + +const ListingDetails = () => { + const { + listingName, + setListingName, + listingPrice, + setListingPrice, + listingDescription, + setListingDescription, + listingCategory, + setListingCategory, + + validListingName, + setValidListingName, + validListingPrice, + setValidListingPrice, + validListingDescription, + setValidListingDescription, + validListingCategory, + setValidListingCategory, + } = useGlobalListing() + + const setListingNameLogic = (e: React.ChangeEvent) => { + setListingName(e.target.value); + setValidListingName(e.target.value.length >= 1); + }; + + const setListingDescriptionLogic = (e: React.ChangeEvent) => { + setListingDescription(e.target.value); + setValidListingDescription(e.target.value.length >= 1); + }; + + const setListingPriceLogic = (e: React.ChangeEvent) => { + setListingPrice(Number(e.target.value)); + setValidListingPrice( + e.target.value.length >= 1 + ); + }; + + return ( +
+

+ Listing Info +

+

+ Please provide your listing name, description and price +

+ +
+ + + +
+
+ ); +}; + +export default ListingDetails; diff --git a/scruter-nextjs/app/seller/[sellerId]/components/main.tsx b/scruter-nextjs/app/seller/[sellerId]/components/main.tsx new file mode 100644 index 00000000..a21bda7b --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/main.tsx @@ -0,0 +1,103 @@ +import { useGlobalListing } from "@/context/GlobalListingProvider"; +import { MouseEventHandler } from "react"; +import ListingDetails from "./listingDetails"; + +const Main = ({ sellerId }: { sellerId: string }) => { + const { + listingName,listingCategory,listingPrice,listingDescription, + setValidListingName,setValidListingCategory,setValidListingPrice,setValidListingDescription, + currentStep, setCurrentStep,formCompleted, setFormCompleted ,completed, setCompleted } = useGlobalListing(); + + currentStep === 1 ? setCompleted(false) : setCompleted(true); + + const nextStep: MouseEventHandler = (e) => { + e.preventDefault(); + + let allValid = true; + + // Validate each field and update validity states + if (listingName.trim().length < 1) { + setValidListingName(false); + allValid = false; + } else { + setValidListingName(true); + } + + + // Move to the next step only if all fields are valid + if (allValid) { + setCurrentStep(currentStep + 1); + } + }; + + const goBack: MouseEventHandler = (e) => { + e.preventDefault(); + setCurrentStep(currentStep - 1); + }; + + + return ( +
+
+ {/* {currentStep} */} + {currentStep === 1 && } + + {!formCompleted && ( +
+
+
+ {completed && ( + + )} +
+
+ +
+
+
+ )} + + {!formCompleted && ( +
+
+
+ {completed && ( + + )} +
+
+ +
+
+
+ )} + +
+ ); +}; + +export default Main; diff --git a/scruter-nextjs/app/seller/[sellerId]/components/setupListing.tsx b/scruter-nextjs/app/seller/[sellerId]/components/setupListing.tsx new file mode 100644 index 00000000..d6348eb8 --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/setupListing.tsx @@ -0,0 +1,31 @@ +"use client" +import { Sidebar } from "lucide-react"; +import Image from "next/image"; +import React from "react"; +import Main from "./main"; + +const SetUpListing= ({params}:{params:{sellerId:string}}) => { + + const {sellerId}=params + + return ( + <> + +
+ {/*
+
Store Setup Guide
+ storeSetuppage +
*/} + +
+ +
+
+
+ + ); +}; + +export default SetUpListing; diff --git a/scruter-nextjs/app/seller/[sellerId]/components/sidebar.tsx b/scruter-nextjs/app/seller/[sellerId]/components/sidebar.tsx new file mode 100644 index 00000000..2213a12a --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/sidebar.tsx @@ -0,0 +1,83 @@ +// import { useGlobalStore } from "@/context/storeContext"; +import { useGlobalListing } from "@/context/GlobalListingProvider"; +import { data } from "./sidebarConstants"; + +const Sidebar = () => { + const { + listingName,listingCategory,listingPrice,listingDescription, + setValidListingName,setValidListingCategory,setValidListingPrice,setValidListingDescription, + currentStep, setCurrentStep, setFormCompleted + } = useGlobalListing() + + const changeStep = (id:number) => { + let allValid = true; + + // Validate each field and update validity states + if (listingName.trim().length < 1) { + setValidListingName(false); + allValid = false; + } else { + setValidListingName(true); + } + + if (listingDescription.trim().length < 1) { + setValidListingDescription(false); + allValid = false; + } else { + setValidListingDescription(true); + } + + if (listingPrice< 1) { + setValidListingPrice(false); + allValid = false; + } else { + setValidListingPrice(true); + } + + if (listingCategory.trim().length < 1) { + setValidListingCategory(false); + allValid = false; + } else { + setValidListingCategory(true); + } + + // Move to the next step only if all fields are valid + if (allValid) { + setCurrentStep(id); + } + + // Reset the form completion status + setFormCompleted (false); + }; + + + return ( + <> + + + ); +}; + +export default Sidebar; diff --git a/scruter-nextjs/app/seller/[sellerId]/components/sidebarConstants.tsx b/scruter-nextjs/app/seller/[sellerId]/components/sidebarConstants.tsx new file mode 100644 index 00000000..18acb253 --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/components/sidebarConstants.tsx @@ -0,0 +1,18 @@ +export const data = [ + { + id: 1, + step: "step 1", + title: "your store info", + }, + { + id: 2, + step: "step 2", + title: "your cover URL", + }, + { + id: 3, + step: "step 3", + title: "Store Address", + } + ]; + \ No newline at end of file diff --git a/scruter-nextjs/app/seller/[sellerId]/page.tsx b/scruter-nextjs/app/seller/[sellerId]/page.tsx new file mode 100644 index 00000000..f7ff7932 --- /dev/null +++ b/scruter-nextjs/app/seller/[sellerId]/page.tsx @@ -0,0 +1,41 @@ +import prismadb from "@/lib/prismadb"; +import { Listing } from "@prisma/client"; +import SetUpListing from "./components/setupListing"; +// import SetUpGuide from "./components/setupListing"; +// import SellerDashboard from "./components/sellerDashboard"; + +interface SellerPageProps{ + params:{ + sellerId:string, + }, +}; + +const SellerPage:React.FC = async({params}) => { + + let Listings: Listing[] | null = []; + + const {sellerId} = await params + try { + Listings = await prismadb.listing.findMany({ + where: { + SellerId:sellerId, + }, + }); + } catch (err) { + console.error( + "Error fetching Listing", + err instanceof Error ? err.message : err + ); + } + + // if (Listings.length){ + // return + // } + + // else{ + return + // } + +} + +export default SellerPage; \ No newline at end of file diff --git a/scruter-nextjs/app/seller/layout.tsx b/scruter-nextjs/app/seller/layout.tsx new file mode 100644 index 00000000..1cc88349 --- /dev/null +++ b/scruter-nextjs/app/seller/layout.tsx @@ -0,0 +1,21 @@ +import { Providers } from "@/lib/providers" + +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + {children} + + + + ) +} diff --git a/scruter-nextjs/context/GlobalListingProvider.tsx b/scruter-nextjs/context/GlobalListingProvider.tsx new file mode 100644 index 00000000..35d16069 --- /dev/null +++ b/scruter-nextjs/context/GlobalListingProvider.tsx @@ -0,0 +1,99 @@ +"use client" +import { createContext, ReactNode, useContext, useState } from "react"; + +interface GlobalContextType { + currentStep: number; + setCurrentStep: (step: number) => void; + completed: boolean; + setCompleted: (completed: boolean) => void; + + listingName: string; + setListingName: (name: string) => void; + listingPrice: number; + setListingPrice: (price: number) => void; + listingDescription: string; + setListingDescription: (description: string) => void; + listingCategory: string; + setListingCategory: (category: string) => void; + + validListingName: boolean; + setValidListingName: (name: boolean) => void; + validListingPrice: boolean; + setValidListingPrice: (price: boolean) => void; + validListingDescription: boolean; + setValidListingDescription: (description: boolean) => void; + validListingCategory: boolean; + setValidListingCategory: (category: boolean) => void; + + checkedBox: boolean; + setCheckedBox: (checkedBox: boolean) => void; + + formCompleted: boolean; + setFormCompleted: (completed: boolean) => void; +} + +export const GlobalListingContext = createContext(undefined); + +export const useGlobalListing = () => { + const context = useContext(GlobalListingContext); + if (!context) { + throw new Error("useGlobalListing must be used within a GlobalListingProvider"); + } + return context; +}; + +export const GlobalListingProvider = ({ children }: { children: ReactNode }) => { + const [currentStep, setCurrentStep] = useState(1); + const [completed, setCompleted] = useState(false); + + const [listingName, setListingName] = useState(""); + const [listingPrice, setListingPrice] = useState(0); + const [listingDescription, setListingDescription] = useState(""); + const [listingCategory, setListingCategory] = useState("Housing"); + + const [validListingName, setValidListingName] = useState(false); + const [validListingPrice, setValidListingPrice] = useState(false); + const [validListingDescription, setValidListingDescription] = useState(false); + const [validListingCategory, setValidListingCategory] = useState(false); + + const [checkedBox, setCheckedBox] = useState(false); + const [formCompleted, setFormCompleted] = useState(false); + + + return ( + + {children} + + ); +}; diff --git a/scruter-nextjs/lib/providers.tsx b/scruter-nextjs/lib/providers.tsx index 595e3cae..bc0965dc 100644 --- a/scruter-nextjs/lib/providers.tsx +++ b/scruter-nextjs/lib/providers.tsx @@ -1,7 +1,8 @@ 'use client'; import React from 'react'; import { SessionProvider } from 'next-auth/react'; +import { GlobalListingProvider } from '@/context/GlobalListingProvider'; export const Providers = ({ children }: { children: React.ReactNode }) => { - return {children}; + return {children} }; diff --git a/scruter-nextjs/middleware.ts b/scruter-nextjs/middleware.ts new file mode 100644 index 00000000..c4bdb980 --- /dev/null +++ b/scruter-nextjs/middleware.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { getToken } from 'next-auth/jwt'; + +export async function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Check if the pathname is in the (seller) route and contains a sellerId + // console.log("control") + const isSellerRoute = pathname.startsWith('/seller'); + const sellerIdMatch = pathname.match(/\/seller\/([^/]+)/); + + if (isSellerRoute) { + // Get the token from the request + const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET }); + + // console.log(token); + // Redirect to home if the token doesn't exist, the role is not 'seller', or the sellerId is missing + if (!token || token.role !== 'seller' ||! token.uid) { + return NextResponse.redirect(new URL('/', request.url)); + } + } + + // Continue to the page if the user is authorized + return NextResponse.next(); +} + +// Specify the paths where this middleware applies +export const config = { + matcher: ['/seller/:path*'], +}; From 13c09e0829be9ff4808da1837861cff64ab82a1b Mon Sep 17 00:00:00 2001 From: Shivansh Date: Mon, 4 Nov 2024 18:18:48 +0530 Subject: [PATCH 2/3] made the multistep form functional with necessary logic, added pretty UI to the form, added seller navbar with auth options --- scruter-nextjs/app/(routes)/layout.tsx | 17 +--- scruter-nextjs/app/layout.tsx | 26 ++++++ .../[sellerId]/components/formControl.tsx | 2 +- .../[sellerId]/components/listingDetails.tsx | 10 +- .../app/seller/[sellerId]/components/main.tsx | 93 +++++++++++++------ .../[sellerId]/components/selectCategory.tsx | 46 +++++++++ .../[sellerId]/components/setupListing.tsx | 20 ++-- .../seller/[sellerId]/components/sidebar.tsx | 4 +- .../components/sidebarConstants.tsx | 7 +- scruter-nextjs/app/seller/[sellerId]/page.tsx | 18 ++-- scruter-nextjs/app/seller/layout.tsx | 25 ++--- .../components/common/sellerNavBar.tsx | 28 ++++++ .../components/ui/dropdown-menu.tsx | 92 +++++++++--------- .../context/GlobalListingProvider.tsx | 93 +++++++++++++++++-- scruter-nextjs/package-lock.json | 17 ++++ scruter-nextjs/package.json | 1 + scruter-nextjs/public/listingGuide.svg | 1 + scruter-nextjs/tailwind.config.ts | 3 + 18 files changed, 354 insertions(+), 149 deletions(-) create mode 100644 scruter-nextjs/app/layout.tsx create mode 100644 scruter-nextjs/app/seller/[sellerId]/components/selectCategory.tsx create mode 100644 scruter-nextjs/components/common/sellerNavBar.tsx create mode 100644 scruter-nextjs/public/listingGuide.svg diff --git a/scruter-nextjs/app/(routes)/layout.tsx b/scruter-nextjs/app/(routes)/layout.tsx index 8d5b5c76..20d8d543 100644 --- a/scruter-nextjs/app/(routes)/layout.tsx +++ b/scruter-nextjs/app/(routes)/layout.tsx @@ -1,14 +1,6 @@ -import type { Metadata } from 'next'; -import '../globals.css'; import Navbar from '@/components/common/navbar'; import Footer from '@/components/common/footer'; -import CustomCursor from '@/components/ui/CustomCursor'; -import { Providers } from '@/lib/providers'; -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -}; export default function RootLayout({ children, @@ -16,15 +8,10 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - - - + <> {children}