admin/components/consumer/ConsumerButton.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

169 lines
4.8 KiB
TypeScript

'use client'
/**
* ConsumerButton — consumer-only actions (Figma-aligned).
*
* **When to use**
* - Any button, submit, or link-styled action in `app/(consumer)/**`,
* `components/consumer/**`, and shared consumer overlays (auth, PWA, booking).
*
* **When NOT to use**
* - Admin/dashboard and event-create wizard → `components/formElements/Button`.
* - Dual cancel/primary rows → `ConsumerActionButtons` (do not hand-roll pairs).
*
* **Props (design)**
* - `fill`: `orange` | `navy` | `fourth` | `fifth` | `gray` | `none` (default `orange`)
* - `size`: `xs` | `sm` | `md` | `lg` | `xl` — default `lg` text, `sm` icon-only
* - `radius`: `full` | `control` (10px)
* - `textColor`: `white`, Tailwind `text-*`, or hex — default white on filled
* - `fontSize`: default 14
* - `align`: `center` | `start` (RTL: start = right)
* - Icons: `iconStart` / `iconEnd` / `icon` + `iconPosition`
* - `isLoading` disables the control and swaps the icon slot for a spinner (label stays)
*
* **هم‌نام بودن ≠ هم‌اندازه بودن:** `size` here ≠ `ConsumerInput` `size` for
* the same token (see `frontend/docs/consumer-ui-guidelines.md` §4).
*
* **className policy:** layout-only (`flex-1`, `mt-5`, `w-full`, …) is OK.
* Do **not** pass visual overrides (`bg-*`, `text-*`, `rounded-*`, `min-h-*`)
* to match Figma — stop, tell the product owner, and extend this component
* after explicit approval.
*
* Guidelines: `frontend/docs/consumer-ui-guidelines.md`
*/
import Link from 'next/link'
import { Spinner } from '@heroui/react'
import { forwardRef } from 'react'
import { buildConsumerButtonStyle } from '@/components/consumer/consumerButtonStyles'
import type { ConsumerButtonProps } from '@/components/consumer/consumerButtonTypes'
export type { ConsumerButtonProps } from '@/components/consumer/consumerButtonTypes'
const ConsumerButton = forwardRef<HTMLButtonElement, ConsumerButtonProps>(
(
{
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-pressed': ariaPressed,
'aria-checked': ariaChecked,
'aria-selected': ariaSelected,
align = 'center',
children,
className,
radius = 'full',
disabled = false,
fill = 'orange',
fontSize = 14,
fullWidth = false,
icon,
iconEnd,
iconOnly = false,
iconPosition = 'start',
iconStart,
isLoading = false,
role,
size,
target = '_self',
textColor,
to,
type = 'button',
onClick,
},
ref
) => {
const isDisabled = disabled || isLoading
const resolvedIconStart = iconStart ?? (iconPosition === 'start' ? icon : undefined)
const resolvedIconEnd = iconEnd ?? (iconPosition === 'end' ? icon : undefined)
const { className: resolvedClassName, style } = buildConsumerButtonStyle({
align,
className,
radius,
fill,
fontSize,
fullWidth,
iconOnly,
size,
textColor,
})
const iconContent = resolvedIconStart ?? (iconOnly ? children : undefined)
const loadingSpinner = (
<Spinner
color="current"
size="sm"
/>
)
// لودینگ: فقط اسلات آیکن با اسپینر عوض شود؛ لیبل باقی بماند
const showStartSpinner = isLoading && Boolean(resolvedIconStart || !resolvedIconEnd)
const showEndSpinner = isLoading && !resolvedIconStart && Boolean(resolvedIconEnd)
const content = iconOnly ? (
isLoading ? (
loadingSpinner
) : (
iconContent
)
) : (
<>
{showStartSpinner ? loadingSpinner : resolvedIconStart}
{children}
{showEndSpinner ? loadingSpinner : isLoading ? null : resolvedIconEnd}
</>
)
const ariaProps = {
'aria-checked': ariaChecked,
'aria-controls': ariaControls,
'aria-expanded': ariaExpanded,
'aria-label': ariaLabel,
'aria-pressed': ariaPressed,
'aria-selected': ariaSelected,
role,
}
if (to) {
return (
<Link
{...ariaProps}
aria-disabled={isDisabled || undefined}
className={resolvedClassName}
href={to}
style={style}
target={target}
onClick={
isDisabled
? (event) => {
event.preventDefault()
}
: onClick
}
>
{content}
</Link>
)
}
return (
<button
ref={ref}
{...ariaProps}
className={resolvedClassName}
disabled={isDisabled}
style={style}
type={type}
onClick={onClick}
>
{content}
</button>
)
}
)
ConsumerButton.displayName = 'ConsumerButton'
export default ConsumerButton