fix: OrderCard layout and filter UX improvements

Fixed all issues from user feedback round 2.

1. OrderCard Layout - Icon Inline with 2 Lines
   Problem: Too much vertical space wasted, icon in separate column

   New Layout:
   ┌─────────────────────────────────┐
   │ ☐ [Icon] Nov 04, 2025, 11:44 PM │ ← Line 1: Date (small)
   │         #337                  →│ ← Line 2: Order# (big)
   │         Dwindi Ramadhana        │ ← Line 3: Customer
   │         1 item · Test Digital   │ ← Line 4: Items
   │         Rp64.500    Completed   │ ← Line 5: Total + Status
   └─────────────────────────────────┘

   Changes:
   - Icon inline with first 2 lines (date + order#)
   - Date: text-xs, muted, top line
   - Order#: text-lg, bold, second line
   - Better space utilization
   - Reduced padding: p-4 → p-3
   - Cleaner hierarchy

   Result: More compact, better use of horizontal space!

2. FilterBottomSheet Backdrop Margin
   Problem: Backdrop had top margin from parent space-y-4

   Fix:
   - Added !m-0 to backdrop to override parent spacing
   - Backdrop now properly covers entire screen

   Result: Clean full-screen overlay!

3. DateRange Component Fixes
   Problem:
   - Horizontal overflow when custom dates selected
   - WP forms.css overriding input styles
   - Redundant "Apply" button

   Fixes:
   a) Layout:
      - Changed from horizontal to vertical (flex-col)
      - Full width inputs (w-full)
      - Prevents overflow in bottom sheet

   b) Styling:
      - Override WP forms.css with shadcn classes
      - border-input, bg-background, ring-offset-background
      - focus-visible:ring-2 focus-visible:ring-ring
      - WebkitAppearance: none to remove browser defaults
      - Custom calendar picker cursor

   c) Instant Filtering:
      - Removed "Apply" button
      - Added start/end to useEffect deps
      - Filters apply immediately on date change

   Result: Clean vertical layout, proper styling, instant filtering!

4. Filter Bottom Sheet UX
   Problem: Apply/Cancel buttons confusing (filters already applied)

   Industry Standard: Instant filtering on mobile
   - Gmail: Filters apply instantly
   - Amazon: Filters apply instantly
   - Airbnb: Filters apply instantly

   Solution:
   - Removed "Apply" button
   - Removed "Cancel" button
   - Keep "Clear all filters" button (only when filters active)
   - Filters apply instantly on change
   - User can close sheet anytime (tap backdrop or X)

   Result: Modern, intuitive mobile filter UX!

Files Modified:
- routes/Orders/components/OrderCard.tsx
- routes/Orders/components/FilterBottomSheet.tsx
- components/filters/DateRange.tsx

Summary:
 OrderCard: Icon inline, better space usage
 Backdrop: No margin, full coverage
 DateRange: Vertical layout, no overflow, proper styling
 Filters: Instant application, industry standard UX
 Clean, modern, mobile-first! 🎯
This commit is contained in:
dwindown
2025-11-08 14:02:02 +07:00
parent c62fbd9436
commit ff485a889a
3 changed files with 69 additions and 74 deletions

View File

