import product catalogue
This commit is contained in:
@@ -23,7 +23,6 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react';
|
||||
import { useFiestaStockStatement, useFiestaCreateStockRequest, useFiestaGetStockRequests, useFiestaUpdateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries';
|
||||
import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi';
|
||||
import { categoryName } from '../services/fiestaMappers';
|
||||
import { useStoreCatalogue } from '../services/storeCatalogue';
|
||||
import AwaitingApi from './AwaitingApi';
|
||||
import { SlideDrawer, StatusChip, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND } from './consoleUi';
|
||||
@@ -35,6 +34,7 @@ interface StoreCatalogViewProps {
|
||||
locationid?: number;
|
||||
storeName?: string;
|
||||
tenantId?: number;
|
||||
isSidebarOpen?: boolean;
|
||||
}
|
||||
|
||||
function stockStatus(closing: number): { label: string; color: string } {
|
||||
@@ -53,9 +53,10 @@ function catBadgeClass(category: string): string {
|
||||
return 'bg-rose-50 text-rose-600 border border-rose-100';
|
||||
}
|
||||
|
||||
export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID }: StoreCatalogViewProps) {
|
||||
export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID, isSidebarOpen = false }: StoreCatalogViewProps) {
|
||||
const tenantid = tenantId;
|
||||
const [view, setView] = useState<'catalogue' | 'inventory' | 'requests'>('catalogue');
|
||||
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [requestDate, setRequestDate] = useState(() => new Date().toISOString().split('T')[0]);
|
||||
@@ -70,16 +71,18 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
const storeCat = useStoreCatalogue();
|
||||
const products = useMemo(
|
||||
() =>
|
||||
storeCat.items.map((it) => ({
|
||||
id: it.productid,
|
||||
name: it.name,
|
||||
sku: it.sku || `SKU-${it.productid}`,
|
||||
image: it.image || PLACEHOLDER,
|
||||
category: it.category || 'General',
|
||||
price: it.price,
|
||||
unit: it.unit,
|
||||
adminQty: it.qty,
|
||||
})),
|
||||
storeCat.items
|
||||
.filter((it) => it.status === 'Active')
|
||||
.map((it) => ({
|
||||
id: it.productid,
|
||||
name: it.name,
|
||||
sku: it.sku || `SKU-${it.productid}`,
|
||||
image: it.image || PLACEHOLDER,
|
||||
category: it.category || 'General',
|
||||
price: it.price,
|
||||
unit: it.unit,
|
||||
adminQty: it.qty,
|
||||
})),
|
||||
[storeCat.items],
|
||||
);
|
||||
|
||||
@@ -160,6 +163,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
});
|
||||
return set;
|
||||
}, [stockQ.data, picks]);
|
||||
|
||||
const inventory = useMemo(
|
||||
() => {
|
||||
const baseInventory = (stockQ.data ?? []).map((r: Row) => {
|
||||
@@ -169,7 +173,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
name: fstr(r.productname) || 'Unnamed product',
|
||||
sku: fstr(r.sku) || `SKU-${fstr(r.productid)}`,
|
||||
image: fstr(r.productimage) || PLACEHOLDER,
|
||||
category: categoryName(fnum(r.categoryid)),
|
||||
category: fstr(r.categoryname) || 'General',
|
||||
closing,
|
||||
...stockStatus(closing),
|
||||
price: Math.floor(Math.random() * 50) + 10, // mock price
|
||||
@@ -252,77 +256,80 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
<div className="animate-in fade-in duration-300 font-sans pb-28">
|
||||
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center gap-1 bg-zinc-100/80 p-1 rounded-xl border border-zinc-200/60 w-full sm:w-auto sm:inline-flex">
|
||||
<button
|
||||
onClick={() => setView('catalogue')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'catalogue' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Boxes size={14} /> Browse Catalogue ({products.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('inventory')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'inventory' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Store size={14} /> My Store Inventory ({inventory.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('requests')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'requests' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Inbox size={14} /> My Requests ({Object.keys(picks).length})
|
||||
</button>
|
||||
{/* Tabs and Controls */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 w-full">
|
||||
<div className="flex items-center gap-1 bg-zinc-100/80 p-1 rounded-xl border border-zinc-200/60 w-full sm:w-auto sm:inline-flex">
|
||||
<button
|
||||
onClick={() => setView('catalogue')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'catalogue' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Boxes size={14} /> Browse Catalogue ({products.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('inventory')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'inventory' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Store size={14} /> My Store Inventory ({inventory.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('requests')}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||||
view === 'requests' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
|
||||
}`}
|
||||
>
|
||||
<Inbox size={14} /> My Requests ({Object.keys(picks).length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{view === 'requests' && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">Date:</span>
|
||||
<input
|
||||
type="date"
|
||||
value={requestDate}
|
||||
onChange={(e) => setRequestDate(e.target.value)}
|
||||
className="px-3 py-1.5 border border-slate-200 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#662582] focus:border-[#662582] text-xs font-semibold text-slate-700 bg-white"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-6 items-start mt-4">
|
||||
<div className="flex flex-col md:flex-row gap-6 mt-2 flex-1 min-h-0 overflow-hidden pb-4">
|
||||
{/* Sticky Sidebar Filter */}
|
||||
{view !== 'requests' && (
|
||||
<div className="w-full md:w-64 shrink-0 bg-white border border-slate-200 rounded-2xl p-5 sticky top-24 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] z-10 hidden md:flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
|
||||
<Search size={14} className="text-[#662582]" /> Search
|
||||
</h3>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Product name or SKU..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-3 pr-8 py-2.5 border border-[#e2e8f0] rounded-xl text-xs font-semibold outline-none bg-slate-50 focus:bg-white focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-all shadow-sm"
|
||||
/>
|
||||
{search && (
|
||||
<button onClick={() => setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 cursor-pointer">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{view !== 'requests' && isLocalSidebarOpen && (
|
||||
<div className="w-full xl:w-64 shrink-0 flex flex-col gap-5 pr-2 pb-2">
|
||||
<div className="bg-white border border-slate-200 shadow-sm overflow-hidden flex-1 min-h-0 flex flex-col">
|
||||
<div className="p-4 bg-slate-50 border-b border-slate-100 shrink-0">
|
||||
<h3 className="font-bold text-slate-800 flex items-center gap-2">
|
||||
<Layers size={16} className="text-[#662582]" /> Filter Product
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 space-y-5 overflow-y-auto custom-scrollbar">
|
||||
|
||||
<div className="w-full h-px bg-slate-100" />
|
||||
|
||||
<div>
|
||||
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
|
||||
<Layers size={14} className="text-[#662582]" /> Categories
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{categories.map((c) => (
|
||||
<label key={c} className="flex items-center gap-2.5 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategories.includes(c)}
|
||||
onChange={() => toggleCategory(c)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded-[4px] border flex items-center justify-center transition-colors ${selectedCategories.includes(c) ? 'bg-[#662582] border-[#662582]' : 'bg-slate-50 border-slate-300 group-hover:border-purple-400'}`}>
|
||||
{selectedCategories.includes(c) && <Check size={10} className="text-white" strokeWidth={3} />}
|
||||
<label key={c} className="flex items-center gap-3 p-2 rounded-xl hover:bg-slate-50 cursor-pointer group transition-colors">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedCategories.includes(c)}
|
||||
onChange={() => toggleCategory(c)}
|
||||
className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-lg checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
|
||||
/>
|
||||
<Check size={12} className="absolute text-white opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-700 group-hover:text-slate-900">{c}</span>
|
||||
<span className={`text-xs font-semibold select-none transition-colors ${selectedCategories.includes(c) ? 'text-[#662582]' : 'text-slate-600 group-hover:text-slate-900'}`}>
|
||||
{c}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -335,19 +342,21 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
|
||||
<Activity size={14} className="text-[#662582]" /> Stock Health
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1.5">
|
||||
{['Healthy', 'Low', 'Critical', 'Out of stock'].map((h) => (
|
||||
<label key={h} className="flex items-center gap-2.5 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stockHealthFilter.includes(h)}
|
||||
onChange={() => toggleStockHealth(h)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`w-4 h-4 rounded-[4px] border flex items-center justify-center transition-colors ${stockHealthFilter.includes(h) ? 'bg-[#662582] border-[#662582]' : 'bg-slate-50 border-slate-300 group-hover:border-purple-400'}`}>
|
||||
{stockHealthFilter.includes(h) && <Check size={10} className="text-white" strokeWidth={3} />}
|
||||
<label key={h} className="flex items-center gap-3 p-2 rounded-xl hover:bg-slate-50 cursor-pointer group transition-colors">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={stockHealthFilter.includes(h)}
|
||||
onChange={() => toggleStockHealth(h)}
|
||||
className="peer appearance-none w-5 h-5 border-2 border-slate-300 rounded-lg checked:border-[#662582] checked:bg-[#662582] transition-colors cursor-pointer"
|
||||
/>
|
||||
<Check size={12} className="absolute text-white opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-700 group-hover:text-slate-900">{h}</span>
|
||||
<span className={`text-xs font-semibold select-none transition-colors ${stockHealthFilter.includes(h) ? 'text-[#662582]' : 'text-slate-600 group-hover:text-slate-900'}`}>
|
||||
{h}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@@ -363,32 +372,17 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
Clear All Filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Grid Area */}
|
||||
<div className="flex-1 min-w-0 max-h-[850px] overflow-y-auto custom-scrollbar pr-4">
|
||||
{/* Mobile Search Input (Visible only on mobile) */}
|
||||
<div className="md:hidden mb-4">
|
||||
<div className="relative">
|
||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={view === 'catalogue' ? 'Search catalogue...' : 'Search your stock...'}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full pl-9 pr-9 py-3 border border-[#e2e8f0] rounded-xl text-sm font-semibold outline-none bg-white focus:ring-2 focus:ring-purple-500/20 shadow-sm"
|
||||
/>
|
||||
{search && (
|
||||
<button onClick={() => setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 cursor-pointer">
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 overflow-y-auto custom-scrollbar bg-white/40 backdrop-blur-md border border-[#e2e8f0] p-5 shadow-sm flex flex-col">
|
||||
|
||||
|
||||
{/* Results Summary & Toolbar */}
|
||||
<div className="flex justify-between items-end mb-4 px-1">
|
||||
<div className="flex justify-between items-end mb-4 px-1 gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-slate-900">
|
||||
{view === 'catalogue' ? 'Store Catalogue' : view === 'inventory' ? 'My Inventory' : 'My Requests'}
|
||||
@@ -399,7 +393,31 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Can add sorting dropdown here if needed */}
|
||||
{/* Action Toolbar */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setIsLocalSidebarOpen(!isLocalSidebarOpen)}
|
||||
className="flex items-center gap-2 bg-white border border-slate-200 px-3 py-2 rounded-xl text-xs font-bold hover:bg-slate-50 transition-colors text-slate-700 shadow-sm cursor-pointer h-[36px]"
|
||||
>
|
||||
<Layers size={14} /> Filter
|
||||
</button>
|
||||
|
||||
<div className="relative hidden md:block md:w-64 h-[36px]">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={view === 'catalogue' ? 'Search catalogue...' : 'Search your stock...'}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full h-full pl-9 pr-8 py-2 bg-white border border-slate-200 rounded-xl text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-[#662582] transition-all shadow-sm font-medium"
|
||||
/>
|
||||
{search && (
|
||||
<button onClick={() => setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 cursor-pointer border-none bg-transparent">
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Browse Catalogue ── */}
|
||||
@@ -417,7 +435,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
sub="Try a different keyword or clear the filters to see the full catalogue."
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-4 pb-8">
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 ${!isLocalSidebarOpen ? (!isSidebarOpen ? 'xl:grid-cols-5 2xl:grid-cols-6' : 'xl:grid-cols-4 2xl:grid-cols-5') : (!isSidebarOpen ? 'xl:grid-cols-4 2xl:grid-cols-5' : 'xl:grid-cols-3 2xl:grid-cols-4')} gap-4 pb-8`}>
|
||||
{filtered.map((p) => {
|
||||
const stocked = inStore.has(p.id);
|
||||
const pick = picks[p.id];
|
||||
@@ -516,19 +534,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
{/* ── My Requests ── */}
|
||||
{view === 'requests' && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-end bg-white px-4 py-3 rounded-2xl border border-slate-200 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<label htmlFor="requestDate" className="text-sm font-bold text-slate-500">Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="requestDate"
|
||||
value={requestDate}
|
||||
onChange={(e) => setRequestDate(e.target.value)}
|
||||
className="px-3 py-1.5 border border-slate-200 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-sm font-medium text-slate-700 bg-slate-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(!stockRequestsQ.data || stockRequestsQ.data.length === 0) ? (
|
||||
<CenterState
|
||||
icon={<Inbox size={34} />}
|
||||
@@ -655,13 +660,13 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
|
||||
title="No stock matches your filters"
|
||||
sub="Try a different keyword or clear the sidebar filters to find an item in your store."
|
||||
action={
|
||||
<button onClick={() => { setSearch(''); setSelectedCategories([]); setStockHealthFilter([]); }} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer">
|
||||
<button onClick={() => { setSearch(''); setSelectedCategories([]); setStockHealthFilter([]); }} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-[#531e6a] transition shadow-sm cursor-pointer">
|
||||
<X size={13} /> Clear filters
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-4">
|
||||
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 ${!isLocalSidebarOpen ? (!isSidebarOpen ? 'xl:grid-cols-5 2xl:grid-cols-6' : 'xl:grid-cols-4 2xl:grid-cols-5') : (!isSidebarOpen ? 'xl:grid-cols-4 2xl:grid-cols-5' : 'xl:grid-cols-3 2xl:grid-cols-4')} gap-4 pb-8`}>
|
||||
{finalFilteredInventory.map((it, i) => (
|
||||
<div key={it.id || i} onClick={() => setSelectedProduct(it)} className="cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden">
|
||||
{/* Image Bento Block */}
|
||||
@@ -944,14 +949,14 @@ function CenterState({ icon, title, sub, action }: { icon: React.ReactNode; titl
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-gradient-to-b from-white to-[#faf9ff] border border-[#eceef2] rounded-3xl px-6 py-16 sm:py-20 text-center shadow-sm">
|
||||
{/* Soft decorative glows */}
|
||||
<div className="pointer-events-none absolute -top-20 -right-20 w-60 h-60 rounded-full bg-purple-200/30 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-24 -left-20 w-60 h-60 rounded-full bg-indigo-200/30 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -top-20 -right-20 w-60 h-60 rounded-full bg-[#662582]/10 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -bottom-24 -left-20 w-60 h-60 rounded-full bg-[#662582]/10 blur-3xl" />
|
||||
|
||||
<div className="relative flex flex-col items-center">
|
||||
{/* Icon with halo */}
|
||||
<div className="relative mb-5">
|
||||
<span className="absolute inset-0 -m-3 rounded-full bg-purple-300/25 blur-xl" />
|
||||
<span className="relative flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-[#662582] to-indigo-500 text-white shadow-lg shadow-purple-500/20 ring-8 ring-white">
|
||||
<span className="absolute inset-0 -m-3 rounded-full bg-[#662582]/15 blur-xl" />
|
||||
<span className="relative flex items-center justify-center w-20 h-20 rounded-3xl bg-[#662582] text-white shadow-lg shadow-[#662582]/20 ring-8 ring-white">
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user