Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { FC, ReactNode, FormEvent } from "react";
interface FormProps {
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
children: ReactNode;
className?: string;
}
const Form: FC<FormProps> = ({ onSubmit, children, className }) => {
return (
<form
onSubmit={(event) => {
event.preventDefault(); // Prevent default form submission
onSubmit(event);
}}
className={` ${className}`} // Default spacing between form fields
>
{children}
</form>
);
};
export default Form;

View File

@@ -0,0 +1,167 @@
/**
* FormFieldRenderer - Dynamic form field renderer
* Renders form fields based on configuration objects
*
* Usage:
* ```typescript
* <FormFieldRenderer
* fields={formFieldsConfig}
* values={formValues}
* onChange={handleFieldChange}
* />
* ```
*/
import React from 'react';
import Input from './input/InputField';
import SelectDropdown from './SelectDropdown';
import Label from './Label';
export interface FormFieldConfig {
key: string;
label: string;
type: 'text' | 'number' | 'email' | 'password' | 'select' | 'textarea';
placeholder?: string;
options?: Array<{ value: string; label: string }>;
required?: boolean;
min?: number;
max?: number;
rows?: number;
className?: string;
gridCols?: 1 | 2; // For inline fields (e.g., Volume & Difficulty)
}
interface FormFieldRendererProps {
fields: FormFieldConfig[];
values: Record<string, any>;
onChange: (key: string, value: any) => void;
errors?: Record<string, string>;
disabled?: boolean;
}
export default function FormFieldRenderer({
fields,
values,
onChange,
errors = {},
disabled = false,
}: FormFieldRendererProps) {
// Group fields by grid layout
const fieldGroups: FormFieldConfig[][] = [];
let currentGroup: FormFieldConfig[] = [];
fields.forEach((field) => {
if (field.gridCols === 2) {
// Start new group for inline fields
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
currentGroup.push(field);
if (currentGroup.length === 2) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
} else {
// Full-width field
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
fieldGroups.push([field]);
}
});
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
}
const renderField = (field: FormFieldConfig) => {
const value = values[field.key] || '';
const error = errors[field.key];
const fieldId = `field-${field.key}`;
return (
<div key={field.key} className={field.className}>
<Label htmlFor={fieldId} className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</Label>
{field.type === 'select' ? (
<SelectDropdown
options={field.options || []}
placeholder={field.placeholder || field.label}
value={value}
onChange={(val) => onChange(field.key, val)}
className="w-full"
/>
) : field.type === 'textarea' ? (
<textarea
id={fieldId}
className={`w-full rounded-lg border ${
error
? 'border-error-500'
: 'border-gray-300 dark:border-gray-700'
} bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
value={value}
onChange={(e) => onChange(field.key, e.target.value)}
placeholder={field.placeholder}
required={field.required}
rows={field.rows || 4}
disabled={disabled}
/>
) : (
<Input
id={fieldId}
type={field.type}
value={value}
onChange={(e) => {
const newValue =
field.type === 'number'
? e.target.value === ''
? ''
: parseInt(e.target.value) || 0
: e.target.value;
onChange(field.key, newValue);
}}
placeholder={field.placeholder}
required={field.required}
min={field.min}
max={field.max}
error={!!error}
disabled={disabled}
className="w-full"
/>
)}
{error && (
<p className="mt-1 text-sm text-error-500">{error}</p>
)}
</div>
);
};
return (
<div className="space-y-4">
{fieldGroups.map((group, groupIndex) => {
if (group.length === 2 && group[0].gridCols === 2) {
// Render inline fields
return (
<div key={`group-${groupIndex}`} className="grid grid-cols-2 gap-4">
{group.map((field) => renderField(field))}
</div>
);
} else {
// Render full-width field
return (
<div key={`group-${groupIndex}`}>
{group.map((field) => renderField(field))}
</div>
);
}
})}
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { FC, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
import { clsx } from "clsx";
interface LabelProps {
htmlFor?: string;
children: ReactNode;
className?: string;
}
const Label: FC<LabelProps> = ({ htmlFor, children, className }) => {
return (
<label
htmlFor={htmlFor}
className={clsx(
twMerge(
"mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400",
className,
),
)}
>
{children}
</label>
);
};
export default Label;

View File

@@ -0,0 +1,249 @@
import type React from "react";
import { useState, useEffect, useRef } from "react";
interface Option {
value: string;
text: string;
}
interface MultiSelectProps {
label: string;
options: Option[];
defaultSelected?: string[];
value?: string[];
onChange?: (selected: string[]) => void;
disabled?: boolean;
placeholder?: string;
}
const MultiSelect: React.FC<MultiSelectProps> = ({
label,
options,
defaultSelected = [],
value,
onChange,
disabled = false,
placeholder = "Select options",
}) => {
const isControlled = value !== undefined;
const [internalSelected, setInternalSelected] =
useState<string[]>(defaultSelected);
const selectedOptions = isControlled ? value : internalSelected;
const [isOpen, setIsOpen] = useState(false);
const [focusedIndex, setFocusedIndex] = useState(-1);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen]);
const updateSelection = (newSelected: string[]) => {
if (!isControlled) setInternalSelected(newSelected);
onChange?.(newSelected);
};
const toggleDropdown = () => {
if (!disabled) {
setIsOpen((prev) => !prev);
setFocusedIndex(-1);
}
};
const handleSelect = (optionValue: string) => {
const newSelected = selectedOptions.includes(optionValue)
? selectedOptions.filter((v) => v !== optionValue)
: [...selectedOptions, optionValue];
updateSelection(newSelected);
};
const removeOption = (optionValue: string) => {
updateSelection(selectedOptions.filter((v) => v !== optionValue));
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (disabled) return;
e.preventDefault();
switch (e.key) {
case "Enter":
if (!isOpen) {
setIsOpen(true);
} else if (focusedIndex >= 0) {
handleSelect(options[focusedIndex].value);
}
break;
case "Escape":
setIsOpen(false);
break;
case "ArrowDown":
if (!isOpen) {
setIsOpen(true);
} else {
setFocusedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
}
break;
case "ArrowUp":
if (isOpen) {
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
}
break;
}
};
return (
<div className="w-full" ref={dropdownRef}>
<label
className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400"
id={`${label}-label`}
>
{label}
</label>
<div className="relative z-20 inline-block w-full">
<div className="relative flex flex-col items-center">
<div
onClick={toggleDropdown}
onKeyDown={handleKeyDown}
className="w-full"
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-labelledby={`${label}-label`}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
>
<div
className={`mb-2 flex min-h-11 rounded-lg border border-gray-300 py-1.5 pl-3 pr-3 shadow-theme-xs outline-hidden transition focus:border-brand-300 focus:shadow-focus-ring dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-300 ${
disabled
? "opacity-50 cursor-not-allowed bg-gray-50 dark:bg-gray-800"
: "cursor-pointer"
}`}
>
<div className="flex flex-wrap flex-auto gap-2">
{selectedOptions.length > 0 ? (
selectedOptions.map((value) => {
const text =
options.find((opt) => opt.value === value)?.text || value;
return (
<div
key={value}
className="group flex items-center justify-center rounded-full border-[0.7px] border-transparent bg-gray-100 py-1 pl-2.5 pr-2 text-sm text-gray-800 hover:border-gray-200 dark:bg-gray-800 dark:text-white/90 dark:hover:border-gray-800"
>
<span className="flex-initial max-w-full">{text}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
if (!disabled) removeOption(value);
}}
disabled={disabled}
className="pl-2 text-gray-500 cursor-pointer group-hover:text-gray-400 dark:text-gray-400 disabled:cursor-not-allowed"
aria-label={`Remove ${text}`}
>
<svg
className="fill-current"
width="14"
height="14"
viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.40717 4.46881C3.11428 4.17591 3.11428 3.70104 3.40717 3.40815C3.70006 3.11525 4.17494 3.11525 4.46783 3.40815L6.99943 5.93975L9.53095 3.40822C9.82385 3.11533 10.2987 3.11533 10.5916 3.40822C10.8845 3.70112 10.8845 4.17599 10.5916 4.46888L8.06009 7.00041L10.5916 9.53193C10.8845 9.82482 10.8845 10.2997 10.5916 10.5926C10.2987 10.8855 9.82385 10.8855 9.53095 10.5926L6.99943 8.06107L4.46783 10.5927C4.17494 10.8856 3.70006 10.8856 3.40717 10.5927C3.11428 10.2998 3.11428 9.8249 3.40717 9.53201L5.93877 7.00041L3.40717 4.46881Z"
/>
</svg>
</button>
</div>
);
})
) : (
<div className="w-full h-full p-1 pr-2 text-sm text-gray-400 dark:text-gray-500 pointer-events-none">
{placeholder}
</div>
)}
</div>
<div className="flex items-center self-start py-1 pl-1 pr-1 w-7">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
toggleDropdown();
}}
disabled={disabled}
className="w-5 h-5 text-gray-700 outline-hidden cursor-pointer focus:outline-hidden dark:text-gray-400 disabled:cursor-not-allowed"
>
<svg
className={`stroke-current transition-transform ${
isOpen ? "rotate-180" : ""
}`}
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.79175 7.39551L10.0001 12.6038L15.2084 7.39551"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
</div>
</div>
{isOpen && (
<div
className="absolute left-0 z-40 w-full overflow-y-auto bg-white rounded-lg shadow-sm top-full max-h-select dark:bg-gray-900"
onClick={(e) => e.stopPropagation()}
role="listbox"
aria-label={label}
>
{options.map((option, index) => {
const isSelected = selectedOptions.includes(option.value);
const isFocused = index === focusedIndex;
return (
<div
key={option.value}
className={`hover:bg-primary/5 w-full cursor-pointer rounded-t border-b border-gray-200 dark:border-gray-800 ${
isFocused ? "bg-primary/5" : ""
} ${isSelected ? "bg-primary/10" : ""}`}
onClick={() => handleSelect(option.value)}
role="option"
aria-selected={isSelected}
>
<div className="relative flex w-full items-center p-2 pl-2">
<div className="mx-2 leading-6 text-gray-800 dark:text-white/90">
{option.text}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
);
};
export default MultiSelect;

View File

@@ -0,0 +1,64 @@
import { useState } from "react";
interface Option {
value: string;
label: string;
}
interface SelectProps {
options: Option[];
placeholder?: string;
onChange: (value: string) => void;
className?: string;
defaultValue?: string;
}
const Select: React.FC<SelectProps> = ({
options,
placeholder = "Select an option",
onChange,
className = "",
defaultValue = "",
}) => {
// Manage the selected value
const [selectedValue, setSelectedValue] = useState<string>(defaultValue);
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setSelectedValue(value);
onChange(value); // Trigger parent handler
};
return (
<select
className={`igny8-select-styled h-9 w-full appearance-none rounded-lg border border-gray-300 bg-transparent px-3 py-2 pr-10 text-sm shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 ${
selectedValue
? "text-gray-800 dark:text-white/90"
: "text-gray-400 dark:text-gray-400"
} ${className}`}
value={selectedValue}
onChange={handleChange}
>
{/* Placeholder option */}
<option
value=""
disabled
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
>
{placeholder}
</option>
{/* Map over options */}
{options.map((option) => (
<option
key={option.value}
value={option.value}
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
>
{option.label}
</option>
))}
</select>
);
};
export default Select;

View File

@@ -0,0 +1,162 @@
import { useState, useEffect, useRef } from "react";
import { ChevronDownIcon } from "../../icons";
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface SelectDropdownProps {
options: Option[];
placeholder?: string;
onChange: (value: string) => void;
className?: string;
defaultValue?: string;
value?: string; // Controlled value
disabled?: boolean;
}
const SelectDropdown: React.FC<SelectDropdownProps> = ({
options,
placeholder = "Select an option",
onChange,
className = "",
defaultValue = "",
value,
disabled = false,
}) => {
const isControlled = value !== undefined;
const [isOpen, setIsOpen] = useState(false);
const [internalValue, setInternalValue] = useState<string>(defaultValue);
// For controlled components, always use the value prop; for uncontrolled, use internal state
const selectedValue = isControlled ? (value || '') : internalValue;
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// Sync internalValue with defaultValue when it changes externally (only for uncontrolled)
useEffect(() => {
if (!isControlled) {
setInternalValue(defaultValue);
}
}, [defaultValue, isControlled]);
// When controlled, ensure selectedValue updates when value prop changes
// selectedValue is computed directly from value prop, no effect needed
// Get selected label - normalize values for comparison
const normalizedSelectedValue = String(selectedValue || '');
const selectedOption = options.find((opt) => String(opt.value || '') === normalizedSelectedValue);
const displayText = selectedOption ? selectedOption.label : placeholder;
const isPlaceholder = !selectedOption;
// Handle click outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}
}, [isOpen]);
// Handle option selection
const handleSelect = (val: string | undefined) => {
// Normalize: undefined/null becomes empty string for "All" options
const selectedVal = val === null || val === undefined ? '' : String(val);
if (!isControlled) {
setInternalValue(selectedVal);
}
// Always call onChange - this should trigger parent state update
onChange(selectedVal);
setIsOpen(false);
};
// Handle keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
setIsOpen(false);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setIsOpen(!isOpen);
}
};
return (
<div className={`relative ${className}`}>
{/* Trigger Button - styled like igny8-select-styled */}
<button
ref={buttonRef}
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
onKeyDown={handleKeyDown}
className={`igny8-select-styled h-9 w-full appearance-none rounded-lg border border-gray-300 bg-transparent px-3 py-2 pr-10 text-sm shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-800 ${
isPlaceholder
? "text-gray-400 dark:text-gray-400"
: "text-gray-800 dark:text-white/90"
} ${
isOpen
? "border-brand-300 ring-3 ring-brand-500/10 dark:border-brand-800"
: ""
} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
>
<span className="block text-left truncate">{displayText}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<ChevronDownIcon className={`h-4 w-4 text-gray-400 transition-transform ${isOpen ? 'transform rotate-180' : ''}`} />
</span>
</button>
{/* Dropdown Menu - styled like Dropdown component but compact */}
{isOpen && (
<div
ref={dropdownRef}
className="absolute z-50 left-0 right-0 mt-1 rounded-lg border border-gray-200 bg-white shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark overflow-hidden max-h-60 overflow-y-auto"
>
<div className="py-1">
{options.map((option) => {
const optionValue = String(option.value || ''); // Normalize option value
const normalizedSelected = String(selectedValue || '');
// Compare values strictly
const isSelected = normalizedSelected === optionValue;
return (
<button
key={`option-${option.value || 'empty'}-${option.label}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// Pass the normalized optionValue to ensure consistency
handleSelect(optionValue);
}}
className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 ${
isSelected
? "bg-brand-500 text-white"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
}`}
>
{option.icon && <span className="flex-shrink-0">{option.icon}</span>}
<span>{option.label}</span>
</button>
);
})}
</div>
</div>
)}
</div>
);
};
export default SelectDropdown;

View File

@@ -0,0 +1,60 @@
import { useEffect } from "react";
import flatpickr from "flatpickr";
import "flatpickr/dist/flatpickr.css";
import Label from "./Label";
import { CalenderIcon } from "../../icons";
import Hook = flatpickr.Options.Hook;
import DateOption = flatpickr.Options.DateOption;
type PropsType = {
id: string;
mode?: "single" | "multiple" | "range" | "time";
onChange?: Hook | Hook[];
defaultDate?: DateOption;
label?: string;
placeholder?: string;
};
export default function DatePicker({
id,
mode,
onChange,
label,
defaultDate,
placeholder,
}: PropsType) {
useEffect(() => {
const flatPickr = flatpickr(`#${id}`, {
mode: mode || "single",
static: true,
monthSelectorType: "static",
dateFormat: "Y-m-d",
defaultDate,
onChange,
});
return () => {
if (!Array.isArray(flatPickr)) {
flatPickr.destroy();
}
};
}, [mode, onChange, id, defaultDate]);
return (
<div>
{label && <Label htmlFor={id}>{label}</Label>}
<div className="relative">
<input
id={id}
placeholder={placeholder}
className="h-11 w-full rounded-lg border appearance-none px-4 py-2.5 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:focus:border-brand-800"
/>
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
<CalenderIcon className="size-6" />
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import Checkbox from "../input/Checkbox";
export default function CheckboxComponents() {
const [isChecked, setIsChecked] = useState(false);
const [isCheckedTwo, setIsCheckedTwo] = useState(true);
const [isCheckedDisabled, setIsCheckedDisabled] = useState(false);
return (
<ComponentCard title="Checkbox">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<Checkbox checked={isChecked} onChange={setIsChecked} />
<span className="block text-sm font-medium text-gray-700 dark:text-gray-400">
Default
</span>
</div>
<div className="flex items-center gap-3">
<Checkbox
checked={isCheckedTwo}
onChange={setIsCheckedTwo}
label="Checked"
/>
</div>
<div className="flex items-center gap-3">
<Checkbox
checked={isCheckedDisabled}
onChange={setIsCheckedDisabled}
disabled
label="Disabled"
/>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,115 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import Label from "../Label";
import Input from "../input/InputField";
import Select from "../Select";
import { EyeCloseIcon, EyeIcon, TimeIcon } from "../../../icons";
import DatePicker from "../date-picker.tsx";
export default function DefaultInputs() {
const [showPassword, setShowPassword] = useState(false);
const options = [
{ value: "marketing", label: "Marketing" },
{ value: "template", label: "Template" },
{ value: "development", label: "Development" },
];
const handleSelectChange = (value: string) => {
console.log("Selected value:", value);
};
return (
<ComponentCard title="Default Inputs">
<div className="space-y-6">
<div>
<Label htmlFor="input">Input</Label>
<Input type="text" id="input" />
</div>
<div>
<Label htmlFor="inputTwo">Input with Placeholder</Label>
<Input type="text" id="inputTwo" placeholder="info@gmail.com" />
</div>
<div>
<Label>Select Input</Label>
<Select
options={options}
placeholder="Select an option"
onChange={handleSelectChange}
className="dark:bg-dark-900"
/>
</div>
<div>
<Label>Password Input</Label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="Enter your password"
/>
<button
onClick={() => setShowPassword(!showPassword)}
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
>
{showPassword ? (
<EyeIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
) : (
<EyeCloseIcon className="fill-gray-500 dark:fill-gray-400 size-5" />
)}
</button>
</div>
</div>
<div>
<DatePicker
id="date-picker"
label="Date Picker Input"
placeholder="Select a date"
onChange={(dates, currentDateString) => {
// Handle your logic
console.log({ dates, currentDateString });
}}
/>
</div>
<div>
<Label htmlFor="tm">Time Picker Input</Label>
<div className="relative">
<Input
type="time"
id="tm"
name="tm"
onChange={(e) => console.log(e.target.value)}
/>
<span className="absolute text-gray-500 -translate-y-1/2 pointer-events-none right-3 top-1/2 dark:text-gray-400">
<TimeIcon className="size-6" />
</span>
</div>
</div>
<div>
<Label htmlFor="tm">Input with Payment</Label>
<div className="relative">
<Input
type="text"
placeholder="Card number"
className="pl-[62px]"
/>
<span className="absolute left-0 top-1/2 flex h-11 w-[46px] -translate-y-1/2 items-center justify-center border-r border-gray-200 dark:border-gray-800">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="6.25" cy="10" r="5.625" fill="#E80B26" />
<circle cx="13.75" cy="10" r="5.625" fill="#F59D31" />
<path
d="M10 14.1924C11.1508 13.1625 11.875 11.6657 11.875 9.99979C11.875 8.33383 11.1508 6.8371 10 5.80713C8.84918 6.8371 8.125 8.33383 8.125 9.99979C8.125 11.6657 8.84918 13.1625 10 14.1924Z"
fill="#FC6020"
/>
</svg>
</span>
</div>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,76 @@
import ComponentCard from "../../common/ComponentCard";
import { useDropzone } from "react-dropzone";
// import Dropzone from "react-dropzone";
const DropzoneComponent: React.FC = () => {
const onDrop = (acceptedFiles: File[]) => {
console.log("Files dropped:", acceptedFiles);
// Handle file uploads here
};
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: {
"image/png": [],
"image/jpeg": [],
"image/webp": [],
"image/svg+xml": [],
},
});
return (
<ComponentCard title="Dropzone">
<div className="transition border border-gray-300 border-dashed cursor-pointer dark:hover:border-brand-500 dark:border-gray-700 rounded-xl hover:border-brand-500">
<form
{...getRootProps()}
className={`dropzone rounded-xl border-dashed border-gray-300 p-7 lg:p-10
${
isDragActive
? "border-brand-500 bg-gray-100 dark:bg-gray-800"
: "border-gray-300 bg-gray-50 dark:border-gray-700 dark:bg-gray-900"
}
`}
id="demo-upload"
>
{/* Hidden Input */}
<input {...getInputProps()} />
<div className="dz-message flex flex-col items-center m-0!">
{/* Icon Container */}
<div className="mb-[22px] flex justify-center">
<div className="flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-400">
<svg
className="fill-current"
width="29"
height="28"
viewBox="0 0 29 28"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14.5019 3.91699C14.2852 3.91699 14.0899 4.00891 13.953 4.15589L8.57363 9.53186C8.28065 9.82466 8.2805 10.2995 8.5733 10.5925C8.8661 10.8855 9.34097 10.8857 9.63396 10.5929L13.7519 6.47752V18.667C13.7519 19.0812 14.0877 19.417 14.5019 19.417C14.9161 19.417 15.2519 19.0812 15.2519 18.667V6.48234L19.3653 10.5929C19.6583 10.8857 20.1332 10.8855 20.426 10.5925C20.7188 10.2995 20.7186 9.82463 20.4256 9.53184L15.0838 4.19378C14.9463 4.02488 14.7367 3.91699 14.5019 3.91699ZM5.91626 18.667C5.91626 18.2528 5.58047 17.917 5.16626 17.917C4.75205 17.917 4.41626 18.2528 4.41626 18.667V21.8337C4.41626 23.0763 5.42362 24.0837 6.66626 24.0837H22.3339C23.5766 24.0837 24.5839 23.0763 24.5839 21.8337V18.667C24.5839 18.2528 24.2482 17.917 23.8339 17.917C23.4197 17.917 23.0839 18.2528 23.0839 18.667V21.8337C23.0839 22.2479 22.7482 22.5837 22.3339 22.5837H6.66626C6.25205 22.5837 5.91626 22.2479 5.91626 21.8337V18.667Z"
/>
</svg>
</div>
</div>
{/* Text Content */}
<h4 className="mb-3 font-semibold text-gray-800 text-theme-xl dark:text-white/90">
{isDragActive ? "Drop Files Here" : "Drag & Drop Files Here"}
</h4>
<span className=" text-center mb-5 block w-full max-w-[290px] text-sm text-gray-700 dark:text-gray-400">
Drag and drop your PNG, JPG, WebP, SVG images here or browse
</span>
<span className="font-medium underline text-theme-sm text-brand-500">
Browse File
</span>
</div>
</form>
</div>
</ComponentCard>
);
};
export default DropzoneComponent;

View File

@@ -0,0 +1,21 @@
import ComponentCard from "../../common/ComponentCard";
import FileInput from "../input/FileInput";
import Label from "../Label";
export default function FileInputExample() {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
console.log("Selected file:", file.name);
}
};
return (
<ComponentCard title="File Input">
<div>
<Label>Upload file</Label>
<FileInput onChange={handleFileChange} className="custom-class" />
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,54 @@
import ComponentCard from "../../common/ComponentCard";
import Label from "../Label";
import Input from "../input/InputField";
import { EnvelopeIcon } from "../../../icons";
import PhoneInput from "../group-input/PhoneInput";
export default function InputGroup() {
const countries = [
{ code: "US", label: "+1" },
{ code: "GB", label: "+44" },
{ code: "CA", label: "+1" },
{ code: "AU", label: "+61" },
];
const handlePhoneNumberChange = (phoneNumber: string) => {
console.log("Updated phone number:", phoneNumber);
};
return (
<ComponentCard title="Input Group">
<div className="space-y-6">
<div>
<Label>Email</Label>
<div className="relative">
<Input
placeholder="info@gmail.com"
type="text"
className="pl-[62px]"
/>
<span className="absolute left-0 top-1/2 -translate-y-1/2 border-r border-gray-200 px-3.5 py-3 text-gray-500 dark:border-gray-800 dark:text-gray-400">
<EnvelopeIcon className="size-6" />
</span>
</div>
</div>
<div>
<Label>Phone</Label>
<PhoneInput
selectPosition="start"
countries={countries}
placeholder="+1 (555) 000-0000"
onChange={handlePhoneNumberChange}
/>
</div>{" "}
<div>
<Label>Phone</Label>
<PhoneInput
selectPosition="end"
countries={countries}
placeholder="+1 (555) 000-0000"
onChange={handlePhoneNumberChange}
/>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,73 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import Input from "../input/InputField";
import Label from "../Label";
export default function InputStates() {
const [email, setEmail] = useState("");
const [emailTwo, setEmailTwo] = useState("");
const [error, setError] = useState(false);
// Simulate a validation check
const validateEmail = (value: string) => {
const isValidEmail =
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
setError(!isValidEmail);
return isValidEmail;
};
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
validateEmail(value);
};
const handleEmailTwoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmailTwo(value);
validateEmail(value);
};
return (
<ComponentCard
title="Input States"
desc="Validation styles for error, success and disabled states on form controls."
>
<div className="space-y-5 sm:space-y-6">
{/* Error Input */}
<div>
<Label>Email</Label>
<Input
type="email"
value={email}
error={error}
onChange={handleEmailChange}
placeholder="Enter your email"
hint={error ? "This is an invalid email address." : ""}
/>
</div>
{/* Success Input */}
<div>
<Label>Email</Label>
<Input
type="email"
value={emailTwo}
success={!error}
onChange={handleEmailTwoChange}
placeholder="Enter your email"
hint={!error ? "This is an success message." : ""}
/>
</div>
{/* Disabled Input */}
<div>
<Label>Email</Label>
<Input
type="text"
value="disabled@example.com"
disabled={true}
placeholder="Disabled email"
/>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,42 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import Radio from "../input/Radio";
export default function RadioButtons() {
const [selectedValue, setSelectedValue] = useState<string>("option2");
const handleRadioChange = (value: string) => {
setSelectedValue(value);
};
return (
<ComponentCard title="Radio Buttons">
<div className="flex flex-wrap items-center gap-8">
<Radio
id="radio1"
name="group1"
value="option1"
checked={selectedValue === "option1"}
onChange={handleRadioChange}
label="Default"
/>
<Radio
id="radio2"
name="group1"
value="option2"
checked={selectedValue === "option2"}
onChange={handleRadioChange}
label="Selected"
/>
<Radio
id="radio3"
name="group1"
value="option3"
checked={selectedValue === "option3"}
onChange={handleRadioChange}
label="Disabled"
disabled={true}
/>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,51 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import Label from "../Label";
import Select from "../Select";
import MultiSelect from "../MultiSelect";
export default function SelectInputs() {
const options = [
{ value: "marketing", label: "Marketing" },
{ value: "template", label: "Template" },
{ value: "development", label: "Development" },
];
const handleSelectChange = (value: string) => {
console.log("Selected value:", value);
};
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const multiOptions = [
{ value: "1", text: "Option 1", selected: false },
{ value: "2", text: "Option 2", selected: false },
{ value: "3", text: "Option 3", selected: false },
{ value: "4", text: "Option 4", selected: false },
{ value: "5", text: "Option 5", selected: false },
];
return (
<ComponentCard title="Select Inputs">
<div className="space-y-6">
<div>
<Label>Select Input</Label>
<Select
options={options}
placeholder="Select Option"
onChange={handleSelectChange}
className="dark:bg-dark-900"
/>
</div>
<div>
<MultiSelect
label="Multiple Select Options"
options={multiOptions}
defaultSelected={["1", "3"]}
onChange={(values) => setSelectedValues(values)}
/>
<p className="sr-only">
Selected Values: {selectedValues.join(", ")}
</p>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,42 @@
import { useState } from "react";
import ComponentCard from "../../common/ComponentCard";
import TextArea from "../input/TextArea";
import Label from "../Label";
export default function TextAreaInput() {
const [message, setMessage] = useState("");
const [messageTwo, setMessageTwo] = useState("");
return (
<ComponentCard title="Textarea input field">
<div className="space-y-6">
{/* Default TextArea */}
<div>
<Label>Description</Label>
<TextArea
value={message}
onChange={(value) => setMessage(value)}
rows={6}
/>
</div>
{/* Disabled TextArea */}
<div>
<Label>Description</Label>
<TextArea rows={6} disabled />
</div>
{/* Error TextArea */}
<div>
<Label>Description</Label>
<TextArea
rows={6}
value={messageTwo}
error
onChange={(value) => setMessageTwo(value)}
hint="Please enter a valid message."
/>
</div>
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,40 @@
import ComponentCard from "../../common/ComponentCard";
import Switch from "../switch/Switch";
export default function ToggleSwitch() {
const handleSwitchChange = (checked: boolean) => {
console.log("Switch is now:", checked ? "ON" : "OFF");
};
return (
<ComponentCard title="Toggle switch input">
<div className="flex gap-4">
<Switch
label="Default"
defaultChecked={true}
onChange={handleSwitchChange}
/>
<Switch
label="Checked"
defaultChecked={true}
onChange={handleSwitchChange}
/>
<Switch label="Disabled" disabled={true} />
</div>{" "}
<div className="flex gap-4">
<Switch
label="Default"
defaultChecked={true}
onChange={handleSwitchChange}
color="gray"
/>
<Switch
label="Checked"
defaultChecked={true}
onChange={handleSwitchChange}
color="gray"
/>
<Switch label="Disabled" disabled={true} color="gray" />
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,140 @@
import { useState } from "react";
interface CountryCode {
code: string;
label: string;
}
interface PhoneInputProps {
countries: CountryCode[];
placeholder?: string;
onChange?: (phoneNumber: string) => void;
selectPosition?: "start" | "end"; // New prop for dropdown position
}
const PhoneInput: React.FC<PhoneInputProps> = ({
countries,
placeholder = "+1 (555) 000-0000",
onChange,
selectPosition = "start", // Default position is 'start'
}) => {
const [selectedCountry, setSelectedCountry] = useState<string>("US");
const [phoneNumber, setPhoneNumber] = useState<string>("+1");
const countryCodes: Record<string, string> = countries.reduce(
(acc, { code, label }) => ({ ...acc, [code]: label }),
{}
);
const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const newCountry = e.target.value;
setSelectedCountry(newCountry);
setPhoneNumber(countryCodes[newCountry]);
if (onChange) {
onChange(countryCodes[newCountry]);
}
};
const handlePhoneNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPhoneNumber = e.target.value;
setPhoneNumber(newPhoneNumber);
if (onChange) {
onChange(newPhoneNumber);
}
};
return (
<div className="relative flex">
{/* Dropdown position: Start */}
{selectPosition === "start" && (
<div className="absolute">
<select
value={selectedCountry}
onChange={handleCountryChange}
className="appearance-none bg-none rounded-l-lg border-0 border-r border-gray-200 bg-transparent py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
>
{countries.map((country) => (
<option
key={country.code}
value={country.code}
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
>
{country.code}
</option>
))}
</select>
<div className="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none bg-none right-3 dark:text-gray-400">
<svg
className="stroke-current"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
)}
{/* Input field */}
<input
type="tel"
value={phoneNumber}
onChange={handlePhoneNumberChange}
placeholder={placeholder}
className={`dark:bg-dark-900 h-11 w-full ${
selectPosition === "start" ? "pl-[84px]" : "pr-[84px]"
} rounded-lg border border-gray-300 bg-transparent py-3 px-4 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
/>
{/* Dropdown position: End */}
{selectPosition === "end" && (
<div className="absolute right-0">
<select
value={selectedCountry}
onChange={handleCountryChange}
className="appearance-none bg-none rounded-r-lg border-0 border-l border-gray-200 bg-transparent py-3 pl-3.5 pr-8 leading-tight text-gray-700 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:text-gray-400"
>
{countries.map((country) => (
<option
key={country.code}
value={country.code}
className="text-gray-700 dark:bg-gray-900 dark:text-gray-400"
>
{country.code}
</option>
))}
</select>
<div className="absolute inset-y-0 flex items-center text-gray-700 pointer-events-none right-3 dark:text-gray-400">
<svg
className="stroke-current"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.79175 7.396L10.0001 12.6043L15.2084 7.396"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
)}
</div>
);
};
export default PhoneInput;

View File

@@ -0,0 +1,82 @@
import type React from "react";
interface CheckboxProps {
label?: string;
checked: boolean;
className?: string;
id?: string;
onChange: (checked: boolean) => void;
disabled?: boolean;
}
const Checkbox: React.FC<CheckboxProps> = ({
label,
checked,
id,
onChange,
className = "",
disabled = false,
}) => {
return (
<label
className={`flex items-center space-x-3 group cursor-pointer ${
disabled ? "cursor-not-allowed opacity-60" : ""
}`}
>
<div className="relative w-4 h-4">
<input
id={id}
type="checkbox"
className={`w-4 h-4 appearance-none cursor-pointer dark:border-gray-700 border border-gray-300 checked:border-transparent rounded-md checked:bg-brand-500 disabled:opacity-60
${className}`}
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
/>
{checked && (
<svg
className="absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-none top-1/2 left-1/2"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<path
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
stroke="white"
strokeWidth="1.94437"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
{disabled && (
<svg
className="absolute transform -translate-x-1/2 -translate-y-1/2 pointer-events-none top-1/2 left-1/2"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<path
d="M11.6666 3.5L5.24992 9.91667L2.33325 7"
stroke="#E4E7EC"
strokeWidth="2.33333"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
{label && (
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
{label}
</span>
)}
</label>
);
};
export default Checkbox;

View File

@@ -0,0 +1,22 @@
import { FC } from "react";
interface FileInputProps {
className?: string;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
accept?: string;
disabled?: boolean;
}
const FileInput: FC<FileInputProps> = ({ className, onChange, accept, disabled = false }) => {
return (
<input
type="file"
accept={accept}
disabled={disabled}
className={`focus:border-ring-brand-300 h-11 w-full overflow-hidden rounded-lg border border-gray-300 bg-transparent text-sm text-gray-500 shadow-theme-xs transition-colors file:mr-5 file:border-collapse file:cursor-pointer file:rounded-l-lg file:border-0 file:border-r file:border-solid file:border-gray-200 file:bg-gray-50 file:py-3 file:pl-3.5 file:pr-3 file:text-sm file:text-gray-700 placeholder:text-gray-400 hover:file:bg-gray-100 focus:outline-hidden focus:file:ring-brand-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400 dark:text-white/90 dark:file:border-gray-800 dark:file:bg-white/[0.03] dark:file:text-gray-400 dark:placeholder:text-gray-400 disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
onChange={onChange}
/>
);
};
export default FileInput;

View File

@@ -0,0 +1,82 @@
import type React from "react";
import type { FC } from "react";
interface InputProps {
type?: "text" | "number" | "email" | "password" | "date" | "time" | string;
id?: string;
name?: string;
placeholder?: string;
value?: string | number;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
className?: string;
min?: string;
max?: string;
step?: number;
disabled?: boolean;
success?: boolean;
error?: boolean;
hint?: string;
}
const Input: FC<InputProps> = ({
type = "text",
id,
name,
placeholder,
value,
onChange,
className = "",
min,
max,
step,
disabled = false,
success = false,
error = false,
hint,
}) => {
let inputClasses = ` h-9 w-full rounded-lg border appearance-none px-3 py-2 text-sm shadow-theme-xs placeholder:text-gray-400 focus:outline-hidden focus:ring-3 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 ${className}`;
if (disabled) {
inputClasses += ` text-gray-500 border-gray-300 opacity-40 bg-gray-100 cursor-not-allowed dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700 opacity-40`;
} else if (error) {
inputClasses += ` border-error-500 focus:border-error-300 focus:ring-error-500/20 dark:text-error-400 dark:border-error-500 dark:focus:border-error-800`;
} else if (success) {
inputClasses += ` border-success-500 focus:border-success-300 focus:ring-success-500/20 dark:text-success-400 dark:border-success-500 dark:focus:border-success-800`;
} else {
inputClasses += ` bg-transparent text-gray-800 border-gray-300 focus:border-brand-300 focus:ring-brand-500/20 dark:border-gray-700 dark:text-white/90 dark:focus:border-brand-800`;
}
return (
<div className="relative">
<input
type={type}
id={id}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
min={min}
max={max}
step={step}
disabled={disabled}
className={inputClasses}
/>
{hint && (
<p
className={`mt-1.5 text-xs ${
error
? "text-error-500"
: success
? "text-success-500"
: "text-gray-500"
}`}
>
{hint}
</p>
)}
</div>
);
};
export default Input;

View File

@@ -0,0 +1,63 @@
interface RadioProps {
id: string; // Unique ID for the radio button
name: string; // Radio group name
value: string; // Value of the radio button
checked: boolean; // Whether the radio button is checked
label: string; // Label for the radio button
onChange: (value: string) => void; // Handler for value change
className?: string; // Optional additional classes
disabled?: boolean; // Optional disabled state for the radio button
}
const Radio: React.FC<RadioProps> = ({
id,
name,
value,
checked,
label,
onChange,
className = "",
disabled = false,
}) => {
return (
<label
htmlFor={id}
className={`relative flex cursor-pointer select-none items-center gap-3 text-sm font-medium ${
disabled
? "text-gray-300 dark:text-gray-600 cursor-not-allowed"
: "text-gray-700 dark:text-gray-400"
} ${className}`}
>
<input
id={id}
name={name}
type="radio"
value={value}
checked={checked}
onChange={() => !disabled && onChange(value)} // Prevent onChange when disabled
className="sr-only"
disabled={disabled} // Disable input
/>
<span
className={`flex h-5 w-5 items-center justify-center rounded-full border-[1.25px] ${
checked
? "border-brand-500 bg-brand-500"
: "bg-transparent border-gray-300 dark:border-gray-700"
} ${
disabled
? "bg-gray-100 dark:bg-gray-700 border-gray-200 dark:border-gray-700"
: ""
}`}
>
<span
className={`h-2 w-2 rounded-full bg-white ${
checked ? "block" : "hidden"
}`}
></span>
</span>
{label}
</label>
);
};
export default Radio;

View File

@@ -0,0 +1,57 @@
interface RadioProps {
id: string; // Unique ID for the radio button
name: string; // Group name for the radio button
value: string; // Value of the radio button
checked: boolean; // Whether the radio button is checked
label: string; // Label text for the radio button
onChange: (value: string) => void; // Handler for when the radio button is toggled
className?: string; // Optional custom classes for styling
}
const RadioSm: React.FC<RadioProps> = ({
id,
name,
value,
checked,
label,
onChange,
className = "",
}) => {
return (
<label
htmlFor={id}
className={`flex cursor-pointer select-none items-center text-sm text-gray-500 dark:text-gray-400 ${className}`}
>
<span className="relative">
{/* Hidden Input */}
<input
type="radio"
id={id}
name={name}
value={value}
checked={checked}
onChange={() => onChange(value)}
className="sr-only"
/>
{/* Styled Radio Circle */}
<span
className={`mr-2 flex h-4 w-4 items-center justify-center rounded-full border ${
checked
? "border-brand-500 bg-brand-500"
: "bg-transparent border-gray-300 dark:border-gray-700"
}`}
>
{/* Inner Dot */}
<span
className={`h-1.5 w-1.5 rounded-full ${
checked ? "bg-white" : "bg-white dark:bg-[#1e2636]"
}`}
></span>
</span>
</span>
{label}
</label>
);
};
export default RadioSm;

View File

@@ -0,0 +1,63 @@
import React from "react";
interface TextareaProps {
placeholder?: string; // Placeholder text
rows?: number; // Number of rows
value?: string; // Current value
onChange?: (value: string) => void; // Change handler
className?: string; // Additional CSS classes
disabled?: boolean; // Disabled state
error?: boolean; // Error state
hint?: string; // Hint text to display
}
const TextArea: React.FC<TextareaProps> = ({
placeholder = "Enter your message", // Default placeholder
rows = 3, // Default number of rows
value = "", // Default value
onChange, // Callback for changes
className = "", // Additional custom styles
disabled = false, // Disabled state
error = false, // Error state
hint = "", // Default hint text
}) => {
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (onChange) {
onChange(e.target.value);
}
};
let textareaClasses = `w-full rounded-lg border px-4 py-2.5 text-sm shadow-theme-xs focus:outline-hidden ${className} `;
if (disabled) {
textareaClasses += ` bg-gray-100 opacity-50 text-gray-500 border-gray-300 cursor-not-allowed opacity40 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700`;
} else if (error) {
textareaClasses += ` bg-transparent border-gray-300 focus:border-error-300 focus:ring-3 focus:ring-error-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-error-800`;
} else {
textareaClasses += ` bg-transparent text-gray-900 dark:text-gray-300 text-gray-900 border-gray-300 focus:border-brand-300 focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800`;
}
return (
<div className="relative">
<textarea
placeholder={placeholder}
rows={rows}
value={value}
onChange={handleChange}
disabled={disabled}
className={textareaClasses}
/>
{hint && (
<p
className={`mt-2 text-sm ${
error ? "text-error-500" : "text-gray-500 dark:text-gray-400"
}`}
>
{hint}
</p>
)}
</div>
);
};
export default TextArea;

View File

@@ -0,0 +1,99 @@
import { useState, useEffect } from "react";
interface SwitchProps {
label: string;
defaultChecked?: boolean;
checked?: boolean; // Controlled mode - when provided, component is controlled
disabled?: boolean;
onChange?: (checked: boolean) => void;
color?: "blue" | "gray"; // Added prop to toggle color theme
}
const Switch: React.FC<SwitchProps> = ({
label,
defaultChecked = false,
checked, // Controlled mode
disabled = false,
onChange,
color = "blue", // Default to blue color
}) => {
// Use controlled mode if 'checked' prop is provided, otherwise use uncontrolled
const isControlled = checked !== undefined;
const [internalChecked, setInternalChecked] = useState(defaultChecked);
// Sync internal state with controlled prop - only update when checked actually changes
useEffect(() => {
if (isControlled && checked !== internalChecked) {
setInternalChecked(checked);
}
}, [checked, isControlled, internalChecked]);
// Sync internal state with defaultChecked prop changes (for uncontrolled mode)
useEffect(() => {
if (!isControlled && defaultChecked !== internalChecked) {
setInternalChecked(defaultChecked);
}
}, [defaultChecked, isControlled, internalChecked]);
// Use controlled value directly, or fallback to internal state
const isChecked = isControlled ? (checked ?? false) : internalChecked;
const handleToggle = () => {
if (disabled) return;
const newCheckedState = !isChecked;
// Only update internal state if uncontrolled
if (!isControlled) {
setInternalChecked(newCheckedState);
}
// Always call onChange callback
if (onChange) {
onChange(newCheckedState);
}
};
const switchColors =
color === "blue"
? {
background: isChecked
? "bg-brand-500 "
: "bg-gray-200 dark:bg-white/10", // Blue version
knob: isChecked
? "translate-x-full bg-white"
: "translate-x-0 bg-white",
}
: {
background: isChecked
? "bg-gray-800 dark:bg-white/10"
: "bg-gray-200 dark:bg-white/10", // Gray version
knob: isChecked
? "translate-x-full bg-white"
: "translate-x-0 bg-white",
};
return (
<label
className={`flex cursor-pointer select-none items-center gap-3 text-sm font-medium ${
disabled ? "text-gray-400" : "text-gray-700 dark:text-gray-400"
}`}
onClick={handleToggle} // Toggle when the label itself is clicked
>
<div className="relative">
<div
className={`block transition duration-150 ease-linear h-6 w-11 rounded-full ${
disabled
? "bg-gray-100 pointer-events-none dark:bg-gray-800"
: switchColors.background
}`}
></div>
<div
className={`absolute left-0.5 top-0.5 h-5 w-5 rounded-full shadow-theme-sm duration-150 ease-linear transform ${switchColors.knob}`}
></div>
</div>
{label}
</label>
);
};
export default Switch;