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:
Alireza Hassani
2026-08-19 19:46:57 +03:30
co-authored by Cursor
parent 832ac32a09
commit f8a9a28346
55 changed files with 1264 additions and 907 deletions
@@ -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