From 0e186540fd94c4c87416efc4e8623a87a69403c6 Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Thu, 23 Oct 2025 21:23:07 +0530 Subject: [PATCH 01/14] feat: enhance user profile management and fix some state issues - Introduced a memoized ProfileManagement component to optimize rendering. - Fix re-renders on tabs switching - Improved form handling for user profile updates, including name, email, website, and GitHub. - Added `User Not Logged In` Contents. - Refactored state management for better clarity and performance. --- src/pages/Dashboard.tsx | 909 ++++++++++++++++++++-------------------- 1 file changed, 446 insertions(+), 463 deletions(-) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index b6e95b5..e001ecf 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -14,20 +14,13 @@ import { Edit, Eye, LogOut, - Package, Plus, - Search, Settings, Shield, - Star, - Trash2, - Upload, - Users, - Wallet, X, XCircle, } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState, memo } from "react"; import { Link } from "react-router-dom"; import { EarningsOverview } from "@/components/dashboard/earnings-overview"; import { PaymentMethods } from "@/components/dashboard/payment-methods"; @@ -150,7 +143,7 @@ const handleUpdateProfile = async ( if (emailOtp) formData.append("otp", emailOtp.toString()); const response = await fetch( - `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}api/user`, + `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/user`, { method: "PUT", body: formData, @@ -193,13 +186,252 @@ const handleUpdateProfile = async ( }; }; +const hasEmailChanged = (originalEmail: string, currentEmail: string) => { + return originalEmail !== currentEmail; +}; + +// Memoized ProfileManagement component to prevent unnecessary re-renders +const ProfileManagement = memo(({ + currentUser, + name, + currentEmail, + originalEmail, + website, + github, + handleSubmit, + isSubmitting, + showOTPDialog, + otpValue, + otpError, + isSendingOTP, + isVerifyingOTP, + handleOTPVerification, + handleResendOTP, + handleCancel, + queryClient, + handleLogOut, + setName, + setCurrentEmail, + setWebsite, + setGithub, + setOtpValue, + setOtpError +}: { + currentUser: any; + name: string; + currentEmail: string; + originalEmail: string; + website: string; + github: string; + handleSubmit: (e: React.FormEvent) => void; + isSubmitting: boolean; + showOTPDialog: boolean; + otpValue: string; + otpError: string; + isSendingOTP: boolean; + isVerifyingOTP: boolean; + handleOTPVerification: () => void; + handleResendOTP: () => void; + handleCancel: () => void; + queryClient: any; + handleLogOut: (queryClient: any, e: React.MouseEvent) => void; + setName: (name: string) => void; + setCurrentEmail: (email: string) => void; + setWebsite: (website: string) => void; + setGithub: (github: string) => void; + setOtpValue: (value: string) => void; + setOtpError: (error: string) => void; +}) => ( +
+ {/* Profile Information */} + + +
+ + + Profile Information + + +
+
+ +
+ + {currentUser.github ? ( + + ) : null} + + {currentUser.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{currentUser.name}

+

{currentUser.email}

+
+ {currentUser.role} + {currentUser.verified && ( + + Verified + + )} +
+
+ +
+
handleSubmit(e)} className="space-y-4"> +
+
+ + setName(e.target.value)} + /> +
+
+ + setCurrentEmail(e.target.value)} + /> + {/* Visual indicator when email has changed */} + {hasEmailChanged(originalEmail, currentEmail) && ( +

+ Email will be changed from: {originalEmail} +

+ )} +
+
+ + setWebsite(e.target.value)} + /> +
+
+ + setGithub(e.target.value)} + /> +
+
+ + +
+ + {/* Radix-UI Dialog for OTP Verification */} + + {}}> + + Verify Your New Email + + We've sent a 6-digit verification code to{" "} + {currentEmail}. Please enter the code below to + confirm your email change. + + +
+
+ + { + const value = e.target.value + .replace(/\D/g, "") + .slice(0, 6); + setOtpValue(value); + setOtpError(""); + }} + placeholder="Enter 6-digit code" + maxLength={6} + className="text-center text-lg tracking-widest" + /> + {otpError && ( +

{otpError}

+ )} +
+ +
+ +
+ +
+ + +
+
+
+
+
+
+ + {/* Payment Methods */} + +
+)); + export default function Dashboard() { // states - const [activeTab, setActiveTab] = useState("overview"); + // const [activeTab, setActiveTab] = useState("overview"); const [formData, setFormData] = useState(new FormData()); - const [originalEmail, setOriginalEmail] = useState(); + const [originalEmail, setOriginalEmail] = useState(""); const [currentEmail, setCurrentEmail] = useState(""); const [pluginSearchQuery, setPluginSearchQuery] = useState(""); + + // Form input states + const [name, setName] = useState(""); + const [website, setWebsite] = useState(""); + const [github, setGithub] = useState(""); // State to control when the OTP dialog should be open const [showOTPDialog, setShowOTPDialog] = useState(false); @@ -215,44 +447,7 @@ export default function Dashboard() { // State for Error Msgs. const [otpError, setOtpError] = useState(""); - const userMutation = useMutation({ - mutationFn: async (body: { - formData: FormData; - currentUser: User; - emailOtp?: number; - }) => - await handleUpdateProfile(body.formData, body.currentUser, body.emailOtp), - onSuccess: async (data) => { - const { statusCode, body } = data; - - // handleUpdateProfile, handles 401. & redirects the User. - if (statusCode !== 401 && body.message) { - toast({ - title: "Successfully Updated - User Profile", - description: `${body.message}` || "User Updated", - }); - await queryClient.invalidateQueries({ queryKey: ["LoggedInUser"] }); - - setIsSubmitting(false); - setIsVerifyingOTP(false); - setOriginalEmail(currentEmail); - return; - } - - return; - }, - onError: (error) => { - toast({ - title: "Failed to Update Profile", - description: `${error.message}`, - variant: "destructive", - }); - - setOtpError(`${error.message}`); - return; - }, - }); - + // All hooks must be called before any conditional returns const queryClient = useQueryClient(); const deletePluginMutation = useDeletePlugin(); const { @@ -261,98 +456,26 @@ export default function Dashboard() { isLoading, ...args } = useLoggedInUser(); - const { - data: userPlugins, - isLoading: isPluginsLoading, - error: pluginsError, - ...pluginArgs - } = useQuery({ - queryKey: ["loggedInUser", "plugins"], - staleTime: 2 * 60 * 1000, // 2 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - refetchOnWindowFocus: false, - queryFn: async () => { - const response = await fetch( - `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/plugin?user=${currentLoggedUser.id}`, - ); - const data = response.headers - .get("content-type") - .includes("application/json") - ? await response.json() - : null; - if (!response.ok) { - throw new Error( - `Could not get Your Plugins (request status code: ${response.status})`, - ); - } - - return data; - }, - enabled: !!currentLoggedUser?.id, - }); - - console.log( - "Logged In User: ", - currentLoggedUser, - "IsError: ", - isError, - "isLoading: ", - isLoading, - "Args: ", - args, - ); - console.log(userPlugins, isPluginsLoading, pluginsError, pluginArgs); - if (isError) { - toast({ - title: "User Not Logged in. redirecting...", - }); - // - // setTimeout(() => { - // window.location.href = "/login" - // }, 1000) - return; - } - - if (isLoading) { - return ; - } - - // also using mocked data for now. - const currentUser = { + // Memoize currentUser to prevent unnecessary re-renders + const currentUser = useMemo(() => ({ ...currentMockUser, ...currentLoggedUser, - }; - - // Filter plugins based on search query - const filteredUserPlugins = - userPlugins?.filter( - (plugin) => - plugin.name.toLowerCase().includes(pluginSearchQuery.toLowerCase()) || - plugin.author?.toLowerCase().includes(pluginSearchQuery.toLowerCase()), - ) || []; - // useEffect(() => setCurrentEmail(currentUser.email), [currentLoggedUser.email]); - - const handleDeletePlugin = async ( - pluginId: string, - mode: "soft" | "hard", - ) => { - try { - await deletePluginMutation.mutateAsync({ pluginId, mode }); - toast({ - title: "Plugin Deleted", - description: `Plugin ${mode === "hard" ? "permanently deleted" : "deleted"} successfully`, - }); - } catch (error) { - toast({ - title: "Delete Failed", - description: "Failed to delete plugin. Please try again.", - variant: "destructive", - }); + }), [currentLoggedUser]); + + useEffect(() => { + if (currentLoggedUser?.email) { + console.log("useEffect :: setCurrentEmail") + setCurrentEmail(currentUser.email); + setOriginalEmail(currentUser.email); + setName(currentUser.name || ""); + setWebsite(currentUser.website || ""); + setGithub(currentUser.github || ""); } - }; + }, [currentUser.email, currentUser.name, currentUser.website, currentUser.github]); - const UserDashboard = () => ( + // Memoize UserDashboard component to prevent unnecessary re-renders + const UserDashboard = useMemo(() => (
{/* Overview Grid */}
@@ -387,12 +510,110 @@ export default function Dashboard() {
- ); + ), []); + + + // const { + // data: userPlugins, + // isLoading: isPluginsLoading, + // error: pluginsError, + // ...pluginArgs + // } = useQuery({ + // queryKey: ["loggedInUser", "plugins"], + // staleTime: 2 * 60 * 1000, // 2 minutes + // gcTime: 10 * 60 * 1000, // 10 minutes + // refetchOnWindowFocus: false, + // queryFn: async () => { + // const response = await fetch( + // `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/plugin?user=${currentLoggedUser.id}`, + // ); + // const data = response.headers + // .get("content-type") + // .includes("application/json") + // ? await response.json() + // : null; + + // if (!response.ok) { + // throw new Error( + // `Could not get Your Plugins (request status code: ${response.status})`, + // ); + // } + + // return data; + // }, + // enabled: !!currentLoggedUser?.id, + // }); - // Profile Management Utils - const hasEmailChanged = () => { - return originalEmail !== currentEmail; - }; + const userMutation = useMutation({ + mutationFn: async (body: { + formData: FormData; + currentUser: User; + emailOtp?: number; + }) => + await handleUpdateProfile(body.formData, body.currentUser, body.emailOtp), + onSuccess: async (data) => { + const { statusCode, body } = data; + + // handleUpdateProfile, handles 401. & redirects the User. + if (statusCode !== 401 && body.message) { + toast({ + title: "Successfully Updated - User Profile", + description: `${body.message}` || "User Updated", + }); + await queryClient.invalidateQueries({ queryKey: ["LoggedInUser"] }); + + setIsSubmitting(false); + setIsVerifyingOTP(false); + setOriginalEmail(currentEmail); + return; + } + + return; + }, + onError: (error) => { + toast({ + title: "Failed to Update Profile", + description: `${error.message}`, + variant: "destructive", + }); + setIsSubmitting(false) + setOtpError(`${error.message}`); + return; + }, + }); + + console.log( + "Logged In User: ", + currentLoggedUser, + "IsError: ", + isError, + "isLoading: ", + isLoading, + "Args: ", + args, + ); + // console.log(userPlugins, isPluginsLoading, pluginsError, pluginArgs); + + // const handleDeletePlugin = async ( + // pluginId: string, + // mode: "soft" | "hard", + // ) => { + // try { + // await deletePluginMutation.mutateAsync({ pluginId, mode }); + // toast({ + // title: "Plugin Deleted", + // description: `Plugin ${mode === "hard" ? "permanently deleted" : "deleted"} successfully`, + // }); + // } catch (error) { + // toast({ + // title: "Delete Failed", + // description: "Failed to delete plugin. Please try again.", + // variant: "destructive", + // }); + // } + // }; + + // Profile Management Utils - memoize callback functions const sendOTPToNewEmail = async ( email: string, @@ -428,12 +649,32 @@ export default function Dashboard() { } }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleActualSubmit = useCallback(async (emailOtp?: number) => { + setIsSubmitting(true); - setFormData(new FormData(e.target as HTMLFormElement)); + userMutation.mutate({ + formData, + currentUser, + emailOtp, + }); - if (hasEmailChanged()) { + // not resetting the states here, + // as we don't know if it success or failed. We clear/set updated states in mutation itself. + }, [formData, currentUser, userMutation]); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + // Create FormData from current form state + const newFormData = new FormData(); + newFormData.append("name", name); + newFormData.append("email", currentEmail); + newFormData.append("website", website); + newFormData.append("github", github); + + console.log("handleSubmit", formData) + setFormData(newFormData); + + if (hasEmailChanged(originalEmail, currentEmail)) { // Email has changed, show OTP dialog and send OTP setShowOTPDialog(true); @@ -452,24 +693,10 @@ export default function Dashboard() { } else { await handleActualSubmit(); } - }; - - const handleActualSubmit = async (emailOtp?: number) => { - setIsSubmitting(true); + }, [name, currentEmail, website, github, originalEmail, sendOTPToNewEmail, handleActualSubmit]); - userMutation.mutate({ - formData, - currentUser, - emailOtp, - }); - - // not resetting the states here, - // as we don't know if it success or failed. We clear/set updated states in mutation itself. - }; - - // Handle OTP verification - - const handleOTPVerification = async () => { + // Handle OTP verification - memoize callback + const handleOTPVerification = useCallback(async () => { if (!otpValue.trim()) { setOtpError("Please enter the OTP"); return; @@ -484,317 +711,48 @@ export default function Dashboard() { } else { setOtpError("Invalid OTP. Please check and try again."); } - }; + }, [otpValue, handleActualSubmit]); - // Handle when user cancels the OTP dialog - const handleCancel = () => { + // Handle when user cancels the OTP dialog - memoize callback + const handleCancel = useCallback(() => { setShowOTPDialog(false); setOtpValue(""); setOtpError(""); // Optionally reset the email to original value setCurrentEmail(originalEmail); - }; + }, [originalEmail]); - // Handle resending OTP - const handleResendOTP = async () => { + // Handle resending OTP - memoize callback + const handleResendOTP = useCallback(async () => { setOtpValue(""); // Clear current OTP input await sendOTPToNewEmail(currentEmail); - }; - - const ProfileManagement = () => ( -
- {/* Profile Information */} - - -
- - - Profile Information - - -
-
- -
- - {currentUser.github ? ( - - ) : null} - - {currentUser.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-

{currentUser.name}

-

{currentUser.email}

-
- {currentUser.role} - {currentUser.verified && ( - - Verified - - )} -
-
- -
-
handleSubmit(e)} className="space-y-4"> -
-
- - -
-
- - setCurrentEmail(e.target.value)} - /> - {/* Visual indicator when email has changed */} - {hasEmailChanged() && ( -

- Email will be changed from: {originalEmail} -

- )} -
-
- - -
-
- - -
-
- - -
- - {/* Radix-UI Dialog for OTP Verification */} - - - - Verify Your New Email - - We've sent a 6-digit verification code to{" "} - {currentEmail}. Please enter the code below to - confirm your email change. - - -
-
- - { - const value = e.target.value - .replace(/\D/g, "") - .slice(0, 6); - setOtpValue(value); - setOtpError(""); - }} - placeholder="Enter 6-digit code" - maxLength={6} - className="text-center text-lg tracking-widest" - /> - {otpError && ( -

{otpError}

- )} -
- -
- -
- -
- - -
-
-
-
-
-
+ }, [currentEmail, sendOTPToNewEmail]); - {/* Payment Methods */} - -
- ); - const DetailedEarningsOverview = () => ( -
- {/* Quick Stats */} -
- - - - Total Earnings - - - - - {/*
${currentUser.totalEarnings.toFixed(2)}
*/} -
0
-

- Available for withdrawal -

-
-
- - - - This Month - - - -
$89.99
-

- +12% from last month -

-
-
- - - - - Pending Payment - - - - -
$156.78
-

- Next payment: Feb 15 -

-
-
-
+ if (isError) { + console.log("User Not Logged in. redirecting...") + // + // setTimeout(() => { + // window.location.href = "/login" + // }, 1000) + return ( +
+ +

User Not Logged In.

+

+ You're not logged in! Redirecting... +

+
+ ); + } - {/* Quick Actions */} - - - Quick Actions - - -
- - - - - -
-
-
- - {/* Recent Transactions */} - - - Recent Transactions - - -
- {[ - { - date: "2024-01-28", - amount: 2.99, - plugin: "Git Manager", - type: "Sale", - }, - { - date: "2024-01-27", - amount: 1.99, - plugin: "Code Formatter Pro", - type: "Sale", - }, - { - date: "2024-01-26", - amount: 4.99, - plugin: "My Theme Studio", - type: "Sale", - }, - ].map((transaction, index) => ( -
-
-

{transaction.plugin}

-

- {transaction.type} • {transaction.date} -

-
-
-

- +${transaction.amount.toFixed(2)} -

-
-
- ))} -
-
-
-
- ); + // if (isLoading) { + // console.log("showing loading Dashboard") + // return ; + // } - return isLoading ? ( -
-
-
-

Loading dashboard...

-
-
+ return isLoading || isError ? ( + ) : (
{/* Header */} @@ -830,18 +788,43 @@ export default function Dashboard() {
{/* Tabs */} - + Overview Profile - + {UserDashboard} - + From c9fcb8ddd98cde9b87993e43b0cc9e33f9b7527c Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Fri, 31 Oct 2025 21:47:40 +0530 Subject: [PATCH 02/14] Improve dashboard and login functionalities - Move profileManagement to it's own component, to avoid states cluttering on parent component - Adds `.env*` and `certs` to `.gitignore` to ignore environmental and setup files. - Introduces lazy loading for images in the UserPluginsOverview component to improve performance. - Refactors Dashboard, Login and Plugins pages to use `useNavigate` hook instead of `window.location.href` to improve routing. --- .gitignore | 7 + .../dashboard/profile-management.tsx | 572 +++++++++++ .../dashboard/user-plugins-overview.tsx | 3 + src/pages/Dashboard.tsx | 931 +++++++----------- src/pages/Login.tsx | 6 +- src/pages/Plugins.tsx | 5 +- src/pages/Signup.tsx | 1 - 7 files changed, 953 insertions(+), 572 deletions(-) create mode 100644 src/components/dashboard/profile-management.tsx diff --git a/.gitignore b/.gitignore index a547bf3..30d38e6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ dist-ssr *.njsproj *.sln *.sw? + +# ignore Environmental & setup Files +.env +.env.production +.env.prod +*/certs/** +_cert.pem \ No newline at end of file diff --git a/src/components/dashboard/profile-management.tsx b/src/components/dashboard/profile-management.tsx new file mode 100644 index 0000000..4dac8d7 --- /dev/null +++ b/src/components/dashboard/profile-management.tsx @@ -0,0 +1,572 @@ +import { + BarChart3, + Building2, + CheckCircle, + Clock, + CreditCard, + DollarSign, + Edit, + Eye, + LogOut, + Plus, + Settings, + Shield, + X, + XCircle, +} from "lucide-react"; +import { useCallback, useEffect, useMemo, useState, useRef, memo } from "react"; +import { Link, redirect, useNavigate } from "react-router-dom"; +import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; +import { PaymentMethods } from "@/components/dashboard/payment-methods"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { DeletePluginDialog } from "@/components/ui/delete-plugin-dialog"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogOverlay, + DialogPortal, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/hooks/use-toast.ts"; +import { User } from "@/types"; + +// Mock user data +const currentMockUser = { + name: "John Doe", + email: "john@example.com", + role: "user", // "user" or "admin" + avatar: "JD", + joinDate: "2024-01-15", + bio: "Full-stack developer passionate about mobile development and creating tools that enhance productivity.", + website: "https://johndoe.dev", + github: "johndoe", + location: "San Francisco, CA", + totalEarnings: 245.67, + bankAccount: { + accountHolder: "John Doe", + bankName: "Chase Bank", + accountNumber: "****1234", + routingNumber: "****567", + }, +}; + +/** + * Handles the user log out process. + * This function typically invalidates or removes local authentication data, + * cleans up the cache, and redirects the user. + * @param {import('@tanstack/react-query').QueryClient} queryClient - The TanStack Query client instance to manage and clear the cache. + * @param {React.MouseEvent} e - The React mouse event from the button click. + * @param {(to: string) => void} handleRedirect - A callback function used to redirect the user to a new URL after successful log out. + * It accepts a single string parameter `to` which is the destination URL (e.g., '/login'). + * @returns {Promise} A promise that resolves when the log out process is complete. + */ +const handleLogOut = async ( + queryClient: QueryClient, + e: React.MouseEvent, + handleRedirect: (to: string) => void, +): Promise => { + // invalidate the Access Token received while Login. + try { + const response = await fetch( + `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/login`, + { + method: "DELETE", + credentials: "include", + }, + ); + + const responseData = + response.headers.get("content-type") === "application/json" + ? await response.json() + : null; + if (responseData?.error || !response.ok) { + toast({ + title: "Unable to Log Out!", + description: + responseData.error || + `Something went wrong, server responded empty (request status code: ${response.status}). Please try again.`, + }); + // Bad Request, in this case means: response as {error: 'Not Logged in'} + if (response.status === 400) { + setTimeout(() => { + handleRedirect("/login"); + }, 1000); + } + } else if (response.ok) { + toast({ + title: "Logged Out!", + description: + responseData.message || "Logged Out Successfully, redirecting....", + }); + } + + await queryClient.invalidateQueries({ + queryKey: ["loggedInUser"], + }); + + setTimeout(() => { + window.location.href = "/login"; + }, 1000); + } catch (error) { + toast({ + title: "Unable to Log Out!", + description: `Something went wrong, server responded empty (error: ${error.message}). Please try again.`, + }); + } +}; + +/** + * Handles Updating of Profile. + * @param formData the formData received from the event or mutation (from Tanstack query). + * @param {(to: string) => void} handleRedirect - A callback function used to redirect the user to a new URL after successful log out. + * @param emailOtp email OTP only required if Email has been changed/opted to update the email. + * @returns + */ +const handleUpdateProfile = async ( + formData: FormData, + handleRedirect: (to: string) => void, + emailOtp?: number, +) => { + if (emailOtp) formData.append("otp", emailOtp.toString()); + + const response = await fetch( + `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/user`, + { + method: "PUT", + body: formData, + credentials: "include", + }, + ); + + const responseData = response.headers + .get("content-type") + .includes("application/json") + ? await response.json() + : null; + + if (responseData?.error || !response.ok) { + // Not Logged-In/Authorized + if (response.status === 401) { + toast({ + title: "Failed to Update Profile", + description: `${responseData?.error} | redirecting....`, + variant: "destructive", + }); + setTimeout(() => { + handleRedirect("/login"); + }, 1000); + return { statusCode: response.status }; + } + + const error = new Error( + `${responseData?.error}` || + `Failed to Update Profile (request status code: ${response.status}). Please try again.)`, + ); + error["code"] = response.status; + + throw error; + } + + return { + statusCode: response.status, + body: responseData as { message: string }, + }; +}; + +const hasEmailChanged = (originalEmail: string, currentEmail: string) => { + return originalEmail !== currentEmail; +}; + + +type ProfileManagementProps = { + currentUser: User; +}; + +// Memoized ProfileManagement component to prevent unnecessary re-renders +const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { + const navigate = useNavigate() + // Form input states + const [name, setName] = useState(currentUser?.name || ""); + const [currentEmail, setCurrentEmail] = useState(currentUser?.email || ""); + const [originalEmail, setOriginalEmail] = useState(currentUser?.email || ""); + const [website, setWebsite] = useState(currentUser?.website || ""); + const [github, setGithub] = useState(currentUser?.github || ""); + + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. + console.table({ name, originalEmail, currentEmail, website, github }) + + // State to control when the OTP dialog should be open + const [showOTPDialog, setShowOTPDialog] = useState(false); + + // State to track the OTP input value + const otpRef = useRef(null); + + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. + console.log(otpRef.current) + + // State to track loading states + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSendingOTP, setIsSendingOTP] = useState(false); + const [isVerifyingOTP, setIsVerifyingOTP] = useState(false); + + // State for Error Msgs. + const [otpError, setOtpError] = useState(""); + + const queryClient = useQueryClient(); + + const userMutation = useMutation({ + mutationFn: async (body: { + formData: FormData; + currentUser: User; + emailOtp?: number; + }) => + await handleUpdateProfile(body.formData, (toUrl) => navigate(`${toUrl}`), body.emailOtp), + onSuccess: async (data) => { + const { statusCode, body } = data; + + // handleUpdateProfile, handles 401. & redirects the User. + if (statusCode !== 401 && body.message) { + toast({ + title: "Successfully Updated - User Profile", + description: `${body.message}` || "User Updated", + }); + await queryClient.invalidateQueries({ queryKey: ["LoggedInUser"] }); + + setIsSubmitting(false); + setIsVerifyingOTP(false); + setOriginalEmail(currentEmail); + return; + } + + return; + }, + onError: (error) => { + toast({ + title: "Failed to Update Profile", + description: `${error.message}`, + variant: "destructive", + }); + setIsSubmitting(false) + setOtpError(`${error.message}`); + setIsVerifyingOTP(false) + return; + }, + }); + + // Profile Management Utils - memoize callback functions + + const sendOTPToNewEmail = useCallback(async ( + email: string, + type: "reset" | (string & {}) = "signup", + ) => { + console.log({ email, type }) + setIsSendingOTP(true); + const formData = new FormData(); + formData.append("email", email); + + try { + const res = await fetch( + `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/otp?type=${type}`, + { + method: "POST", + credentials: "include", + body: formData, + cache: "no-store", + } + ); + const responseData = res.headers + .get("content-type") + .includes("application/json") + ? await res.json() + : null; + + if (!res.ok) { + throw new Error( + `Email OTP Sending Failed (request status: ${res.status}). As server responded: ${responseData?.error || "empty, Please Try again."}`, + ); + } + + return responseData as { message: string }; + } catch (error) { + setOtpError( + `Failed to send OTP(Please Try Again). Error: ${error.message}`, + ); + } finally { + setIsSendingOTP(false); + } + }, []); + + const handleActualSubmit = useCallback(async (emailOtp?: number) => { + setIsSubmitting(true); + + // Create FormData from current form state + const formData = new FormData(); + formData.append("name", name); + formData.append("email", currentEmail); + formData.append("website", website); + formData.append("github", github); + + userMutation.mutate({ + formData, + currentUser, + emailOtp, + }); + + // not resetting the states here, + // as we don't know if it success or failed. We clear/set updated states in mutation itself. + }, [name, currentEmail, website, github, currentUser, userMutation]); + + const handleSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + + if (hasEmailChanged(originalEmail, currentEmail)) { + // Email has changed, show OTP dialog and send OTP + setShowOTPDialog(true); + + // send email OTP + await sendOTPToNewEmail(currentEmail).catch((e) => { + toast({ + title: `Email OTP Sending Failed to ${currentEmail} | Try again, by clicking Submit button.`, + description: e.message, + variant: "destructive", + }); + // not closing the OTP Dialog as User could, click to resend button. + return null; + }); + + return; + } else { + await handleActualSubmit(); + } + }, [originalEmail, currentEmail, sendOTPToNewEmail, handleActualSubmit]); + + // Handle OTP verification - memoize callback + const handleOTPVerification = useCallback(async () => { + console.log(otpRef.current?.value) + console.log(otpRef.current?.value) + if (!otpRef.current?.value?.trim()) { + setOtpError("Please enter the OTP"); + return; + } + + setIsVerifyingOTP(true); + setOtpError(""); + + if (otpRef.current?.value.length === 6 && /^\d+$/.test(otpRef.current?.value)) { + // OTP is valid, proceed with form submission + await handleActualSubmit(Number(otpRef.current)); + } else { + setOtpError("Invalid OTP. Please check and try again."); + setIsVerifyingOTP(false); + } + }, [otpRef?.current?.value, handleActualSubmit]); + + // Handle when user cancels the OTP dialog - memoize callback + const handleCancel = useCallback(() => { + setShowOTPDialog(false); + otpRef.current.value = ""; + setOtpError(""); + if(isVerifyingOTP) setIsVerifyingOTP(false) + setCurrentEmail(originalEmail); + }, [originalEmail]); + + // Handle resending OTP - memoize callback + const handleResendOTP = useCallback(async () => { + otpRef.current.value = ""; // Clear current OTP input + await sendOTPToNewEmail(currentEmail); + }, [currentEmail, sendOTPToNewEmail]); + + return ( +
+ {/* Profile Information */} + + +
+ + + Profile Information + + +
+
+ +
+ + {currentUser.github ? ( + + ) : null} + + {currentUser.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{currentUser.name}

+

{currentUser.email}

+
+ {currentUser.role} + {currentUser.verified && ( + + Verified + + ) || ""} +
+
+ +
+
handleSubmit(e)} className="space-y-4"> +
+
+ + setName(e.target.value)} + /> +
+
+ + setCurrentEmail(e.target.value)} + /> + {/* Visual indicator when email has changed */} + {hasEmailChanged(originalEmail, currentEmail) && ( +

+ Email will be changed from: {originalEmail} +

+ )} +
+
+ + setWebsite(e.target.value)} + /> +
+
+ + setGithub(e.target.value)} + /> +
+
+ + +
+ + {/* Radix-UI Dialog for OTP Verification */} + + + + Verify Your New Email + + We've sent a 6-digit verification code to{" "} + {currentEmail}. Please enter the code below to + confirm your email change. + + +
+
+ + { + const value = e.target.value + .replace(/\D/g, "") + .slice(0, 6); + otpRef.current.value = value; + setOtpError(""); + }} + placeholder="Enter 6-digit code" + maxLength={6} + className="text-center text-lg tracking-widest" + /> + {otpError && ( +

{otpError}

+ )} +
+ +
+ +
+ +
+ + + + + + +
+
+
+
+
+
+ + {/* Payment Methods */} + +
+ ); +}); + +export default ProfileManagement; \ No newline at end of file diff --git a/src/components/dashboard/user-plugins-overview.tsx b/src/components/dashboard/user-plugins-overview.tsx index b18f677..50b31a4 100644 --- a/src/components/dashboard/user-plugins-overview.tsx +++ b/src/components/dashboard/user-plugins-overview.tsx @@ -119,6 +119,7 @@ export function UserPluginsOverview() { ); // Reset pagination when filters change + // biome-ignore lint/correctness/useExhaustiveDependencies: It's necessary, for resetting of pagination useEffect(() => { setCurrentPage(1); }, [searchQuery, statusFilter, sortBy, sortOrder]); @@ -328,6 +329,7 @@ export function UserPluginsOverview() { const target = e.target as HTMLImageElement; target.style.display = "none"; }} + loading="lazy" />
{plugin.name}
@@ -444,6 +446,7 @@ export function UserPluginsOverview() { const target = e.target as HTMLImageElement; target.style.display = "none"; }} + loading="lazy" />
{plugin.name}
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index e001ecf..68cda0a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -20,25 +20,15 @@ import { X, XCircle, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState, memo } from "react"; -import { Link } from "react-router-dom"; +import { useCallback, useEffect, useMemo, useState, useRef } from "react"; +import { Link, useNavigate } from "react-router-dom"; import { EarningsOverview } from "@/components/dashboard/earnings-overview"; -import { PaymentMethods } from "@/components/dashboard/payment-methods"; import { UserPluginsOverview } from "@/components/dashboard/user-plugins-overview"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DeletePluginDialog } from "@/components/ui/delete-plugin-dialog"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogOverlay, - DialogPortal, - DialogTitle, -} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -47,6 +37,7 @@ import { toast } from "@/hooks/use-toast.ts"; import { useDeletePlugin } from "@/hooks/use-user-plugins"; import { useLoggedInUser } from "@/hooks/useLoggedInUser.ts"; import { User } from "@/types"; +import ProfileManagement from "@/components/dashboard/profile-management"; // Mock user data const currentMockUser = { @@ -81,375 +72,359 @@ const LoadingDashboard = () => { ); }; -const handleLogOut = async ( - queryClient: QueryClient, - e: React.MouseEvent, -) => { - // invalidate the Access Token received while Login. - try { - const response = await fetch( - `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/login`, - { - method: "DELETE", - credentials: "include", - }, - ); - - const responseData = - response.headers.get("content-type") === "application/json" - ? await response.json() - : null; - if (responseData?.error || !response.ok) { - toast({ - title: "Unable to Log Out!", - description: - responseData.error || - `Something went wrong, server responded empty (request status code: ${response.status}). Please try again.`, - }); - // Bad Request, in this case means: response as {error: 'Not Logged in'} - if (response.status === 400) { - setTimeout(() => { - window.location.href = "/login"; - }, 1000); - } - } else if (response.ok) { - toast({ - title: "Logged Out!", - description: - responseData.message || "Logged Out Successfully, redirecting....", - }); - } - - await queryClient.invalidateQueries({ - queryKey: ["loggedInUser"], - }); - - setTimeout(() => { - window.location.href = "/login"; - }, 1000); - } catch (error) { - toast({ - title: "Unable to Log Out!", - description: `Something went wrong, server responded empty (error: ${error.message}). Please try again.`, - }); - } -}; - -const handleUpdateProfile = async ( - formData: FormData, - currentUser: User, - emailOtp?: number, -) => { - if (emailOtp) formData.append("otp", emailOtp.toString()); - - const response = await fetch( - `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/user`, - { - method: "PUT", - body: formData, - credentials: "include", - }, - ); - - const responseData = response.headers - .get("content-type") - .includes("application/json") - ? await response.json() - : null; - - if (responseData?.error || !response.ok) { - // Not Logged-In/Authorized - if (response.status === 401) { - toast({ - title: "Failed to Update Profile", - description: `${responseData?.error} | redirecting....`, - variant: "destructive", - }); - setTimeout(() => { - window.location.href = "/login"; - }, 1000); - return { statusCode: response.status }; - } - - const error = new Error( - `${responseData?.error}` || - `Failed to Update Profile (request status code: ${response.status}). Please try again.)`, - ); - error["code"] = response.status; - - throw error; - } - - return { - statusCode: response.status, - body: responseData as { message: string }, - }; -}; - -const hasEmailChanged = (originalEmail: string, currentEmail: string) => { - return originalEmail !== currentEmail; -}; +// Kept for Reference. +// biome-ignore lint: Kept for Reference Only Should be Removed in PROD. +// const handleLogOut = async ( +// queryClient: QueryClient, +// e: React.MouseEvent, +// ) => { +// // invalidate the Access Token received while Login. +// try { +// const response = await fetch( +// `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/login`, +// { +// method: "DELETE", +// credentials: "include", +// }, +// ); + +// const responseData = +// response.headers.get("content-type") === "application/json" +// ? await response.json() +// : null; +// if (responseData?.error || !response.ok) { +// toast({ +// title: "Unable to Log Out!", +// description: +// responseData.error || +// `Something went wrong, server responded empty (request status code: ${response.status}). Please try again.`, +// }); +// // Bad Request, in this case means: response as {error: 'Not Logged in'} +// if (response.status === 400) { +// setTimeout(() => { +// window.location.href = "/login"; +// }, 1000); +// } +// } else if (response.ok) { +// toast({ +// title: "Logged Out!", +// description: +// responseData.message || "Logged Out Successfully, redirecting....", +// }); +// } + +// await queryClient.invalidateQueries({ +// queryKey: ["loggedInUser"], +// }); + +// setTimeout(() => { +// window.location.href = "/login"; +// }, 1000); +// } catch (error) { +// toast({ +// title: "Unable to Log Out!", +// description: `Something went wrong, server responded empty (error: ${error.message}). Please try again.`, +// }); +// } +// }; + +// const handleUpdateProfile = async ( +// formData: FormData, +// currentUser: User, +// emailOtp?: number, +// ) => { +// if (emailOtp) formData.append("otp", emailOtp.toString()); + +// const response = await fetch( +// `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/user`, +// { +// method: "PUT", +// body: formData, +// credentials: "include", +// }, +// ); + +// const responseData = response.headers +// .get("content-type") +// .includes("application/json") +// ? await response.json() +// : null; + +// if (responseData?.error || !response.ok) { +// // Not Logged-In/Authorized +// if (response.status === 401) { +// toast({ +// title: "Failed to Update Profile", +// description: `${responseData?.error} | redirecting....`, +// variant: "destructive", +// }); +// setTimeout(() => { +// window.location.href = "/login"; +// }, 1000); +// return { statusCode: response.status }; +// } + +// const error = new Error( +// `${responseData?.error}` || +// `Failed to Update Profile (request status code: ${response.status}). Please try again.)`, +// ); +// error["code"] = response.status; + +// throw error; +// } + +// return { +// statusCode: response.status, +// body: responseData as { message: string }, +// }; +// }; + +// const hasEmailChanged = (originalEmail: string, currentEmail: string) => { +// return originalEmail !== currentEmail; +// }; // Memoized ProfileManagement component to prevent unnecessary re-renders -const ProfileManagement = memo(({ - currentUser, - name, - currentEmail, - originalEmail, - website, - github, - handleSubmit, - isSubmitting, - showOTPDialog, - otpValue, - otpError, - isSendingOTP, - isVerifyingOTP, - handleOTPVerification, - handleResendOTP, - handleCancel, - queryClient, - handleLogOut, - setName, - setCurrentEmail, - setWebsite, - setGithub, - setOtpValue, - setOtpError -}: { - currentUser: any; - name: string; - currentEmail: string; - originalEmail: string; - website: string; - github: string; - handleSubmit: (e: React.FormEvent) => void; - isSubmitting: boolean; - showOTPDialog: boolean; - otpValue: string; - otpError: string; - isSendingOTP: boolean; - isVerifyingOTP: boolean; - handleOTPVerification: () => void; - handleResendOTP: () => void; - handleCancel: () => void; - queryClient: any; - handleLogOut: (queryClient: any, e: React.MouseEvent) => void; - setName: (name: string) => void; - setCurrentEmail: (email: string) => void; - setWebsite: (website: string) => void; - setGithub: (github: string) => void; - setOtpValue: (value: string) => void; - setOtpError: (error: string) => void; -}) => ( -
- {/* Profile Information */} - - -
- - - Profile Information - - -
-
- -
- - {currentUser.github ? ( - - ) : null} - - {currentUser.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-

{currentUser.name}

-

{currentUser.email}

-
- {currentUser.role} - {currentUser.verified && ( - - Verified - - )} -
-
- -
-
handleSubmit(e)} className="space-y-4"> -
-
- - setName(e.target.value)} - /> -
-
- - setCurrentEmail(e.target.value)} - /> - {/* Visual indicator when email has changed */} - {hasEmailChanged(originalEmail, currentEmail) && ( -

- Email will be changed from: {originalEmail} -

- )} -
-
- - setWebsite(e.target.value)} - /> -
-
- - setGithub(e.target.value)} - /> -
-
- - -
- - {/* Radix-UI Dialog for OTP Verification */} - - {}}> - - Verify Your New Email - - We've sent a 6-digit verification code to{" "} - {currentEmail}. Please enter the code below to - confirm your email change. - - -
-
- - { - const value = e.target.value - .replace(/\D/g, "") - .slice(0, 6); - setOtpValue(value); - setOtpError(""); - }} - placeholder="Enter 6-digit code" - maxLength={6} - className="text-center text-lg tracking-widest" - /> - {otpError && ( -

{otpError}

- )} -
- -
- -
- -
- - -
-
-
-
-
-
- - {/* Payment Methods */} - -
-)); - +// const ProfileManagement = memo(({ +// currentUser, +// name, +// currentEmail, +// originalEmail, +// website, +// github, +// handleSubmit, +// isSubmitting, +// showOTPDialog, +// otpValue, +// otpError, +// isSendingOTP, +// isVerifyingOTP, +// handleOTPVerification, +// handleResendOTP, +// handleCancel, +// queryClient, +// handleLogOut, +// setName, +// setCurrentEmail, +// setWebsite, +// setGithub, +// setOtpValue, +// setOtpError +// }: { +// currentUser: any; +// name: string; +// currentEmail: string; +// originalEmail: string; +// website: string; +// github: string; +// handleSubmit: (e: React.FormEvent) => void; +// isSubmitting: boolean; +// showOTPDialog: boolean; +// otpValue: string; +// otpError: string; +// isSendingOTP: boolean; +// isVerifyingOTP: boolean; +// handleOTPVerification: () => void; +// handleResendOTP: () => void; +// handleCancel: () => void; +// queryClient: any; +// handleLogOut: (queryClient: any, e: React.MouseEvent) => void; +// setName: (name: string) => void; +// setCurrentEmail: (email: string) => void; +// setWebsite: (website: string) => void; +// setGithub: (github: string) => void; +// setOtpValue: (value: string) => void; +// setOtpError: (error: string) => void; +// }) => ( +//
+// {/* Profile Information */} +// +// +//
+// +// +// Profile Information +// +// +//
+//
+// +//
+// +// {currentUser.github ? ( +// +// ) : null} +// +// {currentUser.name +// .split(" ") +// .map((n) => n[0]) +// .join("")} +// +// +//
+//

{currentUser.name}

+//

{currentUser.email}

+//
+// {currentUser.role} +// {currentUser.verified && ( +// +// Verified +// +// ) || ""} +//
+//
+// +//
+//
handleSubmit(e)} className="space-y-4"> +//
+//
+// +// setName(e.target.value)} +// /> +//
+//
+// +// setCurrentEmail(e.target.value)} +// /> +// {/* Visual indicator when email has changed */} +// {hasEmailChanged(originalEmail, currentEmail) && ( +//

+// Email will be changed from: {originalEmail} +//

+// )} +//
+//
+// +// setWebsite(e.target.value)} +// /> +//
+//
+// +// setGithub(e.target.value)} +// /> +//
+//
+ +// +//
+ +// {/* Radix-UI Dialog for OTP Verification */} + +// {}}> +// +// Verify Your New Email +// +// We've sent a 6-digit verification code to{" "} +// {currentEmail}. Please enter the code below to +// confirm your email change. +// + +//
+//
+// +// { +// const value = e.target.value +// .replace(/\D/g, "") +// .slice(0, 6); +// setOtpValue(value); +// setOtpError(""); +// }} +// placeholder="Enter 6-digit code" +// maxLength={6} +// className="text-center text-lg tracking-widest" +// /> +// {otpError && ( +//

{otpError}

+// )} +//
+ +//
+// +//
+ +//
+// +// +//
+//
+//
+//
+//
+//
+ +// {/* Payment Methods */} +// +//
+// )); + +// All hooks must be called before any conditional returns export default function Dashboard() { - // states + // states & Hooks // const [activeTab, setActiveTab] = useState("overview"); - const [formData, setFormData] = useState(new FormData()); - const [originalEmail, setOriginalEmail] = useState(""); - const [currentEmail, setCurrentEmail] = useState(""); - const [pluginSearchQuery, setPluginSearchQuery] = useState(""); - - // Form input states - const [name, setName] = useState(""); - const [website, setWebsite] = useState(""); - const [github, setGithub] = useState(""); - - // State to control when the OTP dialog should be open - const [showOTPDialog, setShowOTPDialog] = useState(false); - - // State to track the OTP input value - const [otpValue, setOtpValue] = useState(""); - - // State to track loading states - const [isSubmitting, setIsSubmitting] = useState(false); - const [isSendingOTP, setIsSendingOTP] = useState(false); - const [isVerifyingOTP, setIsVerifyingOTP] = useState(false); - - // State for Error Msgs. - const [otpError, setOtpError] = useState(""); - - // All hooks must be called before any conditional returns - const queryClient = useQueryClient(); - const deletePluginMutation = useDeletePlugin(); + // const [pluginSearchQuery, setPluginSearchQuery] = useState(""); + + // const queryClient = useQueryClient(); + // const deletePluginMutation = useDeletePlugin(); + // Used for Navigation instead of `window.href.location` + const navigate = useNavigate() + const { data: currentLoggedUser, isError, @@ -463,16 +438,14 @@ export default function Dashboard() { ...currentLoggedUser, }), [currentLoggedUser]); + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. + console.log(currentUser) + useEffect(() => { if (currentLoggedUser?.email) { - console.log("useEffect :: setCurrentEmail") - setCurrentEmail(currentUser.email); - setOriginalEmail(currentUser.email); - setName(currentUser.name || ""); - setWebsite(currentUser.website || ""); - setGithub(currentUser.github || ""); + console.log("useEffect :: ran on change of currentUser") } - }, [currentUser.email, currentUser.name, currentUser.website, currentUser.github]); + }, [currentLoggedUser?.email]); // Memoize UserDashboard component to prevent unnecessary re-renders const UserDashboard = useMemo(() => ( @@ -544,44 +517,7 @@ export default function Dashboard() { // enabled: !!currentLoggedUser?.id, // }); - const userMutation = useMutation({ - mutationFn: async (body: { - formData: FormData; - currentUser: User; - emailOtp?: number; - }) => - await handleUpdateProfile(body.formData, body.currentUser, body.emailOtp), - onSuccess: async (data) => { - const { statusCode, body } = data; - - // handleUpdateProfile, handles 401. & redirects the User. - if (statusCode !== 401 && body.message) { - toast({ - title: "Successfully Updated - User Profile", - description: `${body.message}` || "User Updated", - }); - await queryClient.invalidateQueries({ queryKey: ["LoggedInUser"] }); - - setIsSubmitting(false); - setIsVerifyingOTP(false); - setOriginalEmail(currentEmail); - return; - } - - return; - }, - onError: (error) => { - toast({ - title: "Failed to Update Profile", - description: `${error.message}`, - variant: "destructive", - }); - setIsSubmitting(false) - setOtpError(`${error.message}`); - return; - }, - }); - + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. console.log( "Logged In User: ", currentLoggedUser, @@ -592,6 +528,8 @@ export default function Dashboard() { "Args: ", args, ); + + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. // console.log(userPlugins, isPluginsLoading, pluginsError, pluginArgs); // const handleDeletePlugin = async ( @@ -613,127 +551,13 @@ export default function Dashboard() { // } // }; - // Profile Management Utils - memoize callback functions - - const sendOTPToNewEmail = async ( - email: string, - type: "reset" | (string & {}) = "signup", - ) => { - setIsSendingOTP(true); - const formData = new FormData(); - formData.append("email", email); - - try { - const res = await fetch( - `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/otp?type=${type}`, - ); - const responseData = res.headers - .get("content-type") - .includes("application/json") - ? await res.json() - : null; - - if (!res.ok) { - throw new Error( - `Email OTP Sending Failed (request status: ${res.status}). As server responded: ${responseData?.error || "empty, Please Try again."}`, - ); - } - - return responseData as { message: string }; - } catch (error) { - setOtpError( - `Failed to send OTP(Please Try Again). Error: ${error.message}`, - ); - } finally { - setIsSendingOTP(false); - } - }; - - const handleActualSubmit = useCallback(async (emailOtp?: number) => { - setIsSubmitting(true); - - userMutation.mutate({ - formData, - currentUser, - emailOtp, - }); - - // not resetting the states here, - // as we don't know if it success or failed. We clear/set updated states in mutation itself. - }, [formData, currentUser, userMutation]); - - const handleSubmit = useCallback(async (e: React.FormEvent) => { - e.preventDefault(); - // Create FormData from current form state - const newFormData = new FormData(); - newFormData.append("name", name); - newFormData.append("email", currentEmail); - newFormData.append("website", website); - newFormData.append("github", github); - - console.log("handleSubmit", formData) - setFormData(newFormData); - - if (hasEmailChanged(originalEmail, currentEmail)) { - // Email has changed, show OTP dialog and send OTP - setShowOTPDialog(true); - - // send email OTP - await sendOTPToNewEmail(currentEmail).catch((e) => { - toast({ - title: `Email OTP Sending Failed to ${currentEmail} | Try again, by clicking Submit button.`, - description: e.message, - variant: "destructive", - }); - // not closing the OTP Dialog as User could, click to resend button. - return null; - }); - - return; - } else { - await handleActualSubmit(); - } - }, [name, currentEmail, website, github, originalEmail, sendOTPToNewEmail, handleActualSubmit]); - - // Handle OTP verification - memoize callback - const handleOTPVerification = useCallback(async () => { - if (!otpValue.trim()) { - setOtpError("Please enter the OTP"); - return; - } - - setIsVerifyingOTP(true); - setOtpError(""); - - if (otpValue.length === 6 && /^\d+$/.test(otpValue)) { - // OTP is valid, proceed with form submission - await handleActualSubmit(Number(otpValue)); - } else { - setOtpError("Invalid OTP. Please check and try again."); - } - }, [otpValue, handleActualSubmit]); - - // Handle when user cancels the OTP dialog - memoize callback - const handleCancel = useCallback(() => { - setShowOTPDialog(false); - setOtpValue(""); - setOtpError(""); - // Optionally reset the email to original value - setCurrentEmail(originalEmail); - }, [originalEmail]); - - // Handle resending OTP - memoize callback - const handleResendOTP = useCallback(async () => { - setOtpValue(""); // Clear current OTP input - await sendOTPToNewEmail(currentEmail); - }, [currentEmail, sendOTPToNewEmail]); - if (isError) { + // TODO: Move this to Middleware or make use of contexts. + // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. console.log("User Not Logged in. redirecting...") - // // setTimeout(() => { - // window.location.href = "/login" + // navigate("/login") // }, 1000) return (
@@ -746,11 +570,6 @@ export default function Dashboard() { ); } - // if (isLoading) { - // console.log("showing loading Dashboard") - // return ; - // } - return isLoading || isError ? ( ) : ( @@ -770,6 +589,7 @@ export default function Dashboard() { ) : null} @@ -801,29 +621,6 @@ export default function Dashboard() { diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fd78e8f..d2ed00f 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,6 +1,6 @@ import { Eye, EyeOff, Github, Lock, LogIn, Mail } from "lucide-react"; import { useState } from "react"; -import { Link, redirect, useParams } from "react-router-dom"; +import { Link, redirect, useNavigate, useParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -9,11 +9,13 @@ import { Separator } from "@/components/ui/separator"; import { useToast } from "@/hooks/use-toast"; export default function Login() { + // States & Hooks. const [showPassword, setShowPassword] = useState(false); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); + const navigate = useNavigate(); const params = useParams(); const handleLogin = async (e: React.FormEvent) => { @@ -67,7 +69,7 @@ export default function Login() { redirectUrl = `acode://user/login/${responseData.token}`; } - window.location.href = redirectUrl || "/dashboard"; + navigate(`${redirectUrl || "/dashboard"}`); }, 1000); } catch (error) { console.error(`Login attempt Failed: `, error); diff --git a/src/pages/Plugins.tsx b/src/pages/Plugins.tsx index 6a35855..19765e9 100644 --- a/src/pages/Plugins.tsx +++ b/src/pages/Plugins.tsx @@ -7,7 +7,7 @@ import { Trash2, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { DeletePluginDialog } from "@/components/ui/delete-plugin-dialog"; @@ -73,6 +73,7 @@ const adminFilters = [ }, ]; export default function Plugins() { + const navigate = useNavigate() const [searchQuery, setSearchQuery] = useState(""); const [selectedFilter, setSelectedFilter] = useState("default"); @@ -287,7 +288,7 @@ export default function Plugins() { variant="outline" size="sm" onClick={() => { - window.location.href = "/submit-plugin"; + navigate("/submit-plugin"); }} > diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx index 41e2727..241361a 100644 --- a/src/pages/Signup.tsx +++ b/src/pages/Signup.tsx @@ -134,7 +134,6 @@ export default function Signup() { }); setTimeout(() => { - // window.location.href = `/login${params?.redirect ? `?redirect=${params?.redirect}` : ""}` navigate(`/login${params?.redirect ? `?redirect=${params?.redirect}` : ""}`) }, 2000) } else { From 339dfe0a18611a9ea408b4fefc91795dc028ff3c Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Sat, 1 Nov 2025 19:35:45 +0530 Subject: [PATCH 03/14] update: profile management and login components - Updated logout handling to improve user feedback with toast notifications and added redirect functionality. - Refactored OTP verification process to ensure proper event handling and improved error messaging. - Adjusted button attributes in the Login component to indicate upcoming features and improved accessibility. --- .../dashboard/profile-management.tsx | 39 +++++++++++++------ src/pages/Login.tsx | 4 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/dashboard/profile-management.tsx b/src/components/dashboard/profile-management.tsx index 4dac8d7..54f8871 100644 --- a/src/components/dashboard/profile-management.tsx +++ b/src/components/dashboard/profile-management.tsx @@ -84,7 +84,7 @@ const handleLogOut = async ( ); const responseData = - response.headers.get("content-type") === "application/json" + response.headers.get("content-type").includes("application/json") ? await response.json() : null; if (responseData?.error || !response.ok) { @@ -93,6 +93,9 @@ const handleLogOut = async ( description: responseData.error || `Something went wrong, server responded empty (request status code: ${response.status}). Please try again.`, + duration: 1500, + variant: "destructive", + type: "background" }); // Bad Request, in this case means: response as {error: 'Not Logged in'} if (response.status === 400) { @@ -102,23 +105,27 @@ const handleLogOut = async ( } } else if (response.ok) { toast({ - title: "Logged Out!", + title: "Logged Out! Redirecting....", description: - responseData.message || "Logged Out Successfully, redirecting....", + responseData.message || "Logged Out Successfully", + duration: 4000, + type: "background" }); } + setTimeout(() => { + handleRedirect("/login") + }, 1000); + await queryClient.invalidateQueries({ queryKey: ["loggedInUser"], }); - setTimeout(() => { - window.location.href = "/login"; - }, 1000); } catch (error) { toast({ title: "Unable to Log Out!", - description: `Something went wrong, server responded empty (error: ${error.message}). Please try again.`, + description: `Something went wrong, server responded empty (error: ${error?.message}). Please try again.`, + duration: 4000, }); } }; @@ -159,6 +166,8 @@ const handleUpdateProfile = async ( title: "Failed to Update Profile", description: `${responseData?.error} | redirecting....`, variant: "destructive", + duration: 1500, + type: "background" }); setTimeout(() => { handleRedirect("/login"); @@ -207,10 +216,10 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { const [showOTPDialog, setShowOTPDialog] = useState(false); // State to track the OTP input value - const otpRef = useRef(null); + const otpRef = useRef(null); // biome-ignore lint: Kept for Reference Only Should be Removed in PROD. - console.log(otpRef.current) + console.log("otpRef current Value", otpRef.current?.value) // State to track loading states const [isSubmitting, setIsSubmitting] = useState(false); @@ -237,11 +246,13 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { toast({ title: "Successfully Updated - User Profile", description: `${body.message}` || "User Updated", + duration: 5000, }); await queryClient.invalidateQueries({ queryKey: ["LoggedInUser"] }); setIsSubmitting(false); setIsVerifyingOTP(false); + setShowOTPDialog(false); setOriginalEmail(currentEmail); return; } @@ -253,6 +264,8 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { title: "Failed to Update Profile", description: `${error.message}`, variant: "destructive", + duration: 5000, + type: "background" }); setIsSubmitting(false) setOtpError(`${error.message}`); @@ -349,7 +362,9 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { }, [originalEmail, currentEmail, sendOTPToNewEmail, handleActualSubmit]); // Handle OTP verification - memoize callback - const handleOTPVerification = useCallback(async () => { + const handleOTPVerification = useCallback(async (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); console.log(otpRef.current?.value) console.log(otpRef.current?.value) if (!otpRef.current?.value?.trim()) { @@ -362,7 +377,7 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { if (otpRef.current?.value.length === 6 && /^\d+$/.test(otpRef.current?.value)) { // OTP is valid, proceed with form submission - await handleActualSubmit(Number(otpRef.current)); + await handleActualSubmit(Number(otpRef.current?.value)); } else { setOtpError("Invalid OTP. Please check and try again."); setIsVerifyingOTP(false); @@ -495,7 +510,7 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { {/* Radix-UI Dialog for OTP Verification */} - + !open && handleCancel()}> Verify Your New Email diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index d2ed00f..b2f1ffe 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -108,11 +108,11 @@ export default function Login() {
From 25436d8cc55c41a54dcfc16f707182177da0be4c Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Sat, 1 Nov 2025 19:36:46 +0530 Subject: [PATCH 04/14] remove(Login): console.log --- src/pages/Login.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index b2f1ffe..1ee72c1 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -24,12 +24,6 @@ export default function Login() { try { const formData = new FormData(e.target as HTMLFormElement); - console.log( - formData.get("email"), - formData.get("password"), - formData.get("password"), - ); - console.log(import.meta.env); const response = await fetch( `${import.meta.env.DEV ? import.meta.env.VITE_SERVER_URL : ""}/api/login`, { @@ -44,7 +38,7 @@ export default function Login() { .includes("application/json") ? await response.json() : null; - console.log(responseData); + if (responseData?.error || !response.ok) { setIsLoading(false); return toast({ From 2da20b35e08aac7d7298f7482a6cbc70ea551928 Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Sat, 1 Nov 2025 19:39:57 +0530 Subject: [PATCH 05/14] refactor(Dashboard): clean up imports and remove mock data - Removed unused imports and mock user data from the Dashboard component. - Simplified the component structure by retaining only necessary hooks and elements. - Updated the memoization of the current user to directly use the logged-in user data. --- src/pages/Dashboard.tsx | 51 ++--------------------------------------- 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 68cda0a..c9b4861 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,64 +1,20 @@ -import { - type QueryClient, - useMutation, - useQuery, - useQueryClient, -} from "@tanstack/react-query"; import { BarChart3, - Building2, - CheckCircle, - Clock, - CreditCard, - DollarSign, - Edit, - Eye, - LogOut, Plus, - Settings, - Shield, - X, XCircle, } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState, useRef } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { useEffect, useMemo } from "react"; +import { Link } from "react-router-dom"; import { EarningsOverview } from "@/components/dashboard/earnings-overview"; import { UserPluginsOverview } from "@/components/dashboard/user-plugins-overview"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { DeletePluginDialog } from "@/components/ui/delete-plugin-dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; -import { toast } from "@/hooks/use-toast.ts"; -import { useDeletePlugin } from "@/hooks/use-user-plugins"; import { useLoggedInUser } from "@/hooks/useLoggedInUser.ts"; -import { User } from "@/types"; import ProfileManagement from "@/components/dashboard/profile-management"; -// Mock user data -const currentMockUser = { - name: "John Doe", - email: "john@example.com", - role: "user", // "user" or "admin" - avatar: "JD", - joinDate: "2024-01-15", - bio: "Full-stack developer passionate about mobile development and creating tools that enhance productivity.", - website: "https://johndoe.dev", - github: "johndoe", - location: "San Francisco, CA", - totalEarnings: 245.67, - bankAccount: { - accountHolder: "John Doe", - bankName: "Chase Bank", - accountNumber: "****1234", - routingNumber: "****567", - }, -}; - // Note: Mock data removed - using real API data const LoadingDashboard = () => { @@ -422,8 +378,6 @@ export default function Dashboard() { // const queryClient = useQueryClient(); // const deletePluginMutation = useDeletePlugin(); - // Used for Navigation instead of `window.href.location` - const navigate = useNavigate() const { data: currentLoggedUser, @@ -434,7 +388,6 @@ export default function Dashboard() { // Memoize currentUser to prevent unnecessary re-renders const currentUser = useMemo(() => ({ - ...currentMockUser, ...currentLoggedUser, }), [currentLoggedUser]); From a1154aba7ba212a0c92cf3834bd67bb7cdf22e1f Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Sat, 1 Nov 2025 21:37:32 +0530 Subject: [PATCH 06/14] feat: enhance profile management with ZOD form validation - Integrated @tanstack/react-form for improved form handling in the ProfileManagement component. - Added validation schema using zod for user inputs including name, email, website, and GitHub ID. - Refactored form submission logic to utilize the new form library, enhancing user experience and feedback. - Cleaned up unused imports and improved code readability. --- package-lock.json | 128 +++++++++++- package.json | 3 +- .../dashboard/profile-management.tsx | 196 ++++++++++++------ src/lib/utils.ts | 5 + src/pages/Login.tsx | 6 +- 5 files changed, 273 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 784284b..b282cb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@tanstack/react-form": "^1.23.8", "@tanstack/react-query": "^5.83.0", "@types/jszip": "^3.4.1", "@types/markdown-it": "^14.1.2", @@ -63,7 +64,7 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.3", - "zod": "^3.23.8" + "zod": "^4.1.12" }, "devDependencies": { "@biomejs/biome": "2.1.2", @@ -3917,6 +3918,51 @@ "node": ">=4" } }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.3.4.tgz", + "integrity": "sha512-eq+PpuutUyubXu+ycC1GIiVwBs86NF/8yYJJAKSpPcJLWl6R/761F1H4F/9ziX6zKezltFUH1ah3Cz8Ah+KJrw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/form-core": { + "version": "1.24.4", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.24.4.tgz", + "integrity": "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.3.3", + "@tanstack/pacer": "^0.15.3", + "@tanstack/store": "^0.7.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/pacer": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.15.4.tgz", + "integrity": "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.3.2", + "@tanstack/store": "^0.7.5" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-core": { "version": "5.83.0", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz", @@ -3927,6 +3973,31 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/react-form": { + "version": "1.23.8", + "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.23.8.tgz", + "integrity": "sha512-ivfkiOHAI3aIWkCY4FnPWVAL6SkQWGWNVjtwIZpaoJE4ulukZWZ1KB8TQKs8f4STl+egjTsMHrWJuf2fv3Xh1w==", + "license": "MIT", + "dependencies": { + "@tanstack/form-core": "1.24.4", + "@tanstack/react-store": "^0.7.7", + "decode-formdata": "^0.9.0", + "devalue": "^5.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-start": "^1.130.10", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/react-start": { + "optional": true + } + } + }, "node_modules/@tanstack/react-query": { "version": "5.83.0", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz", @@ -3943,6 +4014,34 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.7.tgz", + "integrity": "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.7", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", + "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -5340,6 +5439,12 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-formdata": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/decode-formdata/-/decode-formdata-0.9.0.tgz", + "integrity": "sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==", + "license": "MIT" + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -5368,6 +5473,12 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devalue": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz", + "integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==", + "license": "MIT" + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8415,6 +8526,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8712,9 +8832,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 19e7ac7..56e63d1 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.4", + "@tanstack/react-form": "^1.23.8", "@tanstack/react-query": "^5.83.0", "@types/jszip": "^3.4.1", "@types/markdown-it": "^14.1.2", @@ -67,7 +68,7 @@ "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.3", - "zod": "^3.23.8" + "zod": "^4.1.12" }, "devDependencies": { "@biomejs/biome": "2.1.2", diff --git a/src/components/dashboard/profile-management.tsx b/src/components/dashboard/profile-management.tsx index 54f8871..e53ef1f 100644 --- a/src/components/dashboard/profile-management.tsx +++ b/src/components/dashboard/profile-management.tsx @@ -1,28 +1,15 @@ import { - BarChart3, - Building2, - CheckCircle, - Clock, - CreditCard, - DollarSign, - Edit, - Eye, LogOut, - Plus, Settings, - Shield, - X, - XCircle, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, useRef, memo } from "react"; -import { Link, redirect, useNavigate } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; import { PaymentMethods } from "@/components/dashboard/payment-methods"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { DeletePluginDialog } from "@/components/ui/delete-plugin-dialog"; import { Dialog, DialogClose, @@ -34,9 +21,12 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/hooks/use-toast.ts"; -import { User } from "@/types"; +import type { User } from "@/types"; +import { z } from "zod" +import { useForm } from '@tanstack/react-form' +import type { AnyFieldApi } from '@tanstack/react-form' +import { isValidGithubId } from "@/lib/utils"; // Mock user data const currentMockUser = { @@ -191,17 +181,53 @@ const handleUpdateProfile = async ( }; const hasEmailChanged = (originalEmail: string, currentEmail: string) => { + console.log("hasEmailChanged :: ", { originalEmail, currentEmail}) return originalEmail !== currentEmail; }; +function FieldInfo({ field }: { field: AnyFieldApi }) { + console.log(field.state) + return ( + <> + {field.state.meta.isTouched && !field.state.meta.isValid ? ( + {field.state.meta.errors.map(e => e?.message).join(", ")} + ) : null} + {field.state.meta.isValidating ? 'Validating...' : null} + + ) +} type ProfileManagementProps = { currentUser: User; }; +const profileManagementSchema = z.object({ + name: z.string().trim().min(3, "Name must be at least 3 characters long").max(255, "Name must not exceed 255 characters"), + email: z.email().max(255, "Email must not exceed 255 characters").toLowerCase(), + website: z.url().trim().toLowerCase(), + github: z.string().trim().refine(val => isValidGithubId(val), "Github Id Must be Valid").max(255, "Github Id Must Not Exceed 255 characters.") +}) + // Memoized ProfileManagement component to prevent unnecessary re-renders const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { + console.log(currentUser) const navigate = useNavigate() + const form = useForm({ + defaultValues: { + name: currentUser?.name || "", + email: currentUser?.email || "", + website: currentUser?.website || "", + github: currentUser?.github || "", + }, + + onSubmit: async (values) => { + console.log("tanstack form :: on-submit values ", values) + }, + validators: { + onChange: profileManagementSchema + } + }); + // Form input states const [name, setName] = useState(currentUser?.name || ""); const [currentEmail, setCurrentEmail] = useState(currentUser?.email || ""); @@ -452,60 +478,114 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { Change Avatar
-
handleSubmit(e)} className="space-y-4"> + { + e.preventDefault() + e.stopPropagation() + form.handleSubmit() + }} className="space-y-4">
+ { + return ( + <> +
+ + field.handleChange(e.target.value)} + /> + +
+ + ) + }} + /> +
- - setName(e.target.value)} - /> -
-
- - setCurrentEmail(e.target.value)} + { + return ( + <> + + field.handleChange(e.target.value)} + /> + + {/* FIX: Checks on blurs */} + {/* Visual indicator when email has changed */} + {hasEmailChanged(originalEmail, field.state.value) && ( +

+ Email will be changed from: {originalEmail} +

+ )} + + ) + }} /> - {/* Visual indicator when email has changed */} - {hasEmailChanged(originalEmail, currentEmail) && ( -

- Email will be changed from: {originalEmail} -

- )}
- - setWebsite(e.target.value)} + { + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} />
- - setGithub(e.target.value)} + { + return ( + <> + + field.handleChange(e.target.value)} + /> + + + ) + }} />
- + [state.canSubmit, state.isSubmitting]} + children={([canSubmit, isSubmitting]) => ( + + )} + /> {/* Radix-UI Dialog for OTP Verification */} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index ac680b3..980abf5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -4,3 +4,8 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export function isValidGithubId(id: string) { + if (!id) return true; + return /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i.test(id); + } \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 1ee72c1..6163f23 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -103,6 +103,8 @@ export default function Login() {
- { From 19aa85c6f2b89db2ce10578722d44c6119db9767 Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Wed, 5 Nov 2025 21:40:40 +0530 Subject: [PATCH 12/14] refactor: clean up profile management component by removing mock user data - Removed unused mock user data from the ProfileManagement component to streamline the code. - Simplified imports by eliminating the unused superRefine from zod. --- .../dashboard/profile-management.tsx | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/components/dashboard/profile-management.tsx b/src/components/dashboard/profile-management.tsx index c17d145..9eccbc4 100644 --- a/src/components/dashboard/profile-management.tsx +++ b/src/components/dashboard/profile-management.tsx @@ -23,32 +23,12 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { toast } from "@/hooks/use-toast.ts"; import type { User } from "@/types"; -import { superRefine, z, } from "zod" +import { z } from "zod" import { useForm } from '@tanstack/react-form' import type { AnyFieldApi } from '@tanstack/react-form' import { isValidGithubId } from "@/lib/utils"; import { AuthContextState, useAuth } from "@/context/AuthContext"; -// Mock user data -const currentMockUser = { - name: "John Doe", - email: "john@example.com", - role: "user", // "user" or "admin" - avatar: "JD", - joinDate: "2024-01-15", - bio: "Full-stack developer passionate about mobile development and creating tools that enhance productivity.", - website: "https://johndoe.dev", - github: "johndoe", - location: "San Francisco, CA", - totalEarnings: 245.67, - bankAccount: { - accountHolder: "John Doe", - bankName: "Chase Bank", - accountNumber: "****1234", - routingNumber: "****567", - }, -}; - /** * Handles the user log out process. * This function typically invalidates or removes local authentication data, @@ -343,7 +323,7 @@ const ProfileManagement = memo(({ currentUser }: ProfileManagementProps) => { duration: 5000, type: "background" }); - + setOtpError(`${error.message}`); setIsVerifyingOTP(false) return; From 1bceb5bc65f7b9fcc6e0231a1297ba53755cef74 Mon Sep 17 00:00:00 2001 From: UnschooledGamer Date: Thu, 6 Nov 2025 21:47:21 +0530 Subject: [PATCH 13/14] refactor(Login): update redirect handling to use searchParams - Replaced useParams with useSearchParams to retrieve redirect URL in the Login component. - Improved redirect logic to handle navigation based on search parameters, enhancing user experience. --- src/pages/Login.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index bd25598..407b713 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,6 +1,6 @@ import { Eye, EyeOff, Github, Lock, LogIn, Mail } from "lucide-react"; import { useState } from "react"; -import { Link, redirect, useNavigate, useParams } from "react-router-dom"; +import { Link, redirect, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -18,8 +18,8 @@ export default function Login() { const { login } = useAuth() const { toast } = useToast(); const navigate = useNavigate(); - const params = useParams(); - + const [searchParams] = useSearchParams(); + console.log(searchParams) const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); @@ -52,10 +52,13 @@ export default function Login() { }); setTimeout(() => { - let redirectUrl = params?.redirect as string; + let redirectUrl = searchParams.get("redirect") as string; setIsLoading(false); - if (params.redirect === "app") { + console.log(searchParams, redirectUrl) + if (searchParams.get("redirect") === "app") { redirectUrl = `acode://user/login/${responseData.token}`; + window.location.href = `${redirectUrl}` + return; } navigate(`${redirectUrl || "/dashboard"}`); From f089059e208e0c09b847a7e0074e9e2342aa4a20 Mon Sep 17 00:00:00 2001 From: UnschooledGamer <76094069+UnschooledGamer@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:58:50 +0530 Subject: [PATCH 14/14] refactor(UserPluginsOverview): adjust grid layout for improved responsiveness - Modified the grid layout in the UserPluginsOverview component to use 2 columns on smaller screens, enhancing the visual presentation and responsiveness of the stats section. --- src/components/dashboard/user-plugins-overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/user-plugins-overview.tsx b/src/components/dashboard/user-plugins-overview.tsx index 15c6711..98972ac 100644 --- a/src/components/dashboard/user-plugins-overview.tsx +++ b/src/components/dashboard/user-plugins-overview.tsx @@ -246,7 +246,7 @@ export function UserPluginsOverview() { {/* Stats */} -
+
{totalPlugins}