Add shared empty states, customer product filters, and SSO handoff across dashboards.
Standardize list empty UI, searchable country fields, customer-products filter row, password/login polish, and one-time business dashboard handoff from the customer app. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
832ac32a09
commit
f8a9a28346
@@ -81,10 +81,12 @@
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: static;
|
||||
margin-top: 6px;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
inset-inline: 0;
|
||||
z-index: 120;
|
||||
max-height: min(280px, 50vh);
|
||||
overflow-y: auto;
|
||||
background: var(--elevated-surface);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
|
||||
@@ -14,6 +14,8 @@ interface CitySearchSelectProps {
|
||||
onChange: (value: string) => void
|
||||
disabled?: boolean
|
||||
placeholder?: string
|
||||
noResultsLabel?: string
|
||||
clearLabel?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
@@ -23,6 +25,8 @@ export function CitySearchSelect({
|
||||
onChange,
|
||||
disabled = false,
|
||||
placeholder,
|
||||
noResultsLabel,
|
||||
clearLabel,
|
||||
id,
|
||||
}: CitySearchSelectProps) {
|
||||
const t = useT()
|
||||
@@ -34,6 +38,8 @@ export function CitySearchSelect({
|
||||
const selected = options.find((option) => option.id === value)
|
||||
const selectedLabel = selected ? getLocationOptionLabel(selected, locale) : ''
|
||||
const searchPlaceholder = placeholder ?? t('customerProducts.form.fields.searchCity')
|
||||
const emptyLabel = noResultsLabel ?? t('customerProducts.form.fields.noCities')
|
||||
const clearAriaLabel = clearLabel ?? t('customerProducts.form.fields.clearCity')
|
||||
|
||||
const filtered = options.filter((option) => {
|
||||
const q = query.trim().toLowerCase()
|
||||
@@ -108,7 +114,7 @@ export function CitySearchSelect({
|
||||
type="button"
|
||||
className={styles.clearBtn}
|
||||
onClick={() => onChange('')}
|
||||
aria-label={t('customerProducts.form.fields.clearCity')}
|
||||
aria-label={clearAriaLabel}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -123,7 +129,7 @@ export function CitySearchSelect({
|
||||
{open && !disabled ? (
|
||||
<ul className={styles.dropdown} role="listbox">
|
||||
{filtered.length === 0 ? (
|
||||
<li className={styles.noResults}>{t('customerProducts.form.fields.noCities')}</li>
|
||||
<li className={styles.noResults}>{emptyLabel}</li>
|
||||
) : (
|
||||
filtered.map((option) => (
|
||||
<li key={option.id}>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
border-block: 1px solid color-mix(in srgb, var(--text-muted) 42%, transparent);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 0;
|
||||
max-width: 36rem;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.7;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action {
|
||||
appearance: none;
|
||||
display: inline;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cell {
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border-bottom: none !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styles from './EmptyState.module.css'
|
||||
|
||||
export interface EmptyStateProps {
|
||||
icon: LucideIcon
|
||||
message: string
|
||||
actionLabel?: string
|
||||
actionTo?: string
|
||||
onAction?: () => void
|
||||
}
|
||||
|
||||
export function EmptyState({
|
||||
icon: Icon,
|
||||
message,
|
||||
actionLabel,
|
||||
actionTo,
|
||||
onAction,
|
||||
}: EmptyStateProps) {
|
||||
const showAction = Boolean(actionLabel && (actionTo || onAction))
|
||||
|
||||
return (
|
||||
<div className={styles.root} role="status">
|
||||
<Icon className={styles.icon} size={36} strokeWidth={1.5} aria-hidden />
|
||||
<p className={styles.message}>{message}</p>
|
||||
{showAction && actionLabel ? (
|
||||
actionTo ? (
|
||||
<Link to={actionTo} className={styles.action} onClick={onAction}>
|
||||
{actionLabel}
|
||||
</Link>
|
||||
) : (
|
||||
<button type="button" className={styles.action} onClick={onAction}>
|
||||
{actionLabel}
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function EmptyTableRow({
|
||||
colSpan,
|
||||
...props
|
||||
}: EmptyStateProps & { colSpan: number }) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={colSpan} className={styles.cell}>
|
||||
<EmptyState {...props} />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ChevronLeft, ChevronRight, Plus } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Images, Plus } from 'lucide-react'
|
||||
import { useRef } from 'react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { WebsiteSliderSlide } from '../types/websiteSlider'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { WebsiteSliderSlideCard } from './WebsiteSliderSlideCard'
|
||||
import styles from './WebsiteSliderGallery.module.css'
|
||||
@@ -33,6 +34,17 @@ export function WebsiteSliderGallery({
|
||||
const showNav = slides.length > 0
|
||||
const addText = addLabel ?? t('website.sliders.addSlide')
|
||||
|
||||
if (slides.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={Images}
|
||||
message={t('website.sliders.empty')}
|
||||
actionLabel={showAdd ? addText : undefined}
|
||||
onAction={showAdd ? onAddSlide : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function scrollBy(direction: -1 | 1) {
|
||||
const track = trackRef.current
|
||||
if (!track) return
|
||||
|
||||
Reference in New Issue
Block a user