@@ -47,12 +47,12 @@ export default function DateRange({ value, onChange }: Props) {
setEnd(pr.date_end); setEnd(pr.date_end);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [preset]); }, [preset, start, end]);
return ( return (
<div className="flex items-center gap-2"> <div className="flex flex-col gap-2 w-full">
<Select value={preset} onValueChange={(v) => setPreset(v)}> <Select value={preset} onValueChange={(v) => setPreset(v)}>
<SelectTrigger className="min-w-[140px]"> <SelectTrigger className="w-full">
<SelectValue placeholder={__("Last 7 days")} /> <SelectValue placeholder={__("Last 7 days")} />
</SelectTrigger> </SelectTrigger>
<SelectContent position="popper" className="z-[1000]"> <SelectContent position="popper" className="z-[1000]">
@@ -66,26 +66,23 @@ export default function DateRange({ value, onChange }: Props) {
</Select> </Select>
{preset === "custom" && ( {preset === "custom" && (
<div className="flex items-center gap-2"> <div className="flex flex-col gap-2 w-full">
<input <input
type="date" type="date"
className="border rounded-md px-3 py-2 text-sm" className="w-full border border-input rounded-md px-3 py-2 text-sm bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:cursor-pointer"
style={{ WebkitAppearance: 'none', MozAppearance: 'textfield' } as any}
value={start || ""} value={start || ""}
onChange={(e) => setStart(e.target.value || undefined)} onChange={(e) => setStart(e.target.value || undefined)}
placeholder={__("Start date")}
/> />
<span className="opacity-60 text-sm">{__("to")}</span>
<input <input
type="date" type="date"
className="border rounded-md px-3 py-2 text-sm" className="w-full border border-input rounded-md px-3 py-2 text-sm bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-calendar-picker-indicator]:cursor-pointer"
style={{ WebkitAppearance: 'none', MozAppearance: 'textfield' } as any}
value={end || ""} value={end || ""}
onChange={(e) => setEnd(e.target.value || undefined)} onChange={(e) => setEnd(e.target.value || undefined)}
placeholder={__("End date")}
/> />
<button
className="border rounded-md px-3 py-2 text-sm"
onClick={() => onChange?.({ date_start: start, date_end: end, preset })}
>
{__("Apply")}
</button>
</div> </div>
)} )}
</div> </div>

View File

@@ -42,9 +42,9 @@ export function FilterBottomSheet({
return ( return (
<> <>
{/* Backdrop */} {/* Backdrop - use !m-0 to override parent spacing */}
<div <div
className="fixed inset-0 bg-black/50 z-[60] md:hidden" className="!m-0 fixed inset-0 bg-black/50 z-[60] md:hidden"
onClick={onClose} onClick={onClose}
/> />
@@ -67,7 +67,7 @@ export function FilterBottomSheet({
</div> </div>
{/* Content */} {/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-24"> <div className="flex-1 overflow-y-auto p-4 space-y-4">
{/* Status Filter */} {/* Status Filter */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">{__('Status')}</label> <label className="text-sm font-medium">{__('Status')}</label>
@@ -129,24 +129,21 @@ export function FilterBottomSheet({
</div> </div>
</div> </div>
{/* Footer */} {/* Footer - Only show Reset if filters active */}
<div className="sticky bottom-0 bg-background border-t p-4 flex gap-3"> {hasActiveFilters && (
{hasActiveFilters && ( <div className="sticky bottom-0 bg-background border-t p-4">
<Button <Button
variant="outline" variant="outline"
onClick={onReset} onClick={() => {
className="flex-1" onReset();
onClose();
}}
className="w-full"
> >
{__('Reset')} {__('Clear all filters')}
</Button> </Button>
)} </div>
<Button )}
onClick={onClose}
className="flex-1"
>
{__('Apply')} {hasActiveFilters && `(${activeFiltersCount})`}
</Button>
</div>
</div> </div>
</> </>
); );

View File

@@ -29,9 +29,9 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
return ( return (
<Link <Link
to={`/orders/${order.id}`} to={`/orders/${order.id}`}
className="block bg-card border border-border rounded-xl p-4 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm" className="block bg-card border border-border rounded-xl p-3 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm"
> >
<div className="flex items-start gap-4"> <div className="flex items-center gap-3">
{/* Checkbox */} {/* Checkbox */}
{onSelect && ( {onSelect && (
<div <div
@@ -40,7 +40,6 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
e.stopPropagation(); e.stopPropagation();
onSelect(order.id); onSelect(order.id);
}} }}
className="pt-0.5"
> >
<Checkbox <Checkbox
checked={selected} checked={selected}
@@ -50,53 +49,55 @@ export function OrderCard({ order, selected, onSelect, currencyConfig }: OrderCa
</div> </div>
)} )}
{/* Icon */} <div className="flex flex-1 flex-col">
<div className="flex-shrink-0 w-12 h-12 rounded-xl bg-primary/10 text-primary flex items-center justify-center"> {/* Icon - inline with first 2 lines */}
<Package className="w-6 h-6" /> <div className="flex items-center gap-2">
</div> <div className={`flex-shrink-0 p-2 rounded-xl flex items-center justify-center ${statusClass}`}>
{/* Line 2: Order Number (big) */}
<span className="font-bold text-lg leading-tight">#{order.number}</span>
</div>
{/* Content */} <div className="flex flex-col">
<div className="flex-1 min-w-0 space-y-2"> {/* Line 1: Date (small) */}
{/* Order Number & Status */} <div className="text-xs text-muted-foreground leading-tight mb-0.5">
<div className="flex items-start justify-between gap-3"> {formatRelativeOrDate(order.date_ts)}
<h3 className="font-bold text-lg leading-tight">#{order.number}</h3> </div>
<span className={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold capitalize whitespace-nowrap ${statusClass}`}> </div>
{order.status || 'unknown'}
</span>
</div>
{/* Customer */}
<div className="text-sm font-medium text-foreground">
{order.customer || __('Guest')}
</div>
{/* Items Brief */}
{order.items_brief && (
<div className="text-sm text-muted-foreground truncate">
{order.items_count} {order.items_count === 1 ? __('item') : __('items')} · {order.items_brief}
</div> </div>
)}
{/* Date & Total */} {/* Content - 2 lines inline with icon */}
<div className="flex items-center justify-between gap-3 pt-1"> <div className="flex-1 min-w-0">
<span className="text-xs text-muted-foreground"> {/* Line 3: Customer */}
{formatRelativeOrDate(order.date_ts)} <div className="text-lg font-medium text-foreground mb-1">
</span> {order.customer || __('Guest')}
<span className="font-bold text-lg tabular-nums text-primary"> </div>
{formatMoney(order.total, {
currency: order.currency || currencyConfig.currency, {/* Line 4: Items */}
symbol: order.currency_symbol || currencyConfig.symbol, {order.items_brief && (
thousandSep: currencyConfig.thousand_sep, <div className="text-sm text-muted-foreground truncate mb-2">
decimalSep: currencyConfig.decimal_sep, {order.items_count} {order.items_count === 1 ? __('item') : __('items')} · {order.items_brief}
position: currencyConfig.position, </div>
decimals: currencyConfig.decimals, )}
})}
</span> {/* Line 5: Total & Status */}
</div> <div className="flex items-center justify-between gap-2">
<span className="font-bold text-base tabular-nums">
{formatMoney(order.total, {
currency: order.currency || currencyConfig.currency,
symbol: order.currency_symbol || currencyConfig.symbol,
thousandSep: currencyConfig.thousand_sep,
decimalSep: currencyConfig.decimal_sep,
position: currencyConfig.position,
decimals: currencyConfig.decimals,
})}
</span>
</div>
</div>
</div> </div>
{/* Chevron */} {/* Chevron */}
<ChevronRight className="w-5 h-5 text-muted-foreground flex-shrink-0 self-center" /> <ChevronRight className="w-5 h-5 text-muted-foreground flex-shrink-0" />
</div> </div>
</Link> </Link>
); );