Dashboard search made functional

This commit is contained in:
Sidharth Prabhu
2026-04-21 14:05:55 +05:30
parent 0c65542fb9
commit 3e35bacc14
9 changed files with 289 additions and 65 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
Ritz/.DS_Store vendored

Binary file not shown.

Binary file not shown.

BIN
Ritz/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -1,7 +1,58 @@
import { Bell, Search, Settings, HelpCircle, Calendar } from 'lucide-react'; import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import {
Bell,
Search,
Settings,
HelpCircle,
Calendar,
LayoutDashboard,
Store,
ShoppingBag,
History,
Users,
Package,
Layers,
FileText,
MessageSquare,
Wallet,
Ticket,
Cpu,
UserCheck,
TrendingUp,
ChevronRight
} from 'lucide-react';
import { Link, useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
const PAGES = [
{ label: 'General Dashboard', path: '/dashboard', category: 'Analytics', icon: <LayoutDashboard size={14} /> },
{ label: 'Store Dashboard', path: '/store-dashboard', category: 'Analytics', icon: <Store size={14} /> },
{ label: 'Active Orders', path: '/sale/orders', category: 'Sales', icon: <ShoppingBag size={14} /> },
{ label: 'Archived Bills', path: '/sale/archived-bills', category: 'Sales', icon: <History size={14} /> },
{ label: 'Manage Products', path: '/inventory/products', category: 'Inventory', icon: <Package size={14} /> },
{ label: 'Base Menu', path: '/inventory/base', category: 'Inventory', icon: <Layers size={14} /> },
{ label: 'Ritz Overview', path: '/ritz/overview', category: 'Finance', icon: <Wallet size={14} /> },
{ label: 'Manage Wallets', path: '/ritz/wallets', category: 'Finance', icon: <Users size={14} /> },
{ label: 'Coupon Codes', path: '/ritz/coupons', category: 'Finance', icon: <Ticket size={14} /> },
{ label: 'Reports & Analytics', path: '/reports', category: 'Support', icon: <FileText size={14} /> },
{ label: 'Customer Feedback', path: '/feedback', category: 'Support', icon: <MessageSquare size={14} /> },
{ label: 'Staff Management', path: '/stores/staffs', category: 'Team', icon: <UserCheck size={14} /> },
{ label: 'Store Terminals', path: '/stores/terminals', category: 'System', icon: <Cpu size={14} /> },
{ label: 'System Settings', path: '/settings', category: 'System', icon: <Settings size={14} /> },
];
const Header = () => { const Header = () => {
const navigate = useNavigate();
const searchRef = useRef<HTMLDivElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const [showResults, setShowResults] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const filteredPages = PAGES.filter(page =>
page.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
page.category.toLowerCase().includes(searchTerm.toLowerCase())
).slice(0, 8);
const currentDate = new Date().toLocaleDateString('en-US', { const currentDate = new Date().toLocaleDateString('en-US', {
weekday: 'long', weekday: 'long',
year: 'numeric', year: 'numeric',
@@ -9,16 +60,97 @@ const Header = () => {
day: 'numeric', day: 'numeric',
}); });
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setShowResults(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
setSelectedIndex(prev => (prev + 1) % filteredPages.length);
} else if (e.key === 'ArrowUp') {
setSelectedIndex(prev => (prev - 1 + filteredPages.length) % filteredPages.length);
} else if (e.key === 'Enter' && filteredPages[selectedIndex]) {
handleNavigate(filteredPages[selectedIndex].path);
} else if (e.key === 'Escape') {
setShowResults(false);
}
};
const handleNavigate = (path: string) => {
navigate(path);
setSearchTerm('');
setShowResults(false);
};
return ( return (
<header className="h-16 bg-white/80 backdrop-blur-md border-b border-[#e2e8f0] px-8 flex items-center justify-between sticky top-0 z-40"> <header className="h-16 bg-white/80 backdrop-blur-md border-b border-[#e2e8f0] px-8 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-4 flex-1"> <div className="flex items-center gap-4 flex-1">
<div className="relative group w-full max-w-md"> <div className="relative group w-full max-w-md" ref={searchRef}>
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[#64748b] group-focus-within:text-[#231651] transition-colors" size={18} /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-[#64748b] group-focus-within:text-[#0f4475] transition-colors" size={18} />
<input <input
type="text" type="text"
placeholder="Search dashboard..." placeholder="Search pages, settings, or analytics..."
className="w-full bg-gray-50 border border-transparent rounded-lg py-2 pl-10 pr-4 text-sm focus:outline-none focus:bg-white focus:border-[#231651]/30 transition-all shadow-sm" className="w-full bg-gray-50 border border-transparent rounded-lg py-2 pl-10 pr-4 text-sm focus:outline-none focus:bg-white focus:border-[#0f4475]/30 transition-all shadow-sm"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setShowResults(true);
setSelectedIndex(0);
}}
onFocus={() => setShowResults(true)}
onKeyDown={handleKeyDown}
/> />
<AnimatePresence>
{showResults && searchTerm && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 5, scale: 0.95 }}
transition={{ duration: 0.1 }}
className="absolute top-full left-0 right-0 mt-2 bg-white/95 backdrop-blur-xl border border-[#e2e8f0] rounded-xl shadow-2xl overflow-hidden z-50"
>
{filteredPages.length > 0 ? (
<div className="py-2">
{filteredPages.map((page, index) => (
<button
key={page.path}
onClick={() => handleNavigate(page.path)}
onMouseEnter={() => setSelectedIndex(index)}
className={`w-full px-4 py-2.5 flex items-center justify-between transition-colors ${index === selectedIndex ? 'bg-[#0f4475]/5' : ''}`}
>
<div className="flex items-center gap-3">
<div className={`p-1.5 rounded-lg transition-colors ${index === selectedIndex ? 'bg-[#0f4475] text-white' : 'bg-gray-100 text-gray-400'}`}>
{page.icon}
</div>
<div className="text-left">
<div className={`text-sm font-bold ${index === selectedIndex ? 'text-[#0f4475]' : 'text-slate-700'}`}>
{page.label}
</div>
<div className="text-[10px] uppercase tracking-widest font-black opacity-40">
{page.category}
</div>
</div>
</div>
<ChevronRight className={`transition-transform duration-300 ${index === selectedIndex ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0'}`} size={14} />
</button>
))}
</div>
) : (
<div className="p-8 text-center">
<HelpCircle className="mx-auto text-slate-200 mb-2" size={32} />
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">No matching pages found</p>
</div>
)}
</motion.div>
)}
</AnimatePresence>
</div> </div>
</div> </div>
@@ -34,7 +166,7 @@ const Header = () => {
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span> <span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border-2 border-white"></span>
</button> </button>
<Link to="/settings" className="p-2 text-[#64748b] hover:text-[#231651] hover:bg-indigo-50 rounded-lg transition-all"> <Link to="/settings" className="p-2 text-[#64748b] hover:text-[#0f4475] hover:bg-indigo-50 rounded-lg transition-all">
<Settings size={20} /> <Settings size={20} />
</Link> </Link>

View File

@@ -38,43 +38,66 @@
cursor: default; cursor: default;
} }
.ritz-payment-card.insufficient { .ritz-payment-card.insufficient-state {
opacity: 0.8; border: 1px solid rgba(244, 63, 94, 0.3);
border-style: dashed; background: rgba(244, 63, 94, 0.03);
background: rgba(239, 68, 68, 0.02); position: relative;
overflow: hidden;
} }
.ritz-icon { .ritz-payment-card.insufficient-state::before {
background: #6366f1; content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: #f43f5e;
}
.status-badge-premium {
font-size: 0.65rem;
font-weight: 900;
padding: 0.25rem 0.75rem;
border-radius: 99px;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.status-badge-premium.error {
background: #f43f5e;
color: white; color: white;
} }
.payment-icon.ritz-token-avatar {
background: transparent;
width: 54px;
height: 54px;
transform: scale(1.1);
filter: drop-shadow(0 4px 12px rgba(99, 102, 241, 0.2));
}
.token-image-main {
width: 100%;
height: 100%;
object-fit: contain;
}
.payment-name-row { .payment-name-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 0.5rem; margin-bottom: 4px;
}
.status-badge {
font-size: 0.7rem;
font-weight: 800;
padding: 0.2rem 0.6rem;
border-radius: 20px;
text-transform: uppercase;
}
.status-badge.error {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
} }
.wallet-balance-info { .wallet-balance-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 6px;
font-size: 0.85rem; font-size: 0.8rem;
color: var(--text-secondary); color: #64748b;
font-weight: 500;
} }
.balance-val { .balance-val {
@@ -82,40 +105,49 @@
color: #6366f1; color: #6366f1;
} }
.insufficient-val {
color: #f43f5e !important;
text-decoration: line-through;
opacity: 0.7;
}
.add-tokens-checkout-btn { .add-tokens-checkout-btn {
width: 100%; width: 100%;
margin-top: 1rem; margin-top: 12px;
background: white; background: white;
border: 1px dashed #6366f1; border: 1.5px dashed rgba(99, 102, 241, 0.4);
padding: 1rem; padding: 1rem;
border-radius: 16px; border-radius: 18px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: #6366f1; color: #6366f1;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
} }
.add-tokens-checkout-btn:active { .add-tokens-checkout-btn:active {
background: rgba(99, 102, 241, 0.05); background: rgba(99, 102, 241, 0.05);
transform: scale(0.98);
} }
.add-tokens-checkout-btn .btn-content { .add-tokens-checkout-btn .btn-content {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 10px;
font-weight: 700; font-weight: 800;
font-size: 0.9rem;
} }
.payment-security-note { .payment-security-note {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 6px;
margin-top: 1.5rem; margin-top: 1.5rem;
color: var(--text-secondary); color: #94a3b8;
font-size: 0.8rem; font-size: 0.75rem;
font-weight: 500;
} }
.ritz-text { .ritz-text {
@@ -123,26 +155,66 @@
font-weight: 800; font-weight: 800;
} }
.insufficient-warning { .insufficient-warning-premium {
margin: 1.5rem; margin: 1.5rem;
padding: 1rem; padding: 1.25rem;
background: rgba(255, 149, 0, 0.1); background: white;
border-radius: 16px; border: 1px solid rgba(244, 63, 94, 0.1);
border-radius: 24px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1.25rem;
color: #cc7700; box-shadow: 0 12px 30px -10px rgba(244, 63, 94, 0.15);
} }
.warning-text h3 { .token-warning-visual {
font-size: 0.95rem; width: 60px;
font-weight: 700; height: 60px;
margin-bottom: 0.1rem; position: relative;
background: transparent;
filter: drop-shadow(0 6px 15px rgba(244, 63, 94, 0.2));
} }
.warning-text p { .token-image-mini {
width: 100%;
height: 100%;
object-fit: contain;
transform: rotate(-10deg);
}
.warning-icon-overlay {
position: absolute;
bottom: -2px;
right: -2px;
width: 24px;
height: 24px;
background: #f43f5e;
color: white;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid white;
box-shadow: 0 4px 8px rgba(244, 63, 94, 0.3);
}
.shortfall-amount {
font-size: 1rem;
font-weight: 800;
color: #334155;
margin-bottom: 0.25rem;
}
.shortfall-amount .highlight {
color: #f43f5e;
font-size: 1.1rem;
}
.warning-instruction {
font-size: 0.8rem; font-size: 0.8rem;
opacity: 0.9; font-weight: 500;
color: #64748b;
margin: 0;
} }
.ritz-order-btn { .ritz-order-btn {

View File

@@ -8,6 +8,7 @@ import {
ShieldCheck, ShieldCheck,
PlusCircle PlusCircle
} from 'lucide-react'; } from 'lucide-react';
import tokenImage from '../assets/display_ritz.png';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Header from '../components/Header'; import Header from '../components/Header';
import StockConflictModal from '../components/StockConflictModal'; import StockConflictModal from '../components/StockConflictModal';
@@ -139,21 +140,29 @@ const CheckoutScreen: React.FC = () => {
</div> </div>
<div className="payment-options"> <div className="payment-options">
<div className={`payment-card ritz-payment-card ${isInsufficient ? 'insufficient' : 'selected'}`}> <div className={`payment-card ritz-payment-card ${isInsufficient ? 'insufficient-state' : 'selected'}`}>
<div className="payment-icon ritz-icon"> <div className="payment-icon ritz-token-avatar">
<CircleDollarSign size={24} /> <img src={tokenImage} alt="Ritz Token" className="token-image-main" />
</div> </div>
<div className="payment-info"> <div className="payment-info">
<div className="payment-name-row"> <div className="payment-name-row">
<span className="payment-name">Pay with Ritz Tokens</span> <span className="payment-name">Pay with Ritz Tokens</span>
{isInsufficient && ( {isInsufficient && (
<span className="status-badge error">Insufficient Balance</span> <motion.span
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="status-badge-premium error"
>
Insufficient
</motion.span>
)} )}
</div> </div>
<div className="wallet-balance-info"> <div className="wallet-balance-info">
<Wallet size={14} /> <Wallet size={14} className={isInsufficient ? 'text-rose-400' : ''} />
<span>Current Balance: </span> <span>Your Balance: </span>
<span className="balance-val">R{currentBalance.toLocaleString()}</span> <span className={`balance-val ${isInsufficient ? 'insufficient-val' : ''}`}>
R{currentBalance.toLocaleString()}
</span>
</div> </div>
</div> </div>
{!isInsufficient && ( {!isInsufficient && (
@@ -194,13 +203,24 @@ const CheckoutScreen: React.FC = () => {
</div> </div>
{isInsufficient && ( {isInsufficient && (
<div className="insufficient-warning"> <motion.div
<AlertCircle size={20} /> className="insufficient-warning-premium"
<div className="warning-text"> initial={{ opacity: 0, y: 20 }}
<strong>Short by R{(totalPrice - currentBalance).toLocaleString()}</strong> animate={{ opacity: 1, y: 0 }}
<p>Add more tokens to complete your order.</p> >
<div className="warning-icon-wrapper token-warning-visual">
<img src={tokenImage} alt="" className="token-image-mini" />
<div className="warning-icon-overlay">
<AlertCircle size={16} />
</div> </div>
</div> </div>
<div className="warning-content">
<div className="shortfall-amount">
Short by <span className="highlight">R{(totalPrice - currentBalance).toLocaleString()}</span>
</div>
<p className="warning-instruction">Add tokens to your wallet to complete this order.</p>
</div>
</motion.div>
)} )}
</main> </main>