From a592a9ddc2c7e3c5984824c1c5f3737e10d0c5e3 Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sun, 10 Nov 2024 17:51:59 +0530 Subject: [PATCH 1/2] added otp form and backend action , admin auth is now fully functional --- alimento-nextjs/actions/Admin/admin-login.tsx | 30 ++++ alimento-nextjs/app/admin/[adminId]/page.tsx | 2 +- .../app/admin/auth/components/login-form.tsx | 112 ++++++++------ .../app/admin/auth/components/otp-form.tsx | 146 ++++++++++++++++++ 4 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 alimento-nextjs/actions/Admin/admin-login.tsx create mode 100644 alimento-nextjs/app/admin/auth/components/otp-form.tsx diff --git a/alimento-nextjs/actions/Admin/admin-login.tsx b/alimento-nextjs/actions/Admin/admin-login.tsx new file mode 100644 index 0000000..6bc9c48 --- /dev/null +++ b/alimento-nextjs/actions/Admin/admin-login.tsx @@ -0,0 +1,30 @@ +'use server'; +import { generateAndSendOTP } from '@/lib/auth'; +import prismadb from '@/lib/prismadb'; +import { Prisma, Admin } from '@prisma/client'; + +export async function AdminVerify({ + email, +}: { + email: string; +}): Promise<{ success: boolean; error?: string; data?: Admin }> { + const exitingAdmin = await prismadb.admin.findUnique({ + where: { + email: email, + }, + }); + + if (!exitingAdmin) { + return { + success: false, + error: 'Admin does not exists', + }; + } + const resp = await generateAndSendOTP(email, 'admin'); + + if (!resp) { + return { success: false, error: 'Error occured in sending otp' }; + } + + return { success: true }; +} diff --git a/alimento-nextjs/app/admin/[adminId]/page.tsx b/alimento-nextjs/app/admin/[adminId]/page.tsx index 4c21631..9231368 100644 --- a/alimento-nextjs/app/admin/[adminId]/page.tsx +++ b/alimento-nextjs/app/admin/[adminId]/page.tsx @@ -1,6 +1,6 @@ const AdminPage = () => { return ( -
hi from admin
+
hiiii
); } diff --git a/alimento-nextjs/app/admin/auth/components/login-form.tsx b/alimento-nextjs/app/admin/auth/components/login-form.tsx index 0dd9209..7e9f80c 100644 --- a/alimento-nextjs/app/admin/auth/components/login-form.tsx +++ b/alimento-nextjs/app/admin/auth/components/login-form.tsx @@ -1,15 +1,17 @@ -"use client"; +'use client'; -import * as React from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Spinner } from "@/components/ui/spinner"; -import { cn } from "@/lib/utils"; -import toast, { Toaster } from "react-hot-toast"; +import * as React from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Spinner } from '@/components/ui/spinner'; +import { OtpForm } from './otp-form'; +import { AdminVerify } from '@/actions/Admin/admin-login'; +import { cn } from '@/lib/utils'; +import toast, { Toaster } from 'react-hot-toast'; interface AdminAuthFormProps extends React.HTMLAttributes { - authType: "signup" | "login"; + authType: 'signup' | 'login'; } export function AdminLoginForm({ @@ -18,52 +20,72 @@ export function AdminLoginForm({ ...props }: AdminAuthFormProps) { const [isLoading, setIsLoading] = React.useState(false); - const [email, setEmail] = React.useState(""); + const [email, setEmail] = React.useState(''); + const [otpOpen, setOtpOpen] = React.useState(false); async function onSubmit(event: React.SyntheticEvent) { event.preventDefault(); setIsLoading(true); - setIsLoading(false); + if (!email) { + toast.error('Please enter an email.'); + setIsLoading(false); + return; + } + + try { + const res = await AdminVerify({ email }); + + if (!res.success) { + toast.error(res.error || 'Error verifying admin.'); + setIsLoading(false); + return; + } + + toast.success('Admin verified! Please enter the OTP.'); + setOtpOpen(true); + } catch (error) { + toast.error('Verification failed, please try again.'); + } finally { + setIsLoading(false); + } } return ( -
+
- -
-
-
- - setEmail(e.target.value)} - /> + {!otpOpen && ( + +
+
+ + setEmail(e.target.value)} + /> +
+
- -
- + + )} + {otpOpen && }
); } diff --git a/alimento-nextjs/app/admin/auth/components/otp-form.tsx b/alimento-nextjs/app/admin/auth/components/otp-form.tsx new file mode 100644 index 0000000..3eca5c0 --- /dev/null +++ b/alimento-nextjs/app/admin/auth/components/otp-form.tsx @@ -0,0 +1,146 @@ +'use client'; + +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from '@/components/ui/input-otp'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +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, 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 + +interface UserAuthFormProps extends React.HTMLAttributes { + roleType: 'user' | 'seller' | 'admin'; + email: string; + setOtpOpen: (otp: boolean) => void; +} + +const FormSchema = z.object({ + pin: z + .string() + .length(6, { message: 'Your one-time password must be 6 characters.' }), +}); + +export function OtpForm({ + className, + roleType, + email, + setOtpOpen, + ...props +}: UserAuthFormProps) { + const [isLoading, setIsLoading] = React.useState(false); + const [redirectUrl, setRedirectUrl] = React.useState(null); + const { data: session } = useSession(); // Access the session + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { pin: '' }, + }); + + async function onOTPSubmit(data: z.infer) { + setIsLoading(true); + + console.log(email,data,roleType) + + + const result = await signIn('credentials', { + email, + otp: data.pin, + role: roleType, + redirect: false, + }); + console.log(result) + if (!result?.ok) { + toast.error('Invalid email or OTP'); + } else { + toast.success('Welcome!'); + + // Set redirect URL based on user role + if (roleType === 'user') { + setRedirectUrl('/'); // Redirect to home for user + } else if (roleType === 'seller') { + setRedirectUrl(`/seller/${email}`); // Temporarily set to seller's page + } + else{ + setRedirectUrl(`/admin/${email}`); + } + } + setIsLoading(false); + } + + React.useEffect(() => { + if (redirectUrl && session) { + const userId = session.user?.id; + const userRole = session.user?.role; + + if (userRole === "seller" && userId) { + router.push(`/seller/${userId}`); // Redirect to seller's page + } else if (userRole === "user") { + router.push(redirectUrl); // Redirect to the specified URL or home for a user + } else if (userRole === "admin") { + router.push(`/admin/${userId}`); // Redirect to admin dashboard + } + } + }, [redirectUrl, session, router]); + + + return ( +
+ +
+ + ( + + + setOtpOpen(false)} + className="h-5 w-5" + /> + One-Time Password + + + + + + + + + + + + + + + + )} + /> + + + +
+ ); +} From c4a466ad28f32672eec9b067790001cfc689cdf6 Mon Sep 17 00:00:00 2001 From: Shivansh Date: Sun, 10 Nov 2024 18:13:31 +0530 Subject: [PATCH 2/2] added dashboard for admin --- .../admin/[adminId]/components/overview.tsx | 36 +++++ .../[adminId]/components/recentSales.tsx | 82 +++++++++++ alimento-nextjs/app/admin/[adminId]/page.tsx | 131 +++++++++++++++++- alimento-nextjs/components/ui/avatar.tsx | 50 +++++++ alimento-nextjs/package.json | 2 + 5 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 alimento-nextjs/app/admin/[adminId]/components/overview.tsx create mode 100644 alimento-nextjs/app/admin/[adminId]/components/recentSales.tsx create mode 100644 alimento-nextjs/components/ui/avatar.tsx diff --git a/alimento-nextjs/app/admin/[adminId]/components/overview.tsx b/alimento-nextjs/app/admin/[adminId]/components/overview.tsx new file mode 100644 index 0000000..fd79888 --- /dev/null +++ b/alimento-nextjs/app/admin/[adminId]/components/overview.tsx @@ -0,0 +1,36 @@ +"use client"; +import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts"; + +interface graphData{ + name:string, + total:number +} +interface OverviewProps { + data:graphData[] +} + +const Overview: React.FC = ({ data }) => { + return ( + + + + `₹${value}`} + /> + + + + ); +}; + +export default Overview; diff --git a/alimento-nextjs/app/admin/[adminId]/components/recentSales.tsx b/alimento-nextjs/app/admin/[adminId]/components/recentSales.tsx new file mode 100644 index 0000000..6d2a2fb --- /dev/null +++ b/alimento-nextjs/app/admin/[adminId]/components/recentSales.tsx @@ -0,0 +1,82 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; + +interface Listing { + name: string; + email: string; + avatarSrc: string; + avatarText: string; + amount: number; // Price for a dish or restaurant + listingType: string; // Restaurant or Dish type +} + +const listingsData: Listing[] = [ + { + name: "Tandoori Delights", + email: "tandoori.delights@scruter.com", + avatarSrc: "/avatars/tandoori.png", + avatarText: "TD", + amount: 300.0, + listingType: "Restaurant - Indian Cuisine", + }, + { + name: "Pasta Mania", + email: "pasta.mania@scruter.com", + avatarSrc: "/avatars/pasta.png", + avatarText: "PM", + amount: 250.0, + listingType: "Restaurant - Italian Cuisine", + }, + { + name: "Sushi World", + email: "sushi.world@scruter.com", + avatarSrc: "/avatars/sushi.png", + avatarText: "SW", + amount: 450.0, + listingType: "Restaurant - Japanese Cuisine", + }, + { + name: "Burger Shack", + email: "burger.shack@scruter.com", + avatarSrc: "/avatars/burger.png", + avatarText: "BS", + amount: 120.0, + listingType: "Restaurant - Fast Food", + }, + { + name: "Vegan Bites", + email: "vegan.bites@scruter.com", + avatarSrc: "/avatars/vegan.png", + avatarText: "VB", + amount: 180.0, + listingType: "Restaurant - Vegan", + }, +]; + +export function RecentSales() { + return ( +
+ {listingsData.map((listing, index) => ( +
+ + + {listing.avatarText} + +
+
+

{listing.name}

+

{listing.email}

+
+
+
+ ₹{listing.amount.toLocaleString()} +
+

+ {listing.listingType} +

+
+
+
+ ))} +
+ ); +} diff --git a/alimento-nextjs/app/admin/[adminId]/page.tsx b/alimento-nextjs/app/admin/[adminId]/page.tsx index 9231368..fcb55a1 100644 --- a/alimento-nextjs/app/admin/[adminId]/page.tsx +++ b/alimento-nextjs/app/admin/[adminId]/page.tsx @@ -1,7 +1,126 @@ -const AdminPage = () => { - return ( -
hiiii
- ); + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Heading } from "@/components/ui/heading"; +import { Separator } from "@/components/ui/separator"; +import { Activity, CreditCard, IndianRupee, Package, Store, User } from "lucide-react"; +import { RecentSales } from "./components/recentSales"; +import Overview from "./components/overview"; + +interface AdminPageProps { + params: { storeId: string }; } - -export default AdminPage; \ No newline at end of file + +const mockGraphData = [ + { name: "Jan", total: 5000 }, + { name: "Feb", total: 4500 }, + { name: "Mar", total: 6000 }, + { name: "Apr", total: 5500 }, + { name: "May", total: 7000 }, + { name: "Jun", total: 8000 }, + { name: "Jul", total: 7500 }, + { name: "Aug", total: 7000 }, + { name: "Sep", total: 8500 }, + { name: "Oct", total: 9000 }, + { name: "Nov", total: 9500 }, + { name: "Dec", total: 10000 }, + ]; + +const AdminPage: React.FC = async () => { + + return ( +
+
+ + +
+ + + + Total Revenue + + + + + +
+ ₹ 2,10,789 +
+
+
+ + + Sales + + + + + +577 + + + + + + Sellers Associated + + + + + + 43 + + + + + + Customer Associated + + + + + + 1450 + + + + + + Website Visits + + + + + + 100,223 + + + + + + Successful Orders + + + + + + 20455+ + + +
+
+ + Overview + + + + +
+ + +
+
+
+
+ ); +}; + +export default AdminPage; diff --git a/alimento-nextjs/components/ui/avatar.tsx b/alimento-nextjs/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/alimento-nextjs/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/alimento-nextjs/package.json b/alimento-nextjs/package.json index 33b009d..dcad6cb 100644 --- a/alimento-nextjs/package.json +++ b/alimento-nextjs/package.json @@ -11,6 +11,7 @@ "dependencies": { "@hookform/resolvers": "^3.9.1", "@prisma/client": "^5.21.1", + "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.1", "@radix-ui/react-label": "^2.1.0", @@ -35,6 +36,7 @@ "react-hot-toast": "^2.4.1", "react-simple-chatbot": "^0.6.1", "react-toastify": "^10.0.6", + "recharts": "^2.13.3", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8"