From 9e298b0232c172eb0ec93001eb4d5501dddbc360 Mon Sep 17 00:00:00 2001 From: Keshav Date: Tue, 5 Aug 2025 20:09:40 +0530 Subject: [PATCH 1/2] feat: implement glassmorphism design for login page UI Signed-off-by: Keshav --- AppUi/src/pages/auth/Login.jsx | 111 +++++++++++++++++++-------- AppUi/src/pages/landing.jsx | 31 ++++---- AppUi/src/store/slices/authSlice.jsx | 56 ++++++++++++-- 3 files changed, 142 insertions(+), 56 deletions(-) diff --git a/AppUi/src/pages/auth/Login.jsx b/AppUi/src/pages/auth/Login.jsx index 1840a81..4ad1099 100644 --- a/AppUi/src/pages/auth/Login.jsx +++ b/AppUi/src/pages/auth/Login.jsx @@ -1,43 +1,88 @@ +import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { login, logout } from "../../store/slices/authSlice"; +import { login } from "../../store/slices/authSlice"; import { useNavigate } from "react-router-dom"; -import { ROLES } from "../../utils/constants/roles"; -import { ADMIN_PATH } from "../../routes/admin/AdminPaths"; -import Button from "../../components/common/Buttons/ButtonComponent"; const LoginPage = () => { - const dispath = useDispatch(); - const navigate = useNavigate(); - const { isAuthenticated, role } = useSelector((state) => state.auth); + const dispatch = useDispatch(); + const navigate = useNavigate(); - const handleLogin = () => { - dispath( - login({ - user: { name: "Ayush" }, - role: ROLES.ADMIN, - }) - ); - navigate(ADMIN_PATH.DASHBOARD); - }; + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const { isLoading } = useSelector((state) => state.auth); + + const handleLogin = async () => { + setError(""); - const handleLogout = () => { - dispath(logout()); - }; + if (!email || !password) { + setError("Email and password are required"); + return; + } - return ( -
-

Login Page

-

Staus: {isAuthenticated ? `Logged in as ${role}` : "Not Logged In"}

- -
- -
-
- ); + try { + await dispatch( + login({ + user: { name: "Mock User", email }, + role: "admin", + }) + ); + navigate("/dashboard"); + } catch (err) { + setError("Login failed. Please try again."); + } + }; + + return ( +
+
+

+ LOGIN +

+
+
+ setEmail(e.target.value)} + className="w-full px-6 py-4 rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-200" + /> +
+
+ setPassword(e.target.value)} + className="w-full px-6 py-4 rounded-2xl bg-white/10 backdrop-blur-sm border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-white/40 transition-all duration-200" + /> +
+ {error && ( +

+ {error} +

+ )} +
+ +
+ +
+
+
+ ); }; export default LoginPage; diff --git a/AppUi/src/pages/landing.jsx b/AppUi/src/pages/landing.jsx index c94d3b1..d1d1314 100644 --- a/AppUi/src/pages/landing.jsx +++ b/AppUi/src/pages/landing.jsx @@ -1,21 +1,20 @@ import React from "react"; import Button from "../components/common/Buttons/ButtonComponent"; -import { useDispatch } from "react-redux"; -import { showToast } from "../store/slices/toastSlice"; +import { useNavigate } from "react-router-dom"; export default function Landing() { - const dispatch = useDispatch(); - const btnHandle = () => { - dispatch(showToast({ - message: "Login Successful!", - type: "success" })); - }; - return ( -
- Landing Page.
- -
- ); + const navigate = useNavigate(); + + const handleLoginRedirect = () => { + navigate("/login"); + }; + + return ( +
+ Landing Page.
+ +
+ ); } diff --git a/AppUi/src/store/slices/authSlice.jsx b/AppUi/src/store/slices/authSlice.jsx index 52c61e4..9c2e44f 100644 --- a/AppUi/src/store/slices/authSlice.jsx +++ b/AppUi/src/store/slices/authSlice.jsx @@ -1,28 +1,70 @@ -import { createSlice } from "@reduxjs/toolkit" +import { createSlice } from "@reduxjs/toolkit"; import { ROLES } from "../../utils/constants/roles"; const initialState = { isAuthenticated: false, role: ROLES.GUEST, user: null, -} + isLoading: false, + error: null, +}; const authSlice = createSlice({ - name: 'auth', + name: "auth", initialState, reducers: { - login: (state, action) => { + loginStart: (state) => { + state.isLoading = true; + state.error = null; + }, + loginSuccess: (state, action) => { state.isAuthenticated = true; state.user = action.payload.user; state.role = action.payload.role; + state.isLoading = false; + state.error = null; + }, + loginFailure: (state, action) => { + state.isAuthenticated = false; + state.user = null; + state.role = ROLES.GUEST; + state.isLoading = false; + state.error = action.payload || "Login failed"; }, - logout: (state, action) => { + logout: (state) => { state.isAuthenticated = false; state.user = null; state.role = ROLES.GUEST; + state.isLoading = false; + state.error = null; }, }, }); -export const {login, logout} = authSlice.actions; -export default authSlice.reducer; \ No newline at end of file +// Async login action (mocked) +export const login = (credentials) => async (dispatch) => { + try { + dispatch(loginStart()); + + // Simulating API delay here for testing + await new Promise((resolve) => setTimeout(resolve, 15000)); + + // You can add email/password validation here if needed + dispatch( + loginSuccess({ + user: { + name: credentials.user.name, + email: credentials.user.email, + }, + role: credentials.role || ROLES.USER, + }) + ); + } catch (error) { + dispatch(loginFailure("Something went wrong during login.")); + } +}; + +export const { loginStart, loginSuccess, loginFailure, logout } = + authSlice.actions; + +export default authSlice.reducer; From 759cc19476696a41407451b73e3cb5e9b7dc2c9a Mon Sep 17 00:00:00 2001 From: Keshav Date: Wed, 6 Aug 2025 00:36:38 +0530 Subject: [PATCH 2/2] feat: improved glassmorphism design for login page UI Signed-off-by: Keshav --- .../common/Buttons/ButtonComponent.jsx | 218 ++--- .../components/common/Form/FormComponents.jsx | 793 ++++++++++++------ AppUi/src/pages/auth/Login.jsx | 32 +- 3 files changed, 680 insertions(+), 363 deletions(-) diff --git a/AppUi/src/components/common/Buttons/ButtonComponent.jsx b/AppUi/src/components/common/Buttons/ButtonComponent.jsx index 9dc326e..5cd931e 100644 --- a/AppUi/src/components/common/Buttons/ButtonComponent.jsx +++ b/AppUi/src/components/common/Buttons/ButtonComponent.jsx @@ -1,115 +1,125 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva } from "class-variance-authority" -import { Loader2 } from "lucide-react" -import PropTypes from "prop-types" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva } from "class-variance-authority"; +import { Loader2 } from "lucide-react"; +import PropTypes from "prop-types"; -import { cn } from "../../../utils/utils" +import { cn } from "../../../utils/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-bold tracking-wide uppercase ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 relative overflow-hidden", - { - variants: { - variant: { - primary: - "bg-gradient-primary text-primary-foreground hover:shadow-glow-primary hover:scale-105 active:scale-95 border border-primary/20", - secondary: - "bg-gradient-secondary text-secondary-foreground hover:shadow-glow-secondary hover:scale-105 active:scale-95 border border-secondary/20", - danger: - "bg-gradient-danger text-destructive-foreground hover:shadow-glow-danger hover:scale-105 active:scale-95 border border-destructive/20", - success: - "bg-gradient-success text-primary-foreground hover:shadow-[0_0_20px_hsl(var(--game-success)/0.5)] hover:scale-105 active:scale-95 border border-game-success/20", - outline: - "border-2 border-primary bg-transparent text-primary hover:bg-primary hover:text-primary-foreground hover:shadow-glow-primary hover:scale-105 active:scale-95", - ghost: - "hover:bg-accent hover:text-accent-foreground hover:scale-105 active:scale-95", - disabled: - "bg-game-disabled text-muted-foreground cursor-not-allowed opacity-60", - link: - "text-primary underline-offset-4 hover:underline hover:text-primary/80", - }, - size: { - default: "h-12 px-6 py-3 text-sm", - sm: "h-9 rounded-md px-4 text-xs", - lg: "h-14 rounded-xl px-8 text-base", - icon: "h-12 w-12", - }, - }, - defaultVariants: { - variant: "primary", - size: "default", - }, - } -) + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-bold tracking-wide uppercase ring-offset-background transition-all duration-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 relative overflow-hidden", + { + variants: { + variant: { + primary: + "bg-gradient-primary text-primary-foreground hover:shadow-glow-primary hover:scale-105 active:scale-95 border border-primary/20", + secondary: + "bg-gradient-secondary text-secondary-foreground hover:shadow-glow-secondary hover:scale-105 active:scale-95 border border-secondary/20", + danger: "bg-gradient-danger text-destructive-foreground hover:shadow-glow-danger hover:scale-105 active:scale-95 border border-destructive/20", + success: + "bg-gradient-success text-primary-foreground hover:shadow-[0_0_20px_hsl(var(--game-success)/0.5)] hover:scale-105 active:scale-95 border border-game-success/20", + outline: + "border-2 border-primary bg-transparent text-primary hover:bg-primary hover:text-primary-foreground hover:shadow-glow-primary hover:scale-105 active:scale-95", + ghost: "hover:bg-accent hover:text-accent-foreground hover:scale-105 active:scale-95", + disabled: + "bg-game-disabled text-muted-foreground cursor-not-allowed opacity-60", + link: "text-primary underline-offset-4 hover:underline hover:text-primary/80", + }, + size: { + default: "h-12 px-6 py-3 text-sm", + sm: "h-9 rounded-md px-4 text-xs", + lg: "h-14 rounded-xl px-8 text-base", + icon: "h-12 w-12", + }, + }, + defaultVariants: { + variant: "primary", + size: "default", + }, + } +); const Button = React.forwardRef( - ({ className, variant, size, asChild = false, isLoading = false, disabled, children, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - const isDisabled = disabled || isLoading || variant === "disabled" - - // Only pass disabled prop to button elements, not to other elements when using asChild - const componentProps = { - className: cn( - buttonVariants({ - variant: isDisabled ? "disabled" : variant, - size, - className - }), - isLoading && "animate-pulse-glow" - ), - ref, - ...props - } - - // Only add disabled prop for button elements - if (!asChild) { - componentProps.disabled = isDisabled - } - - if (asChild) { - // For asChild, we need to clone the child and merge props - return ( - - {React.cloneElement(children, { - className: cn(children.props.className, componentProps.className) - })} - - ) + ( + { + className, + variant, + size, + asChild = false, + isLoading = false, + disabled, + children, + ...props + }, + ref + ) => { + const Comp = asChild ? Slot : "button"; + const isDisabled = disabled || isLoading || variant === "disabled"; + + // Only pass disabled prop to button elements, not to other elements when using asChild + const componentProps = { + className: cn( + buttonVariants({ + variant: isDisabled ? "disabled" : variant, + size, + className, + }), + isLoading && "animate-pulse-glow" + ), + ref, + ...props, + }; + + // Only add disabled prop for button elements + if (!asChild) { + componentProps.disabled = isDisabled; + } + + if (asChild) { + // For asChild, we need to clone the child and merge props + return ( + + {React.cloneElement(children, { + className: cn( + children.props.className, + componentProps.className + ), + })} + + ); + } + + return ( + + {isLoading && } + {children} + {/* Gaming button shine effect */} +
+ + ); } - - return ( - - {isLoading && ( - - )} - {children} - {/* Gaming button shine effect */} -
- - ) - } -) +); -Button.displayName = "Button" +Button.displayName = "Button"; // PropTypes for runtime type checking Button.propTypes = { - className: PropTypes.string, - variant: PropTypes.oneOf([ - "primary", - "secondary", - "danger", - "success", - "outline", - "ghost", - "disabled", - "link" - ]), - size: PropTypes.oneOf(["default", "sm", "lg", "icon"]), - asChild: PropTypes.bool, - isLoading: PropTypes.bool, - disabled: PropTypes.bool, - children: PropTypes.node, -} + className: PropTypes.string, + variant: PropTypes.oneOf([ + "primary", + "secondary", + "danger", + "success", + "outline", + "ghost", + "disabled", + "link", + ]), + size: PropTypes.oneOf(["default", "sm", "lg", "icon"]), + asChild: PropTypes.bool, + isLoading: PropTypes.bool, + disabled: PropTypes.bool, + children: PropTypes.node, +}; -export default Button +export default Button; diff --git a/AppUi/src/components/common/Form/FormComponents.jsx b/AppUi/src/components/common/Form/FormComponents.jsx index d08ae37..8f1b7d4 100644 --- a/AppUi/src/components/common/Form/FormComponents.jsx +++ b/AppUi/src/components/common/Form/FormComponents.jsx @@ -1,302 +1,599 @@ // FormComponents.jsx -import React, { useState, useRef, useEffect } from 'react'; -import { ChevronDown, Calendar, Upload, X, Check, Search, Eye, EyeOff, Loader2 } from 'lucide-react'; +import React, { useState, useRef, useEffect } from "react"; +import { + ChevronDown, + Calendar, + Upload, + X, + Check, + Search, + Eye, + EyeOff, + Loader2, +} from "lucide-react"; import { cva } from "class-variance-authority"; import { cn } from "../../../utils/utils"; - const inputBaseStyles = - "light-glass-input w-full rounded-lg text-slate-800 " + - "placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-300"; + "light-glass-input w-full rounded-lg text-slate-800 " + + "placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-all duration-300"; -const labelBaseStyles = "block text-sm font-semibold text-slate-800 mb-2 text-left"; +const labelBaseStyles = + "block text-sm font-semibold text-slate-800 mb-2 text-left"; const requiredIndicatorStyles = "text-orange-600 ml-1"; const errorTextStyles = "text-red-600 text-xs mt-1 text-left font-semibold"; const inputVariants = cva(inputBaseStyles, { - variants: { - variant: { - default: "", - error: "border-red-500/80 text-red-900 focus:ring-red-500", + variants: { + variant: { + default: "", + error: "border-red-500/80 text-red-900 focus:ring-red-500", + }, + size: { + default: "h-11 px-4 text-sm", + sm: "h-9 px-3 text-xs", + lg: "h-14 px-5 text-base", + }, }, - size: { - default: "h-11 px-4 text-sm", - sm: "h-9 px-3 text-xs", - lg: "h-14 px-5 text-base", + defaultVariants: { + variant: "default", + size: "default", }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, }); - const Input = React.forwardRef( - ({ label, id, name, type = 'text', value, onChange, error, required, size, placeholder, ...props }, ref) => { - const [showPassword, setShowPassword] = useState(false); - const inputType = type === 'password' && showPassword ? 'text' : type; + ( + { + label, + id, + name, + type = "text", + value, + onChange, + error, + required, + size, + placeholder, + ...props + }, + ref + ) => { + const [showPassword, setShowPassword] = useState(false); + const inputType = type === "password" && showPassword ? "text" : type; - return ( -
- {label && ( - - )} -
- - {type === 'password' && ( - - )} -
- {error &&

{error}

} -
- ); - } + return ( +
+ {label && ( + + )} +
+ + {type === "password" && ( + + )} +
+ {error &&

{error}

} +
+ ); + } ); Input.displayName = "Input"; - const SingleSelectDropdown = React.forwardRef( - ({ label, id, name, value, onChange, options, error, required, size, placeholder = "Select an option" }, ref) => { - const [isOpen, setIsOpen] = useState(false); - const wrapperRef = useRef(null); - - const selectedOption = options.find(opt => opt.value === value); + ( + { + label, + id, + name, + value, + onChange, + options, + error, + required, + size, + placeholder = "Select an option", + }, + ref + ) => { + const [isOpen, setIsOpen] = useState(false); + const wrapperRef = useRef(null); - useEffect(() => { - const handleClickOutside = (event) => { - if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { - setIsOpen(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [wrapperRef]); - - const handleSelectOption = (optionValue) => { - onChange({ target: { name, value: optionValue } }); - setIsOpen(false); - }; + const selectedOption = options.find((opt) => opt.value === value); - return ( -
- {label && } -
- {/* This button looks like an input and triggers the dropdown */} - + useEffect(() => { + const handleClickOutside = (event) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target) + ) { + setIsOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, [wrapperRef]); - {isOpen && ( -
-
    - {options.map(option => ( -
  • handleSelectOption(option.value)} - className={cn("flex items-center justify-between p-2 text-sm rounded-md cursor-pointer hover:bg-muted", option.value === value && "font-semibold")} - > - {option.label} - {option.value === value && } -
  • - ))} -
+ const handleSelectOption = (optionValue) => { + onChange({ target: { name, value: optionValue } }); + setIsOpen(false); + }; + + return ( +
+ {label && ( + + )} +
+ {/* This button looks like an input and triggers the dropdown */} + + + {isOpen && ( +
+
    + {options.map((option) => ( +
  • + handleSelectOption(option.value) + } + className={cn( + "flex items-center justify-between p-2 text-sm rounded-md cursor-pointer hover:bg-muted", + option.value === value && + "font-semibold" + )} + > + {option.label} + {option.value === value && ( + + )} +
  • + ))} +
+
+ )} +
+ {error &&

{error}

}
- )} -
- {error &&

{error}

} -
- ); - } + ); + } ); SingleSelectDropdown.displayName = "SingleSelectDropdown"; - const MultiSelectDropdown = React.forwardRef( - ({ label, id, name, selectedValues = [], onChange, options, error, required, placeholder = "Select options...", ...props }, ref) => { - const [isOpen, setIsOpen] = useState(false); - const wrapperRef = useRef(null); + ( + { + label, + id, + name, + selectedValues = [], + onChange, + options, + error, + required, + placeholder = "Select options...", + ...props + }, + ref + ) => { + const [isOpen, setIsOpen] = useState(false); + const wrapperRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event) { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target) + ) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, [wrapperRef]); + + const handleSelectOption = (value) => { + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + onChange({ target: { name, value: newSelectedValues } }); + }; - useEffect(() => { - function handleClickOutside(event) { - if (wrapperRef.current && !wrapperRef.current.contains(event.target)) { - setIsOpen(false); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [wrapperRef]); - - const handleSelectOption = (value) => { - const newSelectedValues = selectedValues.includes(value) - ? selectedValues.filter((v) => v !== value) - : [...selectedValues, value]; - onChange({ target: { name, value: newSelectedValues } }); - }; + const handleRemoveTag = (valueToRemove, e) => { + e.stopPropagation(); + const newSelectedValues = selectedValues.filter( + (v) => v !== valueToRemove + ); + onChange({ target: { name, value: newSelectedValues } }); + }; - const handleRemoveTag = (valueToRemove, e) => { - e.stopPropagation(); - const newSelectedValues = selectedValues.filter((v) => v !== valueToRemove); - onChange({ target: { name, value: newSelectedValues } }); - }; - - const selectedOptionsMap = options.reduce((acc, opt) => { - if (selectedValues.includes(opt.value)) { - acc[opt.value] = opt.label; - } - return acc; - }, {}); + const selectedOptionsMap = options.reduce((acc, opt) => { + if (selectedValues.includes(opt.value)) { + acc[opt.value] = opt.label; + } + return acc; + }, {}); - return ( -
- {label && ( - - )} -
-
setIsOpen(true)} - > - {selectedValues.map(value => ( - - {selectedOptionsMap[value]} - - - ))} - -
- {isOpen && ( -
-
    - {options.map(option => ( -
  • handleSelectOption(option.value)}> -
    - {selectedValues.includes(option.value) && } + return ( +
    + {label && ( + + )} +
    +
    setIsOpen(true)} + > + {selectedValues.map((value) => ( + + {selectedOptionsMap[value]} + + + ))} +
    - {option.label} -
  • - ))} -
+ {isOpen && ( +
+
    + {options.map((option) => ( +
  • + handleSelectOption(option.value) + } + > +
    + {selectedValues.includes( + option.value + ) && } +
    + + {option.label} + +
  • + ))} +
+
+ )} +
+ {error &&

{error}

}
- )} -
- {error &&

{error}

} -
- ); - } + ); + } ); MultiSelectDropdown.displayName = "MultiSelectDropdown"; - const DatePicker = React.forwardRef( - ({ label, id, name, value, onChange, error, required, size, ...props }, ref) => ( -
- {label && } -
- -
-
- {error &&

{error}

} -
- ) + ( + { label, id, name, value, onChange, error, required, size, ...props }, + ref + ) => ( +
+ {label && ( + + )} +
+ +
+ +
+
+ {error &&

{error}

} +
+ ) ); DatePicker.displayName = "DatePicker"; - const FileUpload = React.forwardRef( - ({ label, id, name, onChange, error, required, isLoading = false, ...props }, ref) => { - const [fileName, setFileName] = useState(''); - const handleFileChange = (e) => { - if (e.target.files?.length) setFileName(Array.from(e.target.files).map(file => file.name).join(', ')); - else setFileName(''); - if (onChange) onChange(e); - }; - return ( -
- {label && } - - {error &&

{error}

} -
- ); - } + ( + { + label, + id, + name, + onChange, + error, + required, + isLoading = false, + ...props + }, + ref + ) => { + const [fileName, setFileName] = useState(""); + const handleFileChange = (e) => { + if (e.target.files?.length) + setFileName( + Array.from(e.target.files) + .map((file) => file.name) + .join(", ") + ); + else setFileName(""); + if (onChange) onChange(e); + }; + return ( +
+ {label && ( + + )} + + {error &&

{error}

} +
+ ); + } ); FileUpload.displayName = "FileUpload"; - const RadioGroup = React.forwardRef( - ({ label, idPrefix, name, selectedValue, onChange, options, error, required, ...props }, ref) => ( -
- {label && {label} {required && *}} -
- {options.map((option, index) => ( - - ))} -
- {error &&

{error}

} -
- ) + ( + { + label, + idPrefix, + name, + selectedValue, + onChange, + options, + error, + required, + ...props + }, + ref + ) => ( +
+ {label && ( + + {label}{" "} + {required && ( + * + )} + + )} +
+ {options.map((option, index) => ( + + ))} +
+ {error &&

{error}

} +
+ ) ); RadioGroup.displayName = "RadioGroup"; - const Checkbox = React.forwardRef( - ({ label, id, name, checked, onChange, error, required, ...props }, ref) => ( -
-