Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
169 lines
4.8 KiB
TypeScript
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
|