Skip to content

Instantly share code, notes, and snippets.

@shramee
Created October 14, 2025 20:41
Show Gist options
  • Save shramee/dc25120d3f90ba75af586d9f50e3fa5e to your computer and use it in GitHub Desktop.
Save shramee/dc25120d3f90ba75af586d9f50e3fa5e to your computer and use it in GitHub Desktop.
Reusable UI components
import React, { ReactNode, useState } from 'react';
import { ChevronDown, Globe, Zap, DollarSign, User, Link, Mail, Coins, RotateCcw, Send, RefreshCw } from 'lucide-react';
export const baseUIBoxClasses = "w-full rounded-lg font-medium transition-colors flex items-center justify-center gap-2";
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'tertiary';
isLoading?: boolean;
className?: string;
action?: React.ReactNode;
}
export const Button = ({ children, action, variant = 'primary', className = '', isLoading = false, ...props }: ButtonProps) => {
const baseClasses = `${baseUIBoxClasses} cursor-pointer py-4 px-5`;
const variants = {
primary: "bg-yellow-400 text-black hover:bg-yellow-500",
secondary: "bg-blue-700 text-white hover:bg-blue-600",
tertiary: "bg-gray-700 text-white hover:bg-gray-600",
};
return (
<button
disabled={isLoading}
className={`${baseClasses} ${variants[variant]} ${className}`}
{...props}
>
{children}
{isLoading && <LoadingSpinner />}
{action && (
<span className="ml-auto inline-block" onClick={e => {
e.stopPropagation();
e.preventDefault();
}}>
{action}
</span>
)}
</button>
);
};
export interface DropdownOption {
id: string;
name: string;
icon: ReactNode;
color: string;
textColor: string;
}
export interface DropdownProps {
options: DropdownOption[];
selectedId: string;
onSelect: (option: DropdownOption) => void;
placeholder?: string;
}
type IconName = 'Zap' | 'DollarSign' | 'User' | 'Link' | 'Globe' | 'Mail' | 'Coins' | 'ChevronDown' | 'RotateCcw' | 'Send';
const iconMap: Record<IconName, React.ComponentType> = {
Zap,
DollarSign,
User,
Link,
Globe,
Mail,
Coins,
ChevronDown,
RotateCcw,
Send
};
export const getIcon = (IconName: ReactNode): ReactNode => {
if (typeof IconName == 'string') {
if (IconName.startsWith('<svg')) {
return IconName;
} else {
const Icon = iconMap[IconName as IconName];
return Icon ? <Icon /> : <Globe />;
}
}
return IconName
}
export const renderIcon = (option?: DropdownOption): ReactNode => {
const icon = option ? getIcon(option.icon) : <Globe />;
const className = "w-8 h-8 rounded-full flex items-center justify-center";
const style = { backgroundColor: option?.color || '#374151' };
if (typeof icon == 'string') {
return <div {...{ className, style, dangerouslySetInnerHTML: { __html: icon } }} />;
}
return <div {...{ className, style }}>{icon}</div>
}
export const Dropdown = ({ options, selectedId, onSelect, placeholder = "Select an option" }: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const selectedOption = options.find(option => option.id === selectedId);
const toggleDropdown = () => setIsOpen(!isOpen);
const handleSelect = (option: DropdownOption) => {
onSelect(option);
setIsOpen(false);
};
return (
<div className="relative">
<div
className="bg-gray-800 rounded-lg p-2 border border-gray-700 flex items-center justify-between cursor-pointer hover:bg-gray-750 transition-colors"
onClick={toggleDropdown}
>
<div className="flex items-center gap-3">
{renderIcon(selectedOption)}
<span className="text-white font-medium">
{selectedOption?.name || placeholder}
</span>
</div>
<ChevronDown
className={`w-5 h-5 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
/>
</div>
{
isOpen && (
<div className="absolute py-1 top-full left-0 right-0 mt-2 bg-gray-800 border border-gray-700 rounded-lg shadow-lg z-10 max-h-120 overflow-y-auto">
{options.map((option) => {
return (
<div
key={option.id}
className="p-3 py-2 flex items-center gap-3 cursor-pointer hover:bg-gray-700 transition-colors"
onClick={() => handleSelect(option)}
>
{renderIcon(option)}
<span className="text-white font-medium">{option.name}</span>
</div>
);
})}
</div>
)
}
</div >
);
};
export interface FieldProps {
label: string;
children: React.ReactNode;
subtitle?: string;
}
export const Field = ({ label, children, subtitle }: FieldProps) => (
<div className="mb-6">
<div className="mb-2">
<h3 className="text-white text-sm font-medium">{label}</h3>
</div>
{children}
{subtitle && <p className="text-gray-400 text-xs mt-1">{subtitle}</p>}
</div>
);
export interface InputFieldProps extends React.InputHTMLAttributes<HTMLInputElement> {
icon: {
bg?: string;
content: React.ReactNode;
};
action?: React.ReactNode;
}
export const InputField = ({ icon, action, ...inputProps }: InputFieldProps) => (
<div className="bg-gray-800 rounded-lg p-2 border border-gray-700 flex items-center gap-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={icon.bg ? { backgroundColor: icon.bg } : {}}>
{icon.content}
</div>
<input
type="text"
className="flex-1 bg-transparent text-white font-sm outline-none placeholder-gray-400"
{...inputProps}
/>
{action && (
<button type='button' className="text-gray-400 hover:text-white transition-colors">
{action}
</button>
)}
</div>
);
export function LoadingSpinner() {
// get an icons with spin animation
return <RefreshCw className="animate-spin" />
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